erigontech / erigontech/erigon
Design: TransactionState/BlockState separation, TxTask refactoring, cross-process TX caching
- 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
Assessment
This issue has not been assessed yet.