Skip to content
Merged
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
12 changes: 12 additions & 0 deletions giga/evmonly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ 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.

`ExecuteBlock` advances the state store's version itself, independently of any
ABCI `Commit`, so what the store holds after a restart is decided by the
storage layer (which flushes asynchronously and re-executes blocks from
BlockDB), not by which `Commit` calls the application saw. An ABCI application
built on the executor must therefore derive `Info()` from storage rather than
from memory: after a restart, the Giga router calls `InitChain` and replays
block 1 whenever `Info().LastBlockHeight` is zero, which fails against state
that already exists. `WithBlockChangeSetEncoder(...)` lets the application
commit its own named changesets (for example an execution cursor holding the
app hash and parent hash) in the same `CommitStateChanges` call as the block's
EVM state, so the two can never disagree on disk.

The FlatKV encoder persists balance, nonce, code, and storage changes, and the
executor reads them through the current Giga state view. EVM-only Autobahn load
tests use `WithMissingAccountState(...)` to supply the initial funded state for
Expand Down
28 changes: 26 additions & 2 deletions giga/evmonly/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ type Executor struct {
stateStore gigatypes.StateDB
receiptStore receipt.ReceiptStore
changeSetEncoder NamedChangeSetEncoder
missingState StateReader
closed atomic.Bool
// Optional: nil commits only the state encoder's changesets.
blockChangeSetEncoder BlockChangeSetEncoder
// blockEncoderReadsStore is false only for an encoder registered as
// store-independent, which lets encoding overlap the previous commit.
blockEncoderReadsStore bool
missingState StateReader
closed atomic.Bool

// Breaks a store-backed block into its stages. That path is serialized by storeMu, so one timer
// serves the executor.
Expand Down Expand Up @@ -68,6 +73,25 @@ func WithMissingAccountState(state StateReader) Option {
}
}

// WithBlockChangeSetEncoder commits the encoder's changesets alongside every
// block's state changes.
// WithStoreIndependentBlockChangeSetEncoder registers an encoder that reads only
// the block context and result. Encoding then overlaps the previous block's
// commit. An encoder that touches the store must use WithBlockChangeSetEncoder.
func WithStoreIndependentBlockChangeSetEncoder(encoder BlockChangeSetEncoder) Option {
return func(e *Executor) {
e.blockChangeSetEncoder = encoder
e.blockEncoderReadsStore = false
}
}

func WithBlockChangeSetEncoder(encoder BlockChangeSetEncoder) Option {
return func(e *Executor) {
e.blockChangeSetEncoder = encoder
e.blockEncoderReadsStore = true
}
}

// 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
33 changes: 31 additions & 2 deletions giga/evmonly/giga_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/common"

gigametrics "github.com/sei-protocol/sei-chain/giga/metrics"
"github.com/sei-protocol/sei-chain/sei-db/common/keys"
"github.com/sei-protocol/sei-chain/sei-db/proto"
gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types"
)
Expand All @@ -19,6 +20,7 @@ var (
errMissingStateStore = errors.New("executor requires a state store")
errMissingReceiptStore = errors.New("executor requires a receipt store")
errMissingNamedChangeSetEncoder = errors.New("giga store requires a named changeset encoder")
errBlockEncoderUsedEVMStoreKey = errors.New("block changeset encoder may not write the EVM state changeset")
)

var _ StateReader = gigaSnapshotStateReader{}
Expand All @@ -37,6 +39,16 @@ var _ StateReader = gigaSnapshotStateReader{}
// one exception, and the executor waits for that commit before encoding a block that has one.
type NamedChangeSetEncoder func(StateChangeSet) ([]*proto.NamedChangeSet, error)

// BlockChangeSetEncoder contributes named changesets that are committed in the
// same CommitStateChanges call as the block's EVM state changes, so they are
// durable, rolled back and replayed together with that state. It is called
// after execution with the block's context and result, which it must treat as
// immutable. Changesets under keys.EVMStoreKey are reserved for the state encoder.
//
// As with NamedChangeSetEncoder, what it returns must not alias the result: the commit outlives
// the block.
type BlockChangeSetEncoder func(BlockContext, *BlockResult) ([]*proto.NamedChangeSet, error)

func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req PreparedBlock) (*BlockResult, error) {
stateStore := e.stateStore
if stateStore == nil {
Expand Down Expand Up @@ -118,6 +130,21 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar
if err != nil {
return nil, fmt.Errorf("encode state changes for block %d: %w", req.Context.Number, err)
}
if e.blockChangeSetEncoder != nil {
extra, err := e.blockChangeSetEncoder(req.Context, result)
if err != nil {
return nil, fmt.Errorf("encode block changes for block %d: %w", req.Context.Number, err)
}
// An EVM-keyed changeset here would be written into account, storage and code
// state as part of the block, diverging the app hash from the committed state.
for _, cs := range extra {
if cs != nil && cs.Name == keys.EVMStoreKey {
return nil, fmt.Errorf("block encoder returned a changeset named %q for block %d: %w",
cs.Name, req.Context.Number, errBlockEncoderUsedEVMStoreKey)
}
}
changesets = append(changesets, extra...)
Comment thread
bdchatham marked this conversation as resolved.
}
if err := ctx.Err(); err != nil {
return nil, err
}
Expand Down Expand Up @@ -150,9 +177,11 @@ func (e *Executor) executePreparedBlockWithStore(ctx context.Context, req Prepar
// the block's own changes, which decides whether encoding may overlap the previous block's commit.
//
// Expanding a storage clear iterates the live store to find the slots to delete, so a block that
// clears one must not be encoded against a store mid-commit.
// clears one must not be encoded against a store mid-commit. A block encoder is caller-supplied and
// free to read whatever it likes, so one is assumed to read the store unless it was registered as
// store-independent: assuming otherwise would surrender the receipt-stage slack on every block.
func (e *Executor) encodingReadsTheStore(changes *StateChangeSet) bool {
return len(changes.StorageClears) > 0
return len(changes.StorageClears) > 0 || (e.blockChangeSetEncoder != nil && e.blockEncoderReadsStore)
}

// AwaitCommits blocks until every block this executor has run is committed, and reports the first
Expand Down
Loading
Loading