Skip to content

db/state: restore empty tombstones on unwind (cherry-pick #20627 to release/3.4)#20636

Merged
AskAlexSharov merged 2 commits into
release/3.4from
cherry-pick/pr-20627-to-release-3.4
Apr 18, 2026
Merged

db/state: restore empty tombstones on unwind (cherry-pick #20627 to release/3.4)#20636
AskAlexSharov merged 2 commits into
release/3.4from
cherry-pick/pr-20627-to-release-3.4

Conversation

@AskAlexSharov

Copy link
Copy Markdown
Collaborator

Cherry-pick of #20627 to release/3.4.

Fix the DB-level unwind path so first-time-write reversals leave a proper
empty-value tombstone in the domain values table, rather than deleting the
current-step entry and silently falling back to files (which have no
concept of deletions and therefore return stale pre-unwind data).

`DomainEntryDiff.Value` carries three distinct shapes that must be handled
differently during unwind (per the comment in db/kv/helpers.go):

- nil         — "different step": prev value lives at another step, skip
                  restore (only produced by legacy V0 changesets where
                  valueLen==0 deserializes as nil)
- []byte{}    — "no previous value": key was absent before this step; write
                  an empty tombstone at the unwind step so subsequent reads
                  see the key as absent again
- non-empty   — restore the actual previous value

The old guard `if len(value) > 0` conflated `nil` and `[]byte{}` and
skipped both, leaving no tombstone after unwinding a first-time write.
Corrected to `if value != nil`, matching the diff's documented contract.

The written tombstone lands at max(step, currentFilesEndStep), which by
construction is beyond the frozen file range, so the existing step-age
guard in getLatestFromDb already accepts it without needing to change the
read path.

This is a deliberately narrow re-land of #20483 (reverted by #20509). The
PR's other two pieces — the `len(v)==0` empty-authority bypass in
getLatestFromDb and the LargeValues cross-check in getLatest — are NOT
included here. Regressions surfaced in kurtosis/assertoor_glamsterdam
(which runs with --experimental.bal and the storagerefundtx spammer,
generating a deluge of tombstones) suggest the empty-authority bypass
has downstream interactions with the BAL / post-Amsterdam paths that
need separate narrowing. The interrupted-prune LargeValues case is the
original regression suspect from #20509 and remains out-of-scope.

Tests in db/state/domain_test.go:

- TestDomain_UnwindRestoresDeletionMarker (DupSort + LargeValues) — write
  a key, delete it, re-write within the same step, build files, then unwind
  the re-write. Fails on pre-fix code (getLatest returns the stale post-
  unwind value from the frozen file); passes with this fix. Directly
  exercises the write-side bug.

- TestDomain_DeletedKeyNotResurrectedByFiles (DupSort + LargeValues) —
  retained as a forward regression guard for the read-side contract,
  even though it passes on both pre-fix and post-fix code on current main
  (file-build + prune semantics have evolved since #20483 such that the
  original scenario no longer triggers the read-side bug on its own).

Tested: go test -short ./db/state/... all pass; make lint 0 issues;
make erigon builds.
@AskAlexSharov AskAlexSharov force-pushed the cherry-pick/pr-20627-to-release-3.4 branch from f63977b to d72790e Compare April 18, 2026 02:09
Remove 3 test helpers/tests that depend on main-only APIs (beginForTests,
_testBuildAccessorHook). Replace beginForTests() with BeginFilesRo() in
the two tests that are part of this cherry-pick.
@AskAlexSharov AskAlexSharov enabled auto-merge (squash) April 18, 2026 02:56

@erigon-copilot erigon-copilot Bot 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.

LGTM. Correct fix: unwind was treating empty byte slices like "nothing to restore", but here nil and empty have different meanings. nil = previous value lives at another step / skip restore; empty slice = restore empty tombstone so frozen files do not resurrect stale values. Changing the checks from len(value) > 0 to value != nil is the right semantic fix. The two added regression tests are strong and cover both DupSort and LargeValues paths.

@AskAlexSharov AskAlexSharov merged commit e1526bf into release/3.4 Apr 18, 2026
16 of 21 checks passed
@AskAlexSharov AskAlexSharov deleted the cherry-pick/pr-20627-to-release-3.4 branch April 18, 2026 02:57
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.

2 participants