erigontech / erigontech/erigon

execution: parallel calcFees synthesizes coinbase/burnt AddressPath from the pre-tx snapshot, ignoring same-tx TxOut writes

Open
#22,834 1 comment 0 reactions 1 assignee Claimed by @yperbasis View on GitHub
performance
Dominant language
Go
Stars
3.6k
Forks
1.5k
Avg merge
1d 16h
Merged PRs (30d)
455

Description

## Summary

In the parallel executor's `calcFees`, the AddressPath sibling write emitted alongside the fee-credit BalancePath write is synthesized inconsistently ([exec3_parallel.go#L2014-L2030](https://github.com/erigontech/erigon/blob/67d5531dcb/execution/stagedsync/exec3_parallel.go#L2014-L2030)):

```go
addrAcc := &accounts.Account{Balance: newCoinbaseBalance}
if coinbaseAcc != nil {
addrAcc.Nonce = coinbaseAcc.Nonce // floor(txIndex-1) snapshot — ignores TxOut
addrAcc.Incarnation = coinbaseAcc.Incarnation
addrAcc.CodeHash = coinbaseAcc.CodeHash
} else {
addrAcc.Nonce = coinbaseNonce // TxOut-aware
addrAcc.CodeHash = accounts.EmptyCodeHash // ignores a TxOut CodeHash write
}
```

When the pre-tx snapshot has the account (`coinbaseAcc != nil`), the record carries the snapshot's Nonce/Incarnation/CodeHash even when the current tx's TxOut holds newer writes for those fields — e.g. a sender==coinbase tx under delayed fee calc (nonce bump in TxOut), or a CREATE2 at the coinbase address (CodeHash/Incarnation in TxOut). The `coinbaseAcc == nil` branch is TxOut-aware for Nonce (it uses the `coinbaseNonce` computed a few lines above from `result.TxOut.GetNonce`) but not for CodeHash/Incarnation. The burnt-address mirror block just below ([#L2047](https://github.com/erigontech/erigon/blob/67d5531dcb/execution/stagedsync/exec3_parallel.go#L2047)) has the same shape.

The result: the AddressPath cell flushed at version V can disagree with its sibling per-field cells (NoncePath/CodeHashPath/IncarnationPath) flushed at the same version V.

## Why it appears latent today

Tracing the consumers of the AddressPath record, no observable divergence was found:

- The commit path never sees it: `WriteSet.Normalize` skips AddressPath entries ("record-level — skip for field-level consumers", `execution/state/writeset_normalize.go`), so the stale values cannot reach the domains.
- Finalize-path reads (`versionedStateReader.ReadAccountData`) overlay per-field versionMap cells via `applyVersionedUpdates` on whatever base they resolve, so the sibling cells mask the stale fields.
- Worker-side reads use the record via `versionedAccountBase` for existence only (its doc comment: "does NOT overlay the per-field versionMap cells"); field values go through the per-field read/refresh helpers (`readBalance` / `refreshNonce` / `refreshCodeHash`), and a per-field cell exists at the same version for exactly the fields the tx changed. The EIP-161 emptiness check (`emptyFromVersionedFields`) also refreshes per-field.

So correctness currently relies on every consumer either ignoring the record's field values or overlaying the sibling cells. That invariant is implicit, and this genus of bug has bitten before — see the CachedReaderV3 gate in `versionedAccountBase` ("stale nonce/codeHash flows through the per-field refresh (which only overwrites fields a versionMap cell exists for)").

## Suggested fix

Synthesize `addrAcc` from the TxOut-aware values `calcFees` already computes (`coinbaseNonce`, plus overlays for CodeHash/Incarnation from `result.TxOut`) in both branches, and mirror for the burnt address, so the AddressPath record is self-consistent with its sibling field cells at the same version.

## Origin

Pre-existing since the AddressPath emission was introduced in #21423. Flagged by Copilot while reviewing #22833 (which only removes the dead CollectorWrites mutations beside this code): https://github.com/erigontech/erigon/pull/22833#discussion_r3668885330

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.