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
35 changes: 16 additions & 19 deletions giga/evmonly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,34 +78,31 @@ receipt store, plus the `NamedChangeSetEncoder` for its state implementation.
Unit tests can supply those dependencies independently. Execution fails closed
if the state store or the encoder is missing. The receipt store is optional: a
node configured without one (`enable_receipt_store = false`, meant for
validators that serve no receipt reads) skips receipt persistence entirely and
`AwaitReceipts` returns at once.
validators that serve no receipt reads) skips receipt persistence entirely.
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 is closed once execution returns;
the commit reads from the changeset, not the snapshot. 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.
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
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 and both store
writes run behind the block, one block at a time in block order; `ResultSink`
runs on the block loop once that work has been handed off, so it may see a
result whose writes have not landed yet. Ethereum receipts are converted into
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, including for empty blocks. That write starts
as soon as execution returns, before the block encoder runs and the previous
commit is waited on, and the block's height-advancing state commit waits for it
to land; `AwaitReceipts` blocks until the newest block's receipts are readable.
A failure in either write latches: `AwaitReceipts` reports a receipt failure,
the next block's execution reports a state failure, and the executor accepts
no further blocks. A receipt failure leaves state unchanged; a state failure,
or a block that fails after its receipts were handed off, can leave receipts
behind, which re-executing the block after a restart overwrites.
`receipt.ReceiptStore` interface before the height-advancing state commit,
including for empty blocks. A store with an async write queue only accepts the
write here: the receipts land behind the block, so a reader that follows the
state head can briefly miss the newest block's receipts, and recovery replays
the tail a crash leaves unwritten. 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
accept the block.

`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
Expand Down
33 changes: 8 additions & 25 deletions giga/evmonly/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt"
gigatypes "github.com/sei-protocol/sei-chain/sei-db/state_db/giga/types"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)

// executorMeterName is the OTel meter this package's instruments are created on.
Expand Down Expand Up @@ -47,24 +46,13 @@ type Executor struct {
// Breaks a store-backed block into its stages. That path is serialized by storeMu, so one timer
// serves the executor.
blockPhases *seidbmetrics.PhaseTimer
// Breaks the background persistence of a block's state into its stages. One block is
// committed at a time, so one timer serves it.
pipelinePhases *seidbmetrics.PhaseTimer
// Breaks the background receipt write into its stages. Receipt writes run one at a time, in
// block order, but overlap the state commit, so they have a timer of their own on the same
// metric, told apart by a stage label.
receiptPhases *seidbmetrics.PhaseTimer

// The commit running behind the current block, and what it will write. A block reads the latter
// through an overlay so it need not wait for the former.
pipelineMu sync.Mutex
// Closed once the block is fully persisted.
pipelineDone chan struct{}
pipelineErr error
// The most recent block's receipt write; kept after the block retires so a waiter that arrives
// late still finds its answer.
pipelineReceipts *receiptWrite
pipelineChanges *pendingChanges
pipelineMu sync.Mutex
pipelineDone chan struct{}
pipelineErr error
pipelineChanges *pendingChanges
// Counts commits started, so a reader can tell that a block landed between two of its steps.
pipelineGeneration uint64
// The first commit that failed, kept so no caller can miss it.
Expand Down Expand Up @@ -109,13 +97,10 @@ func WithBlockChangeSetEncoder(encoder BlockChangeSetEncoder) Option {
// NewExecutor constructs an EVM-only executor. Call Close to disable future OCC
// execution on this executor.
func NewExecutor(cfg Config, opts ...Option) *Executor {
pipelineTimers := seidbmetrics.NewPhaseTimerFactory(otel.Meter(executorMeterName), "evmonly_pipeline")
e := &Executor{
cfg: cfg.WithDefaults(),
resultPool: newBlockResultPool(cfg.BlockResultPoolSize),
blockPhases: seidbmetrics.NewPhaseTimer(otel.Meter(executorMeterName), "evmonly_block"),
pipelinePhases: pipelineTimers.Build(attribute.String("stage", "state")),
receiptPhases: pipelineTimers.Build(attribute.String("stage", "receipts")),
cfg: cfg.WithDefaults(),
resultPool: newBlockResultPool(cfg.BlockResultPoolSize),
blockPhases: seidbmetrics.NewPhaseTimer(otel.Meter(executorMeterName), "evmonly_block"),
}
if e.cfg.OCCWorkers > 1 {
e.occPool = newOCCWorkerPool(e.cfg.OCCWorkers)
Expand All @@ -132,10 +117,8 @@ func (e *Executor) Close() {
}
e.closed.Store(true)
// Land the commit running behind the last block before the pool it may need goes away. The
// failure is kept rather than reported, for the next AwaitCommits to return. The receipt write
// is waited on separately: a block that failed after starting it has no commit to land.
// failure is kept rather than reported, for the next AwaitCommits to return.
_ = e.awaitPipelineCommit()
_ = e.AwaitReceipts()
if e.occPool != nil {
e.occPool.Close()
}
Expand Down
12 changes: 5 additions & 7 deletions giga/evmonly/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,21 +174,19 @@ func TestExecutorReturnsReceiptStoreError(t *testing.T) {

require.ErrorIs(t, err, storeErr)
require.Nil(t, result)
// Receipts are written behind the block, so the sink has already seen the result.
require.Len(t, sink.results, 1)
sink.releases[0]()
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()

// A failed write is latched: the executor refuses further blocks rather than run ahead of a
// store that is missing a block.
receiptStore.err = nil
result, err = executor.ExecuteBlock(t.Context(), request)
require.ErrorIs(t, err, storeErr)
require.Nil(t, result)
require.NoError(t, err)
require.NotNil(t, result)
require.Len(t, sink.results, 1)
result.Release()
sink.releases[0]()
}

func TestExecutorPooledResultRelease(t *testing.T) {
Expand Down
Loading
Loading