Skip to content

Parallel-exec: resetObjectChange.dirtied() asymmetry drops SD-revival writes for some scenarios; narrow fix needed #21217

@mh0lt

Description

@mh0lt

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 symmetryfunc (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:<nil>
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=<nil> — 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=<nil> 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

# 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

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions