Follow-up to #21136. The recurring parallel-exec-vs-serial divergences (SELFDESTRUCT/recreate, EIP-161 empty-removal, EIP-7708 burn logs, fork-transition, coinbase) all come from the post-execution wrapper re-deriving "what did this tx do" via the heuristic normalizeWriteSet(versionWritten-log) path and the calcState re-derivation, instead of using the faithful MakeWriteSet-style output that the validated worker already produces. ~half of genuine parallel-executor PRs over the last 2 months are this family. This issue is the consolidation that makes those bugs structurally impossible.
Target per-tx flow (incremental — does NOT require the full "IBS = pure transaction-state" rework)
- Parallel-execute txs (unchanged).
- Validate + serialize via BlockSTM (unchanged) — at this point we have a verified version. The validated worker's IBS already ran
MakeWriteSet into a LightCollector → result.CollectorWrites = every dirty object's final state.
- Per-tx serial-finalize — done from data + the writeset, no reconstructed IBS:
- per-tx delta =
filterWritesByVersionMap(result.CollectorWrites, vmWrites) (faithful, pruned to the keys the tx actually touched — both filterWritesByVersionMap and CollectorWrites already exist).
- accumulate tip serially → final coinbase / burnt balances → patch the writeset (read pre-block balance from
sd).
- EIP-161 empty-removal: a writeset entry that's {bal=0, nonce=0, empty codehash} → convert to a delete.
- EIP-7708 transfer/burn logs: compute from the writeset + a self-destructed-addresses list carried on
ExecutionResult (see below) — delete the minIBS + postApplyMessageFunc reconstruction (exec3_parallel.go ~line 2807; the minIBS exists only to SetBalance then LogSelfDestructedAccounts reads it).
- SD storage cascade if still needed (or it falls out of the collector's final state).
- Pass the finished tx output to the commitment calculator: the one writeset →
applyVersionedWrites writes it into SharedDomains/sd.mem (the public DB the calc reads from), AND VersionedWrites.TouchUpdates(updates) folds it into the calc's commitment.Updates. Calc reads post-block siblings from sd.mem via asOfStateReader.
Block-end finalize (block reward, withdrawals, EIP-4788/7002/7251 syscalls) genuinely runs state transitions and keeps a real IBS — it's outside the per-tx flow and not in scope here.
Concrete changes
ExecutionResult gains SelfDestructedAddresses []common.Address (regardless of residual balance) — populated by the worker; consumed by per-tx serial-finalize for EIP-7708 and (potentially) the SD storage cascade.
txResult.writes = filterWritesByVersionMap(result.CollectorWrites, be.blockIO.WriteSet(txIndex)) instead of normalizeWriteSet(...).
- the commitment calc consumes
txResult.writes.TouchUpdates(...) — delete calcState.ApplyWrites/FlushToUpdates.
- delete
normalizeWriteSet and resolveStorageWrites (the latter is already DEPRECATED dead code).
- unify the per-tx and block-finalize collector paths (
LightCollector vs versionedWriteCollector — one of them).
IBS.VersionedWrites(checkDirty) stays, used only for BlockSTM read/write dependency tracking — it must stop being the apply/commitment payload.
Validation gate before deleting anything
Remove the EXEC3_PARALLEL-gated SkipLoad/t.Skips from #21136 one by one — if the consolidated path is faithful, test_double_kill, test_dynamic_create2_selfdestruct_collision, the prague test_system_contract_deployment variants, tipInsideBlock, and TestEIP7708BurnLogWhenCoinbaseSelfDestructs should all pass without per-bug heuristics. That's the success criterion.
Eventual rework (separate, larger)
Extract the whole serial flow (MakeWriteSet → commitment fold → domain write → BAL) to run post-validation as the single path; serial becomes a 1-tx-at-a-time consumer of it; IBS becomes a pure per-tx transaction-state. This issue is the stepping stone.
Follow-up to #21136. The recurring parallel-exec-vs-serial divergences (SELFDESTRUCT/recreate, EIP-161 empty-removal, EIP-7708 burn logs, fork-transition, coinbase) all come from the post-execution wrapper re-deriving "what did this tx do" via the heuristic
normalizeWriteSet(versionWritten-log)path and thecalcStatere-derivation, instead of using the faithfulMakeWriteSet-style output that the validated worker already produces. ~half of genuine parallel-executor PRs over the last 2 months are this family. This issue is the consolidation that makes those bugs structurally impossible.Target per-tx flow (incremental — does NOT require the full "IBS = pure transaction-state" rework)
MakeWriteSetinto aLightCollector→result.CollectorWrites= every dirty object's final state.filterWritesByVersionMap(result.CollectorWrites, vmWrites)(faithful, pruned to the keys the tx actually touched — bothfilterWritesByVersionMapandCollectorWritesalready exist).sd).ExecutionResult(see below) — delete theminIBS+postApplyMessageFuncreconstruction (exec3_parallel.go~line 2807; theminIBSexists only toSetBalancethenLogSelfDestructedAccountsreads it).applyVersionedWriteswrites it intoSharedDomains/sd.mem(the public DB the calc reads from), ANDVersionedWrites.TouchUpdates(updates)folds it into the calc'scommitment.Updates. Calc reads post-block siblings fromsd.memviaasOfStateReader.Block-end finalize (block reward, withdrawals, EIP-4788/7002/7251 syscalls) genuinely runs state transitions and keeps a real IBS — it's outside the per-tx flow and not in scope here.
Concrete changes
ExecutionResultgainsSelfDestructedAddresses []common.Address(regardless of residual balance) — populated by the worker; consumed by per-tx serial-finalize for EIP-7708 and (potentially) the SD storage cascade.txResult.writes = filterWritesByVersionMap(result.CollectorWrites, be.blockIO.WriteSet(txIndex))instead ofnormalizeWriteSet(...).txResult.writes.TouchUpdates(...)— deletecalcState.ApplyWrites/FlushToUpdates.normalizeWriteSetandresolveStorageWrites(the latter is already DEPRECATED dead code).LightCollectorvsversionedWriteCollector— one of them).IBS.VersionedWrites(checkDirty)stays, used only for BlockSTM read/write dependency tracking — it must stop being the apply/commitment payload.Validation gate before deleting anything
Remove the
EXEC3_PARALLEL-gatedSkipLoad/t.Skips from #21136 one by one — if the consolidated path is faithful,test_double_kill,test_dynamic_create2_selfdestruct_collision, the praguetest_system_contract_deploymentvariants,tipInsideBlock, andTestEIP7708BurnLogWhenCoinbaseSelfDestructsshould all pass without per-bug heuristics. That's the success criterion.Eventual rework (separate, larger)
Extract the whole serial flow (
MakeWriteSet→ commitment fold → domain write → BAL) to run post-validation as the single path; serial becomes a 1-tx-at-a-time consumer of it; IBS becomes a pure per-tx transaction-state. This issue is the stepping stone.