erigontech / erigontech/erigon
Parallel-exec: resetObjectChange.dirtied() asymmetry drops SD-revival writes for some scenarios; narrow fix needed
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
## Summary
Parallel-exec misses `journal.dirties` membership for addresses recreated via `createObject(addr, previous != nil)` in some scenarios, causing the worker IBS to drop the post-revival writeset.
Diagnosis (from PR #21207 comment thread): `resetObjectChange.dirtied()` returns `(NilAddress, false)` while its sibling `createObjectChange.dirtied()` returns `(account, true)`. Both journal entries represent the same logical "stateObject placed at this address" operation; they differ only in revert behaviour. The asymmetry means SD-revival via `GetOrNewStateObject` / `AddBalance` doesn't mark the address dirty, so `MakeWriteSet` skips writing it (the `test_double_kill` / `test_dynamic_create2_selfdestruct_collision_two_different_transactions` failures from the EEST parallel shard).
## Why this is filed instead of fixed
Both narrow shapes I tried for the fix regress `TestSelfDestructReceive` under `EXEC3_PARALLEL=true` on `f05a65e691`:
1. **Unconditional symmetry** — `func (ch resetObjectChange) dirtied() (accounts.Address, bool) { return ch.account, true }`. Test fails: wrong trie root for block 1.
2. **Conditional SD-revival + SelfDestructPath=false re-emit** — inside `createObject` only when `previous.selfdestructed`, do `sdb.journal.dirty(addr)` + `versionWritten(SelfDestructPath=false)`. Same regression.
Diff of the validator's MakeWriteSet emission for the recreated contract addr (`3a220f351252089d385b29beca14e27f204c296a`):
```
Unfixed (passing): 1 (3.0) Update Account Data (*state.Writer): 3a220f... balance:1000,nonce:0,codehash:
Fixed (failing): 1 (3.0) Update Account Data (*state.Writer): 3a220f... balance:1000,nonce:1,codehash:emptyHash
```
The unfixed validator emits the canonical post-revival state (`nonce=0, codeHash=` — the empty Handle, i.e. unset). The fixed validator emits the SD'd contract's prior `nonce=1, codeHash=emptyHash`. Something in the validator's stateObject reconstruction path picks up different field values depending on what's in `journal.dirties` at MakeWriteSet time.
## Test scenarios
| Scenario | Path | Without fix | With fix |
|---|---|---|---|
| `test_double_kill` (Frontier / Homestead, EEST) | tx1 SD's, tx2 empty-touch | ❌ missing empty-removal write | ✅ (per the PR #21207 comment author's local verification) |
| `TestSelfDestructReceive` (`execution/tests/`, SpuriousDragon) | tx1 SD's, tx2 `AddBalance(non-zero)` | ✅ passes | ❌ wrong trie root |
## What's needed
A narrower fix that gets the dirty-mark right for the empty-touch / `CreateAccount` path **without** changing the validator's stateObject reconstruction for the `AddBalance(non-zero)` path. Likely needs:
1. Tracing the validator's read of the recreated stateObject to find where `nonce=0,codeHash=` vs `nonce=1,codeHash=emptyHash` diverges.
2. Identifying whether the right place to mark dirty is in `createObject` itself, in the caller (e.g. `CreateAccount` already does this conditionally at `intra_block_state.go:1876`), or only on specific journal-entry paths.
## Repro
```bash
# Pin to current main
git checkout f05a65e691
# Apply unconditional fix
sed -i '/^func (ch resetObjectChange) dirtied/,/^}$/{s/return accounts.NilAddress, false/return ch.account, true/}' execution/state/journal.go
# Confirm regression
ERIGON_EXEC3_PARALLEL=true go test -run '^TestSelfDestructReceive$' -count=1 -timeout 30s ./execution/tests/
# → FAIL: Wrong trie root of block 1
```
## Related
- #21138 — heuristic / IBS-dependency removal (parent)
- #21207 — PR carrying the original `test_double_kill` diagnosis
- #21211, #21212 — #21138 PR 1 (dead-finalize) and PR 2 (minIBS extraction) — neither addresses this bug
Contributor guide
Assessment
This issue has not been assessed yet.