erigontech / erigontech/erigon

Design: TransactionState/BlockState separation, TxTask refactoring, cross-process TX caching

Open
#19,855 1 comment 0 reactions 1 assignee Claimed by @mh0lt View on GitHub
tech debt reduction
Dominant language
Go
Stars
3.6k
Forks
1.5k
Avg merge
1d 16h
Merged PRs (30d)
455

Description

## Context

Follow-on design from #19623 (2-cache IBS refactor). While #19623 focuses on eliminating `stateObject` and the IBS round-trip within the current architecture, this issue captures the higher-level ownership restructuring and cross-process optimization opportunities that should inform the implementation to avoid costly future refactors.

These observations emerged during PR #19814 (direct finalize path + unit tests), where the brittleness of IBS's multiple implicit roles became apparent.

## Problem

`IntraBlockState` serves multiple implicit roles:
1. **Execution context** — per-TX state objects, journal, snapshots
2. **Version tracking** — versioned reads/writes for parallel validation
3. **Finalize orchestrator** — FinalizeTx / CommitBlock / MakeWriteSet
4. **BAL read generation** — `refreshVersionedAccount` for EIP-7928

The `FinalizeTx` vs `CommitBlock` distinction is extremely subtle:
- `FinalizeTx`: `useBlockOrigin=false` (originStorage — last written value)
- `CommitBlock`: `useBlockOrigin=true` (blockOriginStorage — block-start value)

This exists because `originStorage` gets updated by each `FinalizeTx`, so by `CommitBlock` time it equals the dirty value.

## 1. TransactionState / BlockState Separation

### TransactionState (per-TX, request-scoped)

**Key design decision**: TransactionState should NOT be owned by the worker and reset between TXs. It should be part of the request — a value object with immutable inputs and value-type outputs. This avoids:
- Worker lifecycle bleeding into state lifecycle
- Hidden state leaks between TXs (today's `IBS.Reset()` is fragile)
- Coupling that prevents caching

The execution model becomes: `(TransactionState, inputs) → (TransactionState, outputs)`

Contains:
- Account state objects (balance, nonce, code, storage)
- Journal for revert snapshots
- Dirty tracking
- No version map, no BAL, no block-level concerns

### BlockState (per-block)

Contains:
- Version map (parallel execution)
- Block-origin storage values
- Accumulated block IO (reads/writes per TX)
- BAL computation state
- Fee-calc coordination (coinbase/burnt deltas)
- `Combine(txResult)` merges TX results into block state

## 2. TxTask Refactoring

`TxTask` has grown organically serving multiple scenarios:
- Historical queries (simpler, no dependencies)
- Parallel speculative execution (dependencies, delayed fee calc)
- RPC calls (`eth_call`, `eth_estimateGas`)
- AA transaction batches

The flags `shouldDelayFeeCalc`, `HistoryExecution`, `InBatch`, `AAValidationBatchSize` are symptoms of one object doing too many jobs.

TxTask should be split into:
- **TxInput**: immutable execution request (tx, header, config, gas pool)
- **TxScheduling**: parallel-specific (dependencies, incarnation, version)
- **TxResult**: execution output (state changes, receipt, logs, errors)

## 3. Cross-Process TX Caching

TX execution happens in multiple places in Erigon:
- Block execution (serial + parallel)
- Block building (miner/proposer)
- TxPool validation (and future expanded validation)
- RPC (`eth_call`, `eth_estimateGas`, `trace_*`)

If TransactionState is a pure function of inputs, the cache key becomes:
```
hash(tx_bytes, account_prestate_at_block) → TxResult
```

This enables:
- **TxPool → Block Building**: skip re-execution of already-validated TXs
- **RPC → Recent blocks**: cache recent `eth_call` results
- **Parallel execution**: cache results across incarnations when prestate matches

## 4. BAL Read Generation Decoupling

Currently tightly coupled to IBS via `refreshVersionedAccount`. Must be decoupled so that:
- The direct finalize path (`finalizeTx`) can generate correct BAL reads without IBS reconstruction
- BAL reads become a pure function of `(TxOut addresses, prestate)`

## Implementation Sequencing

These changes build on #19623's 5-phase plan and extend it:

| Phase | Description | Dependency |
|-------|-------------|------------|
| #19623 Phases 1-5 | 2-cache model, eliminate stateObject | Foundation |
| Fix finalizeTx BAL reads | Direct finalize generates correct BalancePath reads | #19623 Phase 2 |
| Extract BlockState | Move version map, block IO, BAL state out of IBS | #19623 Phase 5 |
| Request-scoped TransactionState | Per-TX creation, pure function model, TxTask split | BlockState extraction |
| Cross-process caching | TX result cache shared across execution contexts | TransactionState + serial exec deprecation |

## Key Files

- `execution/stagedsync/exec3_parallel.go` — finalize dispatch, finalizeTx, finalizeWithIBS
- `execution/stagedsync/exec3_finalize_test.go` — comparison unit tests (PR #19814)
- `execution/state/intra_block_state.go` — IBS (the monolith)
- `execution/state/versionedio.go` — version map, BAL computation
- `execution/state/state_object.go` — account state, updateStorage
- `execution/exec/txtask.go` — TxTask (the multi-purpose envelope)

## Notes

- The TransactionState/BlockState separation and TxTask refactoring are too broad for initial implementation but must inform #19623's design to avoid costly future refactors
- Cross-process TX caching requires serial exec deprecation first
- The direct finalize path (PR #19814) is currently bypassed due to BAL read mismatches — fixing this is a near-term prerequisite

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.