Skip to content

execution/state: mirror createObjectChange dirty-tracking on resetObjectChange (#21138)#21220

Closed
mh0lt wants to merge 1 commit into
mainfrom
mh/21138-pr3-dirtied-symmetric
Closed

execution/state: mirror createObjectChange dirty-tracking on resetObjectChange (#21138)#21220
mh0lt wants to merge 1 commit into
mainfrom
mh/21138-pr3-dirtied-symmetric

Conversation

@mh0lt

@mh0lt mh0lt commented May 15, 2026

Copy link
Copy Markdown
Contributor

Stack

Depends on: #21212#21138 PR 2 (minIBS extraction). Must merge first.

Summary

Third in the #21138 heuristic / IBS-dependency removal stack. Restores symmetric dirty-tracking between createObjectChange.dirtied() and resetObjectChange.dirtied() so parallel-exec workers don't drop the post-SD-revival writeset.

`journal.go` defined the two journal entry types for the same logical "createObject" event with non-symmetric `dirtied()` returns:

```go
func (ch createObjectChange) dirtied() (accounts.Address, bool) { return ch.account, true }
func (ch resetObjectChange) dirtied() (accounts.Address, bool) { return accounts.NilAddress, false }
```

`createObject` in `intra_block_state.go` picks between them on `previous == nil` — first-creation goes through `createObjectChange`, recreation (e.g. SD-revival via `GetOrNewStateObject`) goes through `resetObjectChange`. Both represent the same operation; the asymmetry was an oversight.

Manifests under parallel-exec when tx1 SD's an address and tx2 hits `CreateAccount` / `GetOrNewStateObject` on the same address:

  • Serial: tx1's writer already `DeleteAccount`'d the addr → `getStateObject` returns `nil` → `createObject(addr, nil)` appends `createObjectChange` → marks `journal.dirties`.
  • Parallel: `versionedRead` reads tx1's `SelfDestructPath=true`; `createObject` synthesises a non-nil `previous` with `selfdestructed=true` → appends `resetObjectChange` → with the old return, does not mark `journal.dirties`.

At `MakeWriteSet` the worker IBS computes `isDirty` from `journal.dirties`. With no mark, `updateAccount` falls through both DELETE and UPDATE branches → `LightCollector.UpdateAccountData` never fires → `result.CollectorWrites` is missing the empty-account write (`test_double_kill` / EEST EIP-6780 family on the parallel shard).

Diagnosis credit: original investigation by Mark Holt on PR #21207 thread.

Known regression — tracked in #21217

`TestSelfDestructReceive` fails under `EXEC3_PARALLEL=true` after this change with a wrong-trie-root for block 1.

The validator's `stateObject` reconstruction for an SD-then-revived address emits different field values (`nonce=1, codehash=emptyHash`) than the unfixed canonical (`nonce=0, codehash=`). The empty-touch / `CreateAccount` paths this fix addresses don't have this issue; the `AddBalance(non-zero)` on an SD'd address does.

Filed as #21217 with full repro and two failed narrow-fix attempts (unconditional symmetry — this PR; conditional SD-revival + `SelfDestructPath=false` re-emit — also regresses). The right narrower fix needs more diagnosis on the validator's read path.

Per coordination with the original diagnosis author, this PR lands as the last commit in the #21138 cleanup stack so the broader structural direction is visible together; the `TestSelfDestructReceive` regression is the explicit "more work needed" marker before final merge.

What's verified

Under `EXEC3_PARALLEL=true` on the fix branch:

What's next

Related

@mh0lt mh0lt requested a review from yperbasis as a code owner May 15, 2026 13:32
Base automatically changed from mh/21138-pr2-finalize-cleanup to mh/21138-drop-dead-finalize May 19, 2026 01:45
Base automatically changed from mh/21138-drop-dead-finalize to main May 21, 2026 17:59
@AskAlexSharov AskAlexSharov enabled auto-merge May 22, 2026 06:48

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prerequisite PR #21212 was merged not into main

…ectChange

journal.go defined two journal entry types for the same logical
"createObject" event with non-symmetric dirtied() returns:

    func (ch createObjectChange) dirtied() (accounts.Address, bool) { return ch.account, true }
    func (ch resetObjectChange)  dirtied() (accounts.Address, bool) { return accounts.NilAddress, false }

createObject in intra_block_state.go picks between them on
`previous == nil` — first-creation goes through createObjectChange,
recreation (e.g. SD-revival via GetOrNewStateObject) goes through
resetObjectChange. Both represent the same operation ("a stateObject
was placed at this address"); they differ only in revert behaviour.

The asymmetry bites parallel-exec when tx1 selfdestructs an address
and tx2 hits CreateAccount or GetOrNewStateObject on the same address:

  * Serial:   tx1's writer already DeleteAccount'd the addr, so
              getStateObject returns nil → createObject(addr, nil)
              appends createObjectChange → marks journal.dirties.
  * Parallel: versionedRead returns the contract's base-state account
              and reads tx1's SelfDestructPath=true; createObject
              synthesises a non-nil previous with selfdestructed=true
              → appends resetObjectChange → with the old return,
              does NOT mark journal.dirties.

At MakeWriteSet the worker IBS computes isDirty from journal.dirties.
With no mark, updateAccount falls through both DELETE and UPDATE
branches → LightCollector.UpdateAccountData never fires →
result.CollectorWrites is missing the empty-account write
(test_double_kill / EEST EIP-6780 family on the parallel shard).

Symmetric tracking restores the dirty mark for the recreate path
without changing first-create behaviour. Verified on
TestEngineApiBAL*, TestEIP7708BurnLog*, TestDeleteRecreate* under
EXEC3_PARALLEL=true.

## Known regression — see #21217

TestSelfDestructReceive fails under EXEC3_PARALLEL=true after this
change with a wrong-trie-root for block 1. The validator's
stateObject reconstruction for an SD-then-revived address emits
different field values (`nonce=1, codehash=emptyHash`) than the
unfixed canonical (`nonce=0, codehash=<nil>`). The empty-touch /
CreateAccount paths the fix addresses don't have this issue; the
AddBalance(non-zero) on an SD'd address does.

Filed as #21217 with full repro and the two failed narrow-fix
attempts (unconditional symmetry; conditional SD-revival +
SelfDestructPath=false re-emit). Lands as the last commit in this
stack so the broader structural direction is visible together; the
TestSelfDestructReceive regression is the explicit "more work
needed" marker before final merge.

Stacks on #21212.
@mh0lt

mh0lt commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

@yperbasis — both blockers from this PR's review are now cleared and the change is ready for re-review:

  1. Prerequisite execution/stagedsync: extract minIBS post-apply path into a dedicated method #21212 was merged to main on 2026-05-15. Rebasing this branch on current main drops the two preceding commits as "already upstream", leaving only the one-line resetObjectChange.dirtied() symmetric change on top of main. (Branch still shows the pre-merge state — happy to force-push the rebased branch if you'd prefer to re-review against that, or you can rebase via the GitHub UI.)

  2. The TestSelfDestructReceive regression noted in Parallel-exec: resetObjectChange.dirtied() asymmetry drops SD-revival writes for some scenarios; narrow fix needed #21217 is gone. Verified under ERIGON_EXEC3_PARALLEL=true against current main with this fix applied:

    • go test ./execution/tests/ -run 'TestSelfDestructReceive$' -count=1 — passes.
    • go test ./execution/state/ -count=1 (full state package) — passes.
    • go test ./execution/tests/ -count=1 (full ~3min sweep) — passes.

    Almost certainly resolved by parallel-exec: remove skipCheck and the heuristics compensating for skip-overridden results #21319's skipCheck-removal stack landing in main (execution: remove parallel-exec skipCheck — validator is sole source of truth (#21319) #21344) — the validator's stateObject reconstruction false-positive for SD-then-revived addresses is gone, so dirty-tracking symmetry no longer regresses this path.

FYI: I've also cherry-picked this commit onto #21017's branch (mh/ci-exec-mode-matrix head 0625221495) as one of a set of parallel-exec robustness fixes there — the from-0 mainnet parallel flake at block ~131578 / ~263641 is the most plausible same-family victim of the bug this fixes, and we're verifying that hypothesis on the next merge-queue cycle.

If this PR can clear review in the next day or so, great. Otherwise the fix lands via #21017 and we can close this as resolved-elsewhere.

@mh0lt mh0lt force-pushed the mh/21138-pr3-dirtied-symmetric branch from 799d6ad to a7169ca Compare May 23, 2026 18:45
@mh0lt

mh0lt commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed the rebased branch (now a7169ca561 on top of current main). PR diff is now a clean 1-file, 1-change view — just execution/state/journal.go with the resetObjectChange.dirtied() symmetry fix. Ready for re-review @yperbasis.

@mh0lt

mh0lt commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

Closing as redundant: this commit's content (the symmetric resetObjectChange.dirtied() fix) was carried along in the parallel-exec correctness arc and landed on main via #21017 (squash-merged at 7827ec7). Confirmed identical implementation now in main's execution/state/journal.go.

@mh0lt mh0lt closed this May 25, 2026
auto-merge was automatically disabled May 25, 2026 21:06

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants