Skip to content
Open
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
63 changes: 36 additions & 27 deletions giga/evmonly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ 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.StateDB`
- direct snapshot reads and ordered state commits through the Giga `StateDB`
- 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
- receipt persistence through a `ReceiptStore`, with a concurrency-safe
in-memory implementation for the ephemeral runtime
- a versioned `MemoryStore` giga implementation over an immutable `StateReader`
for tests and load generation
- fail-closed custom precompile placeholders
Expand Down Expand Up @@ -70,34 +72,41 @@ 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 is always store-backed. `WithStore(...)` selects the `giga.StateDB`
implementation and its `NamedChangeSetEncoder`; execution fails closed if
either is missing. For each block the executor opens a current
`giga.StateView`, 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.StateDB` defines the protobuf commit
The executor is always store-backed. `WithStorageManager(...)` selects the
`bootstrap.GigaStorageManager` that provides both the Giga `StateDB` and the ledger
receipt store, plus the `NamedChangeSetEncoder` for its state implementation.
Execution fails closed if the manager, either store, or the encoder is missing.
For each block the executor opens a current `giga.StateView`, 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 the Giga `StateDB` 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.
persisted slots that were not read during execution. Encoding, state commit, or
receipt-store failures release the block result and return an error without
invoking `ResultSink`. Ethereum receipts are converted into
`receipt.ReceiptRecord` values and persisted through the shared
`receipt.ReceiptStore` interface before the height-advancing state commit,
including for empty blocks. A receipt failure leaves state unchanged so the
block can be retried. A state failure can leave receipts behind, but retrying
the block overwrites them. `ResultSink` runs only after both stores succeed.

`MemoryStore` and `MemoryReceiptStore` are the non-persistent implementations
installed into a `bootstrap.GigaStorageManager` by the EVM-only app, tests, and
load harness. `MemoryStore` 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. `MemoryReceiptStore`
implements the shared receipt interface and indexes cloned Sei receipt records
by block number and transaction hash. 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
2 changes: 1 addition & 1 deletion giga/evmonly/cmd/evmonly-loadtest/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# evmonly-loadtest

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

The synthetic workload defaults to local EVM chain ID `1337`; override it with
Expand Down
7 changes: 5 additions & 2 deletions giga/evmonly/cmd/evmonly-loadtest/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ 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/bootstrap"
"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)
storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, evmonly.NewMemoryReceiptStore())
return evmonly.WithStorageManager(storage, store.EncodeChangeSet)
}

type readOnlyGeneratedStore struct {
Expand All @@ -41,7 +43,8 @@ func (*readOnlyGeneratedStore) CommitStateChanges(int64, []*proto.NamedChangeSet

func withReadOnlyGeneratedState(state evmonly.StateReader) evmonly.Option {
store := &readOnlyGeneratedStore{MemoryStore: evmonly.NewMemoryStore(state)}
return evmonly.WithStore(store, store.EncodeChangeSet)
storage := bootstrap.NewGigaStorageManagerWithStores(nil, store, evmonly.NewMemoryReceiptStore())
return evmonly.WithStorageManager(storage, store.EncodeChangeSet)
}

func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) {
Expand Down
6 changes: 4 additions & 2 deletions giga/evmonly/cmd/evmonly-loadtest/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"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/bootstrap"
"golang.org/x/sync/errgroup"
)

Expand Down Expand Up @@ -121,10 +122,11 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa

startedAt := time.Now()
group, groupCtx := errgroup.WithContext(ctx)
store := evmonly.NewMemoryStore(state)
stateStore := evmonly.NewMemoryStore(state)
storage := bootstrap.NewGigaStorageManagerWithStores(nil, stateStore, evmonly.NewMemoryReceiptStore())
executor := evmonly.NewExecutor(
executorConfig(cfg),
evmonly.WithStore(store, store.EncodeChangeSet),
evmonly.WithStorageManager(storage, stateStore.EncodeChangeSet),
evmonly.WithResultSink(sinks),
)
defer executor.Close()
Expand Down
14 changes: 2 additions & 12 deletions giga/evmonly/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/sei-protocol/sei-chain/giga/evmonly/precompiles"
gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types"
"github.com/sei-protocol/sei-chain/sei-db/bootstrap"
)

// Executor runs raw EVM transactions against snapshots from a giga store.
Expand All @@ -26,7 +26,7 @@ type Executor struct {
resultPool *blockResultPool
stateDBPool sync.Pool
storeMu sync.Mutex
store gigatypes.StateDB
storageManager *bootstrap.GigaStorageManager
changeSetEncoder NamedChangeSetEncoder
closed atomic.Bool
}
Expand All @@ -39,16 +39,6 @@ func WithResultSink(sink ResultSink) Option {
}
}

// WithStore selects the giga store implementation used for all state reads and
// commits. The encoder owns the implementation-specific conversion from the
// executor's EVM-native StateChangeSet to the store's protobuf changesets.
func WithStore(store gigatypes.StateDB, encoder NamedChangeSetEncoder) Option {
return func(e *Executor) {
e.store = store
e.changeSetEncoder = encoder
}
}

// NewExecutor constructs an EVM-only executor. Call Close to disable future OCC
// execution on this executor.
func NewExecutor(cfg Config, opts ...Option) *Executor {
Expand Down
77 changes: 77 additions & 0 deletions giga/evmonly/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"github.com/stretchr/testify/require"

"github.com/sei-protocol/sei-chain/giga/evmonly/precompiles"
sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
"github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt"
)

const (
Expand All @@ -34,6 +36,18 @@ type recordingResultSink struct {
releases []func()
}

type failingReceiptStore struct {
*MemoryReceiptStore
err error
}

func (s *failingReceiptStore) SetReceipts(ctx sdk.Context, records []receipt.ReceiptRecord) error {
if s.err != nil {
return s.err
}
return s.MemoryReceiptStore.SetReceipts(ctx, records)
}

func (s *recordingResultSink) StoreBlockResult(_ context.Context, height uint64, result *BlockResult, release func()) error {
s.heights = append(s.heights, height)
s.results = append(s.results, result)
Expand Down Expand Up @@ -111,6 +125,69 @@ func TestExecutorInvokesResultSink(t *testing.T) {
sink.releases[0]()
}

func TestExecutorStoresReceipts(t *testing.T) {
chainID := big.NewInt(testChainID)
key, err := crypto.GenerateKey()
require.NoError(t, err)
sender := crypto.PubkeyToAddress(key.PublicKey)
recipient := common.HexToAddress("0x00000000000000000000000000000000000000a9")

state := NewMemoryState()
state.SetBalance(sender, big.NewInt(testFundedBalanceWei))
receiptStore := NewMemoryReceiptStore()
rawTx := signLegacyTx(t, key, chainID, 0, &recipient, big.NewInt(7), nil)
stateStore := NewMemoryStore(state)
executor := NewExecutor(Config{}, withTestStores(stateStore, receiptStore, stateStore.EncodeChangeSet))
ctx := blockContext(chainID)
ctx.Number = 77

result, err := executor.ExecuteBlock(t.Context(), BlockRequest{
Context: ctx,
Txs: [][]byte{rawTx},
})

require.NoError(t, err)
require.Len(t, result.Receipts, 1)
stored, err := receiptStore.GetReceipt(newReceiptContext(t.Context(), int64(ctx.Number)), result.Receipts[0].TxHash)
require.NoError(t, err)
require.Equal(t, result.Receipts[0].TxHash.Hex(), stored.TxHashHex)
require.Equal(t, ctx.Number, stored.BlockNumber)
require.Equal(t, sender.Hex(), stored.From)
require.Equal(t, recipient.Hex(), stored.To)
require.Equal(t, uint64(ethtypes.ReceiptStatusSuccessful), uint64(stored.Status))
}

func TestExecutorReturnsReceiptStoreError(t *testing.T) {
storeErr := errors.New("receipt write failed")
receiptStore := &failingReceiptStore{MemoryReceiptStore: NewMemoryReceiptStore(), err: storeErr}
stateStore := NewMemoryStore(NewMemoryState())
sink := &recordingResultSink{}
executor := NewExecutor(
Config{BlockResultPoolSize: 1},
withTestStores(stateStore, receiptStore, EncodeMemoryStoreChangeSet),
WithResultSink(sink),
)
request := BlockRequest{Context: blockContext(big.NewInt(testChainID))}

result, err := executor.ExecuteBlock(t.Context(), request)

require.ErrorIs(t, err, storeErr)
require.Nil(t, result)
require.Empty(t, sink.results)
require.Equal(t, BlockResultPoolStats{Capacity: 1, Available: 1}, executor.ResultPoolStats())
view := stateStore.OpenView()
require.Zero(t, view.GetBlockHeight())
view.Close()

receiptStore.err = nil
result, err = executor.ExecuteBlock(t.Context(), request)
require.NoError(t, err)
require.NotNil(t, result)
require.Len(t, sink.results, 1)
result.Release()
sink.releases[0]()
}

func TestExecutorPooledResultRelease(t *testing.T) {
chainID := big.NewInt(testChainID)
key, err := crypto.GenerateKey()
Expand Down
27 changes: 22 additions & 5 deletions giga/evmonly/giga_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (
const maxGigaStoreBlockNumber = uint64(1<<63 - 1)

var (
errMissingStore = errors.New("executor requires a giga store")
errMissingStorageManager = errors.New("executor requires a storage manager")
errMissingStateStore = errors.New("storage manager requires a state store")
errMissingReceiptStore = errors.New("storage manager requires a receipt store")
errMissingNamedChangeSetEncoder = errors.New("giga store requires a named changeset encoder")
)

Expand All @@ -28,8 +30,16 @@ var _ StateReader = gigaSnapshotStateReader{}
type NamedChangeSetEncoder func(StateChangeSet) ([]*proto.NamedChangeSet, error)

func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req PreparedBlock) (*BlockResult, error) {
if e.store == nil {
return nil, errMissingStore
if e.storageManager == nil {
return nil, errMissingStorageManager
}
stateStore := e.storageManager.StateStore()
if stateStore == nil {
return nil, errMissingStateStore
}
receiptStore := e.storageManager.ReceiptDB()
if receiptStore == nil {
return nil, errMissingReceiptStore
}
if e.changeSetEncoder == nil {
return nil, errMissingNamedChangeSetEncoder
Expand All @@ -48,7 +58,7 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar
if err := ctx.Err(); err != nil {
return nil, err
}
snapshot := e.store.OpenView()
snapshot := stateStore.OpenView()
if snapshot == nil {
return nil, errors.New("giga store returned a nil snapshot")
}
Expand All @@ -75,7 +85,14 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar
if err := ctx.Err(); err != nil {
return nil, err
}
if err := e.store.CommitStateChanges(blockNumber, changesets); err != nil {
records, err := receiptRecords(req.Context.Number, result)
if err != nil {
return nil, fmt.Errorf("encode receipts for block %d: %w", req.Context.Number, err)
}
if err := receiptStore.SetReceipts(newReceiptContext(ctx, blockNumber), records); err != nil {
Comment thread
codchen marked this conversation as resolved.
return nil, fmt.Errorf("store receipts for block %d: %w", req.Context.Number, err)
}
if err := stateStore.CommitStateChanges(blockNumber, changesets); err != nil {
return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err)
}
ok = true
Expand Down
Loading
Loading