Skip to content

execution/stagedsync: recover CodePath alongside CodeHashPath in parallel normalizeWriteSet - #21706

Open
mh0lt wants to merge 9 commits into
mainfrom
mh/parallel-codepath-recovery
Open

execution/stagedsync: recover CodePath alongside CodeHashPath in parallel normalizeWriteSet#21706
mh0lt wants to merge 9 commits into
mainfrom
mh/parallel-codepath-recovery

Conversation

@mh0lt

@mh0lt mh0lt commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Recover CodePath alongside CodeHashPath in the parallel executor's
normalizeWriteSet, so an account can never be persisted with a non-empty
codeHash but no code bytes (codeHash-no-code).

Root cause

On re-execution, SetCode short-circuits (so.Code() already returns the prior
incarnation's bytes, bytes.Equal), so the validated incarnation emits no fresh
CodePath. normalizeWriteSet keeps CodeHashPath (resolved from the version
map / filled from committed state) but the incarnation filter drops the stale
CodePath — leaving a codeHash with no code. A later block that CALLs the
contract executes it as empty and diverges (wrong gas → wrong root). Two ways the
fresh CodePath goes missing:

  • CREATE/CREATE2 of an ordinary contract — reproduced on mainnet: a Safe proxy
    deployed via the SafeProxyFactory at block 25291004, CALLed and reverted
    6785 blocks later at 25297789.
  • EIP-7702 delegating tx — the delegated sender then fails EIP-3607 as
    "sender not an eoa".

Fix

After the fill-missing pass in normalizeWriteSet: for any address with a
non-empty CodeHashPath in the output but no CodePath (and not
self-destructed), recover the code so code always travels with its hash —

  • versionMap hit → the code was written in this block; recover it for any
    contract (genuine in-block code).
  • versionMap miss → fall back to committed state via stateReader, bounded to
    7702 designators (stateReader also returns an unchanged contract's existing
    code, and re-emitting that for every touched contract is write amplification with
    no correctness benefit; an unchanged contract's code is already in CodeDomain).

The recovered code is validated to hash to the codeHash it is recovered for
(crypto.Keccak256Hash(code) == codeHash.Value()); a mismatch is skipped, so
recovery never writes code that disagrees with its hash.

Tests

  • TestNormalizeWriteSet_CodePathTravelsWithCodeHash — 7702 designator via the
    versionMap.
  • TestNormalizeWriteSet_CodePathRecoveredFromStateReader — committed 7702
    designator via the stateReader fallback.
  • TestNormalizeWriteSet_CodePathRecoveredForCreatedContract — ordinary CREATE2
    contract (the SafeProxyFactory case).
  • TestNormalizeWriteSet_CodePathRecoveryRejectsHashMismatch — code whose keccak
    ≠ the recovered hash is rejected, not emitted.

All fail-without / pass-with the fix on main.

Scope and limitations

Forward-prevention only. This stops new codeHash-no-code from being written;
it cannot repair codeHash-no-code already collated into immutable snapshot .kv
files — that requires a snapshot unwind.

Targets main; a cherry-pick to release/3.5 will follow (not release/3.4,
which does not run parallel execution by default).

@mh0lt
mh0lt requested a review from yperbasis as a code owner June 9, 2026 15:32
@mh0lt

mh0lt commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Update: on-node reproduction + strengthened fix

Running the perf-stack binary on a cold mainnet minimal resync reproduced this bug on re-execution — sender not an eoa at blocks 25277235, 25279079, and 25280960 (different addresses each time). The original recovery did not fire on these ([codepath-recovery] silent), which exposed a gap:

Root cause of the gap: the recovery only re-emitted code present in this tx's versionMap (rr.Version().TxIndex == txIndex). The real failure path is a re-executing 7702 delegation whose code equals the already-committed designator → IBS.SetCode short-circuits (bytes.Equal) → the validated incarnation writes no CodePath, and the prior incarnation's versionMap entry is invalidated on re-exec. So the versionMap has nothing for this tx; the fill-missing loop still fills CodeHashPath from committed state → codeHash persists with no code.

Strengthened fix (7a84cb413d): recover the designator from the versionMap, else fall back to stateReader.ReadAccountCode (mirroring how CodeHashPath is recovered), and gate emission on types.ParseDelegation so only 7702 designators are re-emitted — never ordinary unchanged contract code (no write amplification, no callee misattribution). New TestNormalizeWriteSet_CodePathRecoveredFromStateReader covers the stateReader path.

Scope: this prevents the drop during forward execution. It cannot repair state already collated into immutable snapshots with codeHash-but-no-code — that requires a snapshot unwind (separate work, in development). Full forward validation therefore needs a fresh resync with this fix in place (the existing corrupt datadir can't validate it).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes an intermittent parallel-execution consistency gap in normalizeWriteSet where an address can end up with a non-empty CodeHashPath persisted without its companion CodePath, causing EIP-7702 delegated EOAs to be misclassified (surfacing as false EIP-3607 “sender not an eoa” rejections).

Changes:

  • Add a recovery pass in normalizeWriteSet to re-emit missing CodePath when a non-empty CodeHashPath is present (and the code parses as an EIP-7702 delegation designator).
  • Add unit tests covering both version-map recovery and stateReader fallback recovery scenarios for the dropped-CodePath case.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
execution/stagedsync/exec3_parallel.go Adds CodePath recovery logic to keep delegation designator code aligned with its emitted CodeHashPath during parallel normalize.
execution/stagedsync/exec3_finalize_test.go Adds regression tests ensuring CodePath is recovered alongside CodeHashPath for EIP-7702 delegation scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread execution/stagedsync/exec3_parallel.go Outdated

@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.

Requesting changes on three points (plus a style one):

  1. Verify recovered code against the emitted hash before re-emitting. The hash and the code can come from different sources (vm.Read at ceiling txIndex+1 can return another tx's write; the stateReader current-view races the publish loop's apply progress). On disagreement this persists code that doesn't match the persisted codeHash — and CodeDomain isn't covered by the state root, so a wrong designator target would silently misdirect later execution, worse than today's loud sender not an eoa. A keccak over ≤23 bytes, skipping emission on mismatch, closes this. (Same gap as Copilot's inline comment.)

  2. Narrow the trigger. The fill-missing loop emits CodeHashPath for every touched account, so the recovery runs for every touched non-empty-code account on the serialized result-processing path: a full stateReader.ReadAccountCode per storage-touched contract per tx just to discard via ParseDelegation, and a redundant CodePath re-emit for every tx touching a delegated EOA (deduped at DomainPut, but still TouchKey + GetLatest + writeLog per touch — constant on post-Pectra mainnet). Suggest gating recovery on the address having a CodePath/CodeHashPath entry (any incarnation) in the raw writeset: a drop requires this tx to have written code; when the hash comes purely from the fill loop, the code is already committed in CodeDomain (or unrecoverable anyway). With that, recovery should ~never fire in production — so also add a log when it does; a silent safety net hides the upstream scheduler bug it catches (the [codepath-recovery] log mentioned in the PR comments isn't in this diff).

  3. Correct the mechanism claims in the comments/test docstrings. The premise of TestNormalizeWriteSet_CodePathTravelsWithCodeHash — "blockIO.WriteSet retains both incarnations' entries (versionMap doesn't clear old)" — is contradicted by the pipeline: RecordWrites replaces the per-tx write set (versionedio.go:1321) and re-exec shrinkage deletes dropped paths from the versionMap (exec3_parallel.go:2331); IBS emits CodePath/CodeHashPath/CodeSizePath atomically. The "short-circuit against an already-committed designator" path also produces no corruption — the committed designator bytes are already in CodeDomain, which is why the narrowed gate loses nothing. The tests are fine as robustness tests of normalizeWriteSet's contract against arbitrary inputs, but the docstrings shouldn't present a mechanism the scheduler prevents. If the trigger is narrowed per point 2, TestNormalizeWriteSet_CodePathRecoveredFromStateReader should assert no emission instead.

  4. Comment style: the new multi-paragraph block comments are well over the repo's comment limits — condense to one or two sentences stating the invariant; the scenario forensics belong in the PR description.

mh0lt added a commit that referenced this pull request Jun 11, 2026
…perf-statecache-lru-pr

Reconcile #21386 onto the cleaned commitment-cache model from #21380.

Resolution:
- branch_cache.go (+test), temporal_mem_batch.go, kv_interface.go -> take
  #21380's cleaned model: reduced BranchCache API, txN-watermark UnwindTo,
  FlushOption pattern, flush-callback-after-MDBX-write.
- preload*.go, trunk_pin_test.go -> deleted (trunk-pin extracted to
  mh/branch-cache-trunk-pin).
- domain_shared.go -> combine: keep #21386's StateCache refresh, keccak
  code-cache fix, codeHash->code read bypass, per-worker kvmetrics (wm) and
  the collector/reqMetrics fields; adopt #21380's cleaned FlushOption
  multi-domain callback (cb-after-MDBX-write), MeteredGetterWithTxN watermark
  + txN=0 skip; drop the extracted adaptive-pin controller + PublishMetrics.

Notable behavioural deltas (flagged for review):
- BranchCache unwind: #21386's epoch/unwindFloor model -> #21380's
  txN-watermark UnwindTo (same effect: evict entries above the unwind point).
- StateCache flush entries now stamped with sd.txNum (batch high-water) as the
  unwind watermark: the cleaned WithFlushCallback exposes step, not per-key
  txN; sd.txNum is a safe conservative upper bound.
- temporal_mem_batch.go DomainMetrics refs retargeted changeset -> kvmetrics.

Carries: keccak codeHash fix, #21706 CodePath recovery, stateCache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mh0lt added a commit that referenced this pull request Jun 12, 2026
…comments

Addresses review on #21706:
- Copilot: the CodePath recovery now validates that the recovered code hashes
  to the CodeHashPath it is recovered for; a mismatch is skipped rather than
  emitted, so recovery can never write code that disagrees with its hash.
- AskAlexSharov: drop the cryptic InternCodeHash(hh) != h comparison — compare
  crypto.Keccak256Hash(code) against the codeHash's raw Value() (no interning,
  no heap escape).
- Comment policy: trim the recovery comment to the invariant + gating rationale;
  the forward-only limitation and the mainnet reproduction move to the PR body.

New test TestNormalizeWriteSet_CodePathRecoveryRejectsHashMismatch pins the
reject-on-mismatch guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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.

Core finding

The pipeline can't produce the codeHash-no-code this fix targets, so recovery is a safety net for an invariant that currently holds — not a fix for the reported sender not an eoa.

  • IntraBlockState.SetCode emits CodePath+CodeHashPath+CodeSizePath atomically — all three or none (intra_block_state.go:1322-1339).
  • RecordWrites replaces the per-tx output, it doesn't accumulate incarnations (versionedio.go:1321).
  • Re-exec shrinkage deletes dropped paths from the versionMap (exec3_parallel.go:2335).

So the incarnation filter never sees a lone stale CodePath to drop. A CodeHashPath-without-CodePath in the output can only come from the fill-missing loop, and in both of its sources (a prior tx's versionMap write, or committed state) the code is already in CodeDomain. On a clean datadir no new corruption is created; on a datadir whose snapshots already hold codeHash-no-code, ReadAccountCode returns empty → ParseDelegation fails → recovery is the silent no-op already observed. Defensible only if narrowed so it never fires in production and logs when it does.

Status of the four points (latest 953389bd)

  1. Verify recovered code against the emitted hash — ✅ done. emit checks Keccak256Hash(code) == want.Value(); TestNormalizeWriteSet_CodePathRecoveryRejectsHashMismatch covers it.
  2. Narrow the trigger + log when it fires — ❌ still iterates the filled codeHashInOutput, so ReadAccountCode runs per touched non-empty-code account per tx and re-emits for every touched delegated EOA; no log added.
  3. Correct the false mechanism in the docstrings — ❌ TestNormalizeWriteSet_CodePathTravelsWithCodeHash still claims "blockIO.WriteSet retains both incarnations' entries (versionMap doesn't clear old)", contradicted by versionedio.go:1321 + exec3_parallel.go:2335. If the trigger is narrowed, …RecoveredFromStateReader should assert no emission.
  4. Comment style — ❌ the recovery block is still a 4-sentence scenario paragraph, and the test docstrings still embed mainnet block numbers — over the repo's comment limits.

Point 1 is correct hardening, but 2–4 remain open. Holding at changes requested.

@mh0lt

mh0lt commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Addressed yperbasis's remaining three points in 73ae786a76:

Point 2 — narrow trigger + log when fires. Recovery now gates on a codeAddrInRaw set built from the raw writes parameter (CodePath or CodeHashPath, any incarnation). When the codeHash reaches the output via the fill-missing loop alone, the address isn't in the set and recovery skips. log.Warn("[codepath-recovery] …") fires on every emit so the upstream scheduler gap that would justify the safety net is visible if it ever occurs in production.

Point 3 — flipped TestNormalizeWriteSet_CodePathRecoveredFromStateReader. It now asserts countPath(result, state.CodePath) == 0 with the rationale that the committed designator is already in CodeDomain — exactly the case the narrowed gate is designed to skip. The two surviving recovery tests (…TravelsWithCodeHash and …RecoveredForCreatedContract) keep CodePath/CodeHashPath in their raw writesets so the gate lets them through, and the rejection test still asserts no emission on hash mismatch.

Point 4 — comment style. The production-side recovery block is now 3 lines stating the invariant; the test docstrings are 2-3 lines each, no mainnet block numbers, no SetCode/SafeProxyFactory forensics, no "blockIO.WriteSet retains both incarnations" mechanism claim. make lint clean (twice), make erigon clean, the four TestNormalizeWriteSet_CodePath* tests plus the wider TestNormalizeWriteSet|TestCalcFees|TestFlushToUpdates suite still green.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@AskAlexSharov
AskAlexSharov requested a review from Copilot June 23, 2026 03:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@AskAlexSharov

Copy link
Copy Markdown
Collaborator

The PR branch is based on a pre-isAura version of normalizeWriteSet. Since the branch was authored, main merged a signature change adding isAura bool as an 8th parameter, plus ctx context.Context to execLoopExitCheck and processCommitErr. This causes typecheck failures in calc_state_test.go and exec3_parallel_robustness_test.go.

Fix: rebase onto current main and update the 4 new test calls in exec3_finalize_test.go from 7 to 8 args (append , false for isAura). Rebased version is at alex/fix_21706_lint if you'd like to pull from it.

mh0lt and others added 6 commits June 23, 2026 11:25
…WriteSet

normalizeWriteSet recovered an account's CodeHashPath from the versionMap (via
the CodeHashPath case and the fill-missing-fields loop) but had no equivalent
recovery for CodePath: the CodePath case kept the write only at the validated
incarnation, and the fill loop never emitted code. A tx whose validated
writeset lacked a fresh CodePath — e.g. an EIP-7702 delegating tx that
re-executes, where SetCode short-circuits because so.Code() already returns the
designator written by the prior incarnation (bytes.Equal(prevcode, code)) —
therefore persisted a non-empty codeHash with no code bytes. A later block then
read empty code for the delegated account, and the EIP-3607 sender check
wrongly rejected the 7702 sender ("sender not an eoa").

Recover the code this tx wrote from the versionMap (incarnation-agnostic,
scoped to this tx so a merely-touched contract's prior-tx code is not
re-emitted) whenever an account has a non-empty codeHash but no code in the
normalized output, mirroring CodeHashPath. Code can no longer be lost while its
hash survives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier recovery only re-emitted code found in THIS tx's versionMap
(rr.Version().TxIndex == txIndex). On the real failure path that guard
misses: a re-executing 7702 delegation whose code equals the already-
committed designator makes IBS.SetCode short-circuit (bytes.Equal), so
the validated incarnation writes no CodePath and the prior incarnation's
versionMap entry is invalidated on re-exec — the versionMap holds nothing
for this tx. The fill-missing loop still fills CodeHashPath from committed
state, so the account persists a codeHash with no code; a later 7702
sender then reads empty code and is wrongly rejected "sender not an eoa"
(observed re-executing mainnet blocks 25277235 / 25279079 / 25280960).

Recover the designator from the versionMap, else fall back to the
post-state via stateReader.ReadAccountCode (mirroring how CodeHashPath is
recovered). Gate emission on types.ParseDelegation so only 7702
designators are re-emitted — never ordinary unchanged contract code for a
touched contract (no write amplification, no callee-code misattribution).

This prevents the drop during forward execution. It cannot repair state
already collated into immutable snapshots with codeHash-but-no-code; that
needs a snapshot unwind (separate, in development).

Adds TestNormalizeWriteSet_CodePathRecoveredFromStateReader for the
short-circuit/stateReader path; the existing versionMap-path test stays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… not just 7702

The CodePath recovery in normalizeWriteSet was bounded to EIP-7702 designators
(types.ParseDelegation), so an ordinary CREATE/CREATE2 contract whose validated
incarnation's SetCode short-circuited (re-execution: so.Code() already returns
the prior incarnation's bytes) had its stale-incarnation CodePath dropped by the
incarnation filter while CodeHashPath survived — persisting a non-empty codeHash
with no code (codeHash-no-code). A later block that CALLs the contract then runs
it as empty and diverges: mainnet 25291004 deploys a SafeProxyFactory CREATE2
proxy whose code is dropped; block 25297789 calls it, the call reverts on the
missing code, and the block fails with a gas mismatch (−346,536 gas) — a
deterministic wedge under both parallel and serial re-exec.

Generalize the recovery: when the versionMap holds CodePath for the address
(the code was written in THIS block — a deploy, code change, or 7702 designator),
recover it unconditionally; it is always genuine in-block code, never an
unchanged contract's bytes. Keep the ParseDelegation gate only on the
stateReader fallback (versionMap miss), since stateReader also returns an
unchanged contract's existing code and re-emitting that for every touched
contract would be write amplification.

Forward-prevention only — cannot repair codeHash-no-code already collated into
immutable snapshot files (that needs a snapshot unwind).

Adds TestNormalizeWriteSet_CodePathRecoveredForCreatedContract (ordinary
bytecode, versionMap-hit path; fails under the old 7702-only gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…comments

Addresses review on #21706:
- Copilot: the CodePath recovery now validates that the recovered code hashes
  to the CodeHashPath it is recovered for; a mismatch is skipped rather than
  emitted, so recovery can never write code that disagrees with its hash.
- AskAlexSharov: drop the cryptic InternCodeHash(hh) != h comparison — compare
  crypto.Keccak256Hash(code) against the codeHash's raw Value() (no interning,
  no heap escape).
- Comment policy: trim the recovery comment to the invariant + gating rationale;
  the forward-only limitation and the mainnet reproduction move to the PR body.

New test TestNormalizeWriteSet_CodePathRecoveryRejectsHashMismatch pins the
reject-on-mismatch guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gate the recovery loop on the address having a CodePath/CodeHashPath
entry in the raw writeset, and log when it fires. When the codeHash
reaches the output via the fill-missing loop the code is already
committed in CodeDomain, so recovery would be redundant and would
otherwise iterate stateReader.ReadAccountCode for every touched
non-empty-code account.

Flip TestNormalizeWriteSet_CodePathRecoveredFromStateReader to assert
no CodePath emission, matching the narrowed gate. Strip the false
"blockIO.WriteSet retains both incarnations' entries" mechanism claim
and the mainnet block-number forensics from the other docstrings per
the project comment policy.
Add missing isAura=false argument to the 4 new test calls added by
this branch; main gained the isAura bool parameter after this branch
was authored.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread execution/stagedsync/exec3_parallel.go Outdated
Comment thread execution/stagedsync/exec3_finalize_test.go Outdated
Comment thread execution/stagedsync/exec3_finalize_test.go
- log.Debug instead of Warn for the [codepath-recovery] re-emit (it runs in the
  per-tx hot path; reorg/re-exec shouldn't spam Warn), and drop the redundant
  common.Address() cast — addr.Value() already returns common.Address.
- Rename TestNormalizeWriteSet_CodePathRecoveredFromStateReader →
  …NoCodePathRecoveryWithoutRawCodeWrite: it asserts recovery does NOT fire when
  the raw writeset has no code entry, so the old name was misleading.
- Make …RejectsHashMismatch non-vacuous: assert the CodeHashPath is present
  (recovery eligible) so the 0 CodePath is a genuine hash-mismatch rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mh0lt

mh0lt commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review (current head c68af1f):

yperbasis's four points

  1. Verify recovered code against the emitted hash — done; emit skips unless Keccak256Hash(code) == want.Value() (TestNormalizeWriteSet_CodePathRecoveryRejectsHashMismatch).
  2. Narrow the trigger + log when it fires — done; recovery is gated on the address having a raw-writeset CodePath/CodeHashPath entry (codeAddrInRaw), so it never runs for accounts whose CodeHashPath came purely from the fill loop. It now logs (log.Debug, this commit — Debug not Warn since it's the per-tx hot path).
  3. Correct the mechanism claims / assert-no-emission — done; the docstrings no longer assert the scheduler-prevented mechanism, and the no-raw-code test now asserts no emission (and is renamed …NoCodePathRecoveryWithoutRawCodeWrite).
  4. Comment style — done; the recovery block is a 3-line invariant comment and the test docstrings no longer embed mainnet block numbers.

On the core finding — agreed it's a safety net for an invariant the scheduler currently upholds, not a live repro. It's now narrowed so it should never fire in production and logs (Debug) if it ever does, so it surfaces an upstream scheduler regression rather than silently masking one.

Copilot's 3 inline comments also addressed (log level + redundant cast, test rename, non-vacuous hash-mismatch test). Build + make lint clean, the four normalizeWriteSet tests pass.

@yperbasis yperbasis modified the milestones: 3.5.0, 3.6.0 Jun 23, 2026
@mh0lt

mh0lt commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@yperbasis ready for re-review — CI green (run 28036125669), all review threads resolved. The remaining review items were addressed in c68af1f: log level + redundant common.Address cast, the misleading …RecoveredFromStateReader test renamed to …NoCodePathRecoveryWithoutRawCodeWrite, and …RejectsHashMismatch made non-vacuous (asserts a CodeHashPath is present so the 0 CodePath is a genuine rejection). Points 1–4 from your earlier review are all done; the recovery is now a narrowed, logged safety net.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread execution/stagedsync/exec3_finalize_test.go
Comment thread execution/stagedsync/exec3_finalize_test.go Outdated
Comment thread execution/stagedsync/exec3_parallel.go Outdated
Comment thread execution/stagedsync/exec3_parallel.go Outdated
- Prealloc codeAddrInRaw/codeInOutput/codeHashInOutput maps (hot path).
- Add TestNormalizeWriteSet_CodePathRecoveredFromStateReader covering the
  stateReader+ParseDelegation fallback when the versionMap misses.
- Fix the reversed assertion message (regression is codeHash-without-code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mh0lt
mh0lt requested a review from AskAlexSharov June 26, 2026 09:39
@yperbasis yperbasis modified the milestones: 3.6.0, 3.7.0 Jun 29, 2026
@yperbasis
yperbasis requested a review from Copilot June 29, 2026 10:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

// CodePath must travel with CodeHashPath; gated on raw-writeset
// CodePath/CodeHashPath presence to skip the fill-missing-loop case where
// the code is already in CodeDomain.
codeAddrInRaw := make(map[accounts.Address]bool, len(writes))
codeAddrInRaw[w.Address] = true
}
}
codeInOutput := make(map[accounts.Address]bool, len(filtered))
}
}
codeInOutput := make(map[accounts.Address]bool, len(filtered))
codeHashInOutput := make(map[accounts.Address]accounts.CodeHash, len(filtered))
Resolve conflicts in the parallel normalizeWriteSet CodePath recovery: keep this
branch's refined version (raw-writeset gate via codeAddrInRaw, emit() that
verifies keccak(code)==codeHash before re-emitting, and the [codepath-recovery]
debug log) over main's earlier broad inline recovery, and take this branch's
refined recovery tests (which drop the false "versionMap retains both
incarnations" docstring and add the no-recovery-without-raw-code-write and
hash-mismatch-rejection cases).
mh0lt added a commit that referenced this pull request Jul 2, 2026
The parallel normalizeWriteSet recovery re-emits code recovered from the
versionMap (ceiling read) or stateReader post-state, both of which can race and
return bytes that disagree with the codeHash this tx emitted. Only re-emit when
crypto-hash(code) == codeHash, matching the hardening required on #21706; else
skip, so we never persist code that mismatches its hash.

@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.

Requesting changes:

  • Recovery still fails open: a hash mismatch or ReadAccountCode error leaves CodeHashPath without CodePath and normalization continues. Return an error; the mismatch test should assert failure rather than this inconsistent output.
  • The positive tests hand-construct stale cross-incarnation writes that the live pipeline cannot produce: SetCode emits the code trio atomically, RecordWrites replaces transaction output, and re-execution deletes dropped paths. Add an execution-level failing reproduction, or reframe this as invariant hardening and correct the PR claims.
  • The three recovery maps are sized from all writes. A 1,024-entry storage-only benchmark regressed from 9,472 to 120,304 B/op, 1 to 16 allocs/op, and about 44 to 50 microseconds/op. Allocate lazily or by code-related entries.

The head also conflicts with main, where normalization has moved to execution/state; this needs a rebase and redesign against the current implementation.

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.

4 participants