Skip to content

fix(seidb): stop evm-logical-digest replay mode from mutating a live memiavl - #3840

Open
blindchaser wants to merge 2 commits into
mainfrom
fix/seidb-digest-replay-clone
Open

fix(seidb): stop evm-logical-digest replay mode from mutating a live memiavl#3840
blindchaser wants to merge 2 commits into
mainfrom
fix/seidb-digest-replay-clone

Conversation

@blindchaser

Copy link
Copy Markdown
Contributor

Problem

--memiavl-open-mode replay relied on memiavl.Options{ReadOnly: true},
which only skips the LOCK file. The changelog is still opened read-write —
db.go says so outright ("Even in read-only mode we may need WAL replay") —
and sei-db's wal.open() answers wal.ErrCorrupt with os.Truncate on the
node's segment. Nothing on that path consults ReadOnly.
On a live node a torn tail is the writer mid-append, not corruption.
Truncating it drops committed versions, and since the writer's fd keeps its
old offset, its next append leaves a zero-filled hole — which binary.Uvarint
reads as valid empty records rather than an error. Nothing fails at the time;
the node finds out at its next restart.

Fix

Clone, as openFlatKVReadOnly already does for the other backend: hardlink
the snapshot, byte-copy the changelog, verify it still covers
snapshotVersion+1, retry if the writer prunes mid-clone, then open the clone
normally, lock and all. Every source access is now a read.
Two changes fall out of it:

  • reject a clone that falls short of --height (a tail repair costs the
    trailing version, and a silently early digest looks like real divergence)
  • export memiavl's SeekSnapshotDir instead of reimplementing its snapshot
    layout in the tool

Limits

Tool-scoped only. memiavl.OpenDB(ReadOnly) is still reachable from wasm
snapshot export and storev2/rootmulti proof queries; fixing it at the WAL
layer is tracked separately. The source LOCK is still not taken — a racing
writer costs us a retry now instead of costing the node its changelog.
--memiavl-open-mode snapshot (the default) was never affected.
Callers need a writable dbDir and room for the changelog copy, as the
FlatKV path already did.

Tests

  • source directory is byte-identical after replaying over a torn tail, with
    the torn bytes left intact for the live writer
  • the tool opens while a live writer holds the source LOCK
  • a changelog short of the requested height is rejected, no clone left behind

…memiavl

--memiavl-open-mode=replay relied on memiavl's ReadOnly option, which only
skips the LOCK file. The changelog is still opened read-write, and sei-db's
wal.open() answers wal.ErrCorrupt with os.Truncate on the node's segment.

On a live node a torn tail is the writer mid-append, not corruption.
Truncating it drops committed versions, and since the writer's fd keeps its
old offset, its next append leaves a zero-filled hole that binary.Uvarint
reads as valid empty records rather than an error. The node keeps producing
blocks and finds out at its next restart.

Clone instead, as openFlatKVReadOnly already does for the other backend:
hardlink the snapshot, byte-copy the changelog, verify it still covers
snapshotVersion+1, retry if the writer prunes mid-clone, then open the clone
normally. Every source access is now a read.

Also reject a clone that falls short of --height, since a tail repair costs
the trailing version and a silently early digest looks like real divergence;
and export memiavl's SeekSnapshotDir rather than reimplementing its snapshot
layout in the tool.

Replay mode now needs a writable dbDir and room for the changelog copy, as
the FlatKV path already did. memiavl.OpenDB(ReadOnly) is still reachable from
wasm snapshot export and storev2/rootmulti proof queries; fixing it at the WAL
layer is tracked separately.

Co-authored-by: Cursor <[email protected]>
@blindchaser
blindchaser force-pushed the fix/seidb-digest-replay-clone branch from 66d9e50 to c6308d5 Compare August 3, 2026 13:41
@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes how seidb reads live memiavl on production nodes (disk for changelog copy, clone lifecycle); incorrect clone/WAL logic could mis-report digests, but the fix directly addresses prior live-data corruption from read-only replay.

Overview
evm-logical-digest --memiavl-open-mode replay no longer opens the live memiavl root with ReadOnly. It now hardlinks the snapshot, byte-copies the changelog into a temp clone under dbDir, validates WAL coverage, and replays only inside that private copy—matching the existing FlatKV tooling pattern so WAL “torn tail repair” cannot truncate a running node’s changelog.

Shared toolClone plumbing (owner flock, stale-clone sweep, retryToolingClone, torn-tail warnings) is factored out and wired through FlatKV open paths as well. Memiavl replay adds explicit --height mismatch errors when the clone cannot reach the requested version after repair.

memiavl exports SeekSnapshotName and ReadMetadata so tooling uses the package’s snapshot layout and bootstrap initial version when checking that the cloned WAL covers catchup (fixes snapshot-0 + SetInitialVersion(N) nodes that previously failed with “source kept churning”).

Reviewed by Cursor Bugbot for commit 87b886c. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 3, 2026, 3:45 PM

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.32432% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.64%. Comparing base (a8027cc) to head (87b886c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/tools/cmd/seidb/operations/memiavl_open.go 50.00% 19 Missing and 15 partials ⚠️
sei-db/tools/cmd/seidb/operations/tool_clone.go 74.50% 7 Missing and 6 partials ⚠️
sei-db/tools/cmd/seidb/operations/flatkv_open.go 76.19% 8 Missing and 2 partials ⚠️
...b/tools/cmd/seidb/operations/evm_logical_digest.go 18.18% 9 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3840      +/-   ##
==========================================
- Coverage   61.29%   60.64%   -0.65%     
==========================================
  Files        2351     2280      -71     
  Lines      197369   189948    -7421     
==========================================
- Hits       120982   115202    -5780     
+ Misses      65535    64496    -1039     
+ Partials    10852    10250     -602     
Flag Coverage Δ
sei-chain-pr 31.24% <61.62%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 75.69% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-db/state_db/sc/memiavl/db.go 67.18% <100.00%> (+0.75%) ⬆️
sei-db/state_db/sc/memiavl/multitree.go 79.39% <100.00%> (ø)
...b/tools/cmd/seidb/operations/evm_logical_digest.go 23.14% <18.18%> (+0.23%) ⬆️
sei-db/tools/cmd/seidb/operations/flatkv_open.go 67.67% <76.19%> (+1.69%) ⬆️
sei-db/tools/cmd/seidb/operations/tool_clone.go 74.50% <74.50%> (ø)
sei-db/tools/cmd/seidb/operations/memiavl_open.go 50.00% <50.00%> (ø)

... and 74 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66d9e50150

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// our changelog copy, then prune the changelog up to that newer
// version — leaving a copy that no longer covers snapshotVersion+1 and
// a catchup that would silently skip versions. Retryable.
if err := verifyClonedWALCovers(dstChangelogDir, snapshotVersion); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle non-default initial heights in WAL validation

When memiavl has been initialized with a height greater than 1, its initial snapshot is still snapshot-0, while the first changelog entry uses the configured initial height (for example, SetInitialVersion(100) produces WAL version 100). Passing that clone to verifyClonedWALCovers treats the valid first entry as a gap because it requires firstVer <= snapshotVersion+1, so replay mode retries three times and fails until a later snapshot is written. Derive the expected successor from the snapshot's stored initial-version metadata rather than assuming version 1 follows snapshot 0.

Useful? React with 👍 / 👎.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I didn't find any additional bugs, but this PR reworks memiavl's tooling replay path with several subtle concurrency/correctness invariants (hardlink-vs-copy cloning, WAL-coverage validation, retry-on-churn, shortfall rejection), so a human pass is worthwhile before merging.

What was reviewed:

  • The new clone-and-replay path (openMemiAVLReplay/tryPrepareMemIAVLToolingClone) and its reuse of the FlatKV tooling's retryToolingClone/verifyClonedWALCovers helpers.
  • Confirmed the --height 0 case intentionally skips the shortfall check (no fixed target height to fall short of) — this matches a candidate issue the bug-hunting system already ruled out.
  • Traced clone cleanup (temp dir + lock release) on both the success and version-mismatch error paths in openedMemIAVL.Close.
  • Checked that SeekSnapshotDir's snapshot selection agrees with OpenDB's own internal seekSnapshot when it re-opens the clone, so the two don't disagree about which snapshot backs the replay.
Extended reasoning...

Overview

This PR reworks how the seidb tooling's evm-logical-digest --memiavl-open-mode replay" opens memiavl: instead of opening the live source directory with memiavl.Options{ReadOnly:true}(which still opens the changelog read-write and can truncate a torn tail out from under a live writer), it now clones the snapshot (hardlink) and changelog (byte copy) into a private temp directory underdbDirand replays there. It also exportsmemiavl.SeekSnapshotDir so the tool no longer reimplements memiavl's snapshot-selection logic, and factors the retry-on-churn logic (prepareFlatKVToolingCloneWith->retryToolingClone) so it's shared between the FlatKV and memiavl tooling clones. Two new files (memiavl_open.go/memiavl_open_test.go) and one exported function in sei-db/state_db/sc/memiavl/db.goare added;evm_logical_digest.gois updated to use the new*openedMemIAVL` wrapper.

Security risks

None of significance — this is offline/operator diagnostic tooling (seidb CLI), not a code path exercised during consensus, block execution, or RPC serving. The main risk class is data-safety (accidentally corrupting a live node's changelog while running a read-only diagnostic), which is precisely the bug this PR fixes, and it is covered by a dedicated regression test (TestOpenMemiAVLReplayLeavesSourceUntouched) that snapshots the source directory's file contents before/after and asserts byte-for-byte equality, plus a torn tail is deliberately left in place.

Level of scrutiny

Medium. The blast radius is limited to a tool operators run out-of-band against node data directories (not consensus-critical or app-hash-affecting, per the PR's own label), but the change involves nontrivial reasoning about concurrent writer races (hardlink vs. copy, WAL truncation mid-clone, retry semantics, snapshot-vs-changelog consistency) where a subtle mistake could reintroduce the exact class of bug being fixed, or introduce a new one only visible under a live-writer race. That combination — tool-scoped blast radius but concurrency-sensitive logic — puts this above "approve outright" but the existing tests target the specific hazard well.

Other factors

The PR body itself is unusually thorough (problem/fix/limits/tests sections) and explicitly scopes the fix to tooling only, noting the same ReadOnly weakness still exists in memiavl.OpenDB callers elsewhere (wasm snapshot export, storev2/rootmulti) and is tracked separately — that scoping honesty is a good sign. Three new tests exercise the specific hazard (source left untouched over a torn tail, opening while a live writer holds the LOCK, rejecting a clone short of the requested height and not leaving it behind). No CODEOWNERS or lint/build issues were apparent from the diff. The bug-hunting system found zero issues and one candidate (shortfall check skipped for --height 0) was independently confirmed to be correct-by-design rather than a bug.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Solid, well-motivated fix: the replay path now clones the snapshot (hardlink) + changelog (byte-copy) instead of handing a live node's changelog to memiavl's WAL open, which truncates a torn tail. I verified the invariants it relies on (read-only snapshot mmap, rename-published snapshots, Version-bearing changelog entries, and Catchup silently falling short) all hold, so no blocking issues — only non-blocking notes around the unguarded --height 0 default, stale clone cleanup, and test coverage.

Findings: 0 blocking | 8 non-blocking | 4 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Cursor's second-opinion pass produced no output (cursor-review.md is empty), so it contributed nothing to this review. Codex explicitly reported no material issues.
  • Test coverage gap: both success-path tests use height == 0, and the only height > 0 test asserts the rejection. There is no positive test that a non-zero --height resolves an older snapshot via SeekSnapshotDir's seekSnapshot branch and replays forward to exactly that height — i.e. the new strict equality check's success path and the non-current snapshot-selection branch are both untested.
  • No free-space pre-check before byte-copying the changelog. Because the clone must live inside dbDir for hardlinks to work, running the tool against a live node can fill that node's own data volume and halt it. The PR body notes callers "need room for the changelog copy," but nothing enforces or warns at runtime (same gap as the pre-existing FlatKV clone).
  • Behavior change worth surfacing to operators: --memiavl-open-mode replay now requires a writable --memiavl-dir (it creates a temp dir inside it). The flag help was updated to mention a "temp dir" but not that it is allocated inside the source directory, so a read-only-mounted data dir that used to work will now fail. Consider stating this in the flag help / package doc comment, not just the PR description.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

// when that falls short of the requested height. Every tail repair inside
// the clone costs the trailing version, and a digest computed one version
// early is indistinguishable from real divergence when comparing nodes.
if reached := db.Version(); height > 0 && reached != height {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] The height > 0 guard leaves the flag's default (--height 0) with no coverage guarantee, even though the failure mode is identical. With --height 0, verifyClonedWALCovers opens the cloned WAL, tidwall repairs the torn tail by truncating it, and Catchup then replays to tip-1 and reports success — producing exactly the "digest one version early, indistinguishable from real divergence" outcome this check exists to prevent, on the most common invocation.

I acknowledge there's no cheap exact fix: learning the source's intended tip would mean opening the source WAL, which is the mutation this PR removes. But something is better than nothing — e.g. detect that the clone's WAL was repaired (compare the cloned changelog's byte size / last offset before and after verifyClonedWALCovers opens it) and either warn loudly or refuse for height == 0. At minimum, document in the doc comment above that height == 0 is best-effort so a reader doesn't infer the guarantee extends to it.

}
// Not a "-tmp" suffix: memiavl's removeTmpDirs deletes every "*-tmp"
// directory under its root when a node opens the DB read-write.
tempDir, err := os.MkdirTemp(dbDir, ".seidb-memiavl-tool-*")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Nothing ever sweeps abandoned clones. If the tool is SIGKILLed (or the host reboots) between here and Close(), the .seidb-memiavl-tool-* directory survives inside the node's data dir holding hardlinks to the snapshot plus a full byte-copy of the changelog. The hardlinks mean the live node's snapshot pruning frees no space at all, and by design the name avoids the *-tmp suffix so memiavl's removeTmpDirs won't reclaim it on the next node restart either.

This is the same hazard the FlatKV clone already has, so it's not a regression — but this PR doubles the surface. Worth either a seidb subcommand / startup sweep for stale .seidb-*-tool-* dirs, or an explicit note in the flag help that operators must remove them manually after a crash.

// The clone must sit inside dbDir to share a filesystem with the source
// snapshot: dbDir is often its own mount point, so a sibling directory is
// not enough and hardlinks would fail across the boundary.
if err := os.MkdirAll(dbDir, 0o750); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] This MkdirAll can never do useful work: memiavl.SeekSnapshotDir on line 85 already reads current / lists snapshots under dbDir, so it fails first if dbDir is missing. Carried over from tryPrepareFlatKVToolingClone, where it's equally moot. Dropping it (or moving it above the seek, if the intent was to create the dir) would remove a misleading suggestion that this path tolerates an absent source.

Comment thread sei-db/state_db/sc/memiavl/db.go Outdated
//
// Exported for readers that need to resolve a snapshot without opening the DB,
// so they inherit this package's layout rules instead of restating them.
func SeekSnapshotDir(root string, targetVersion int64) (string, int64, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Two small things on this newly exported API:

  1. The name says Dir but the first return value is a directory name relative to root (callers must filepath.Join(root, name), as tryPrepareMemIAVLToolingClone does). The doc comment says "directory name" correctly, but SeekSnapshotName would make the contract obvious at the call site.
  2. No unit test accompanies the export — worth pinning at least the two branches (targetVersion == 0 resolving through current, and targetVersion below the earliest snapshot returning the target version is pruned error), since this is now part of the package's public surface rather than an internal helper.

Comment thread sei-db/tools/cmd/seidb/operations/memiavl_open.go
The clone-based replay open regressed nodes bootstrapped with a non-default
initial height: memiavl keeps its bootstrap snapshot named snapshot-0 even
after SetInitialVersion(N), while the first changelog entry is version N, so
verifyClonedWALCovers treated the valid successor as a gap and replay mode
failed deterministically with "source kept churning" until the first snapshot
rewrite. A freshly recovered chain (genesis initial_height > 1) is exactly
that shape. Derive the snapshot's successor from the cloned snapshot's
metadata via utils.NextVersion instead of assuming snapshotVersion+1; FlatKV
keeps the +1 rule because SetInitialVersion(N) seeds snapshot-<N-1> there.

Also from review:

- Reap abandoned clones. A SIGKILLed tool leaves its clone inside the data
  dir, where hardlinks pin snapshot inodes so pruning frees no disk and the
  deliberate non-"-tmp" name escapes memiavl's cleanup. Every clone now
  carries a flock-held owner marker (OFD/flock locks die with the process);
  clone creation sweeps siblings whose lock is acquirable, and marker-less
  directories once they are older than the mkdir-to-lock window can explain.
- Document that --height 0 against a live node is best-effort: the clone is
  a consistent committed prefix as of the copy instant, and a detected
  torn-tail repair now prints a warning telling the operator to trust the
  report's version line. Cross-node comparisons should use an explicit
  common height.
- Drop the MkdirAll that could never do useful work (snapshot selection
  already reads dbDir and fails first when it is missing).
- Rename SeekSnapshotDir to SeekSnapshotName — it returns a name relative to
  root, not a path — and pin the exported contract with a unit test, along
  with ReadMetadata which the successor derivation needs.

Co-authored-by: Cursor <[email protected]>

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Solid, well-motivated fix: replay mode now operates on a hardlinked/byte-copied private clone instead of handing a live changelog to memiavl's read-write WAL open, and the shared toolClone + flock ownership marker is a clean generalization of the existing FlatKV path. I verified the key assumptions hold (memiavl publishes snapshots via -tmp+rename and drops them via atomicRemoveDir, traverseSnapshots/removeTmpDirs ignore the .seidb-*-tool-* prefix, tryTruncateWAL truncates to the earliest snapshot so the coverage check really is a mid-clone-race detector, NextVersion + initEmptyDB make the SetInitialVersion case real, and FlatKV's LoadVersion does reject a version shortfall). No blockers; only polish and test-coverage notes.

Findings: 0 blocking | 5 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Test coverage gap on the memiavl side: the FlatKV suite pins the two properties that make the clone affordable (TestPrepareFlatKVToolingCloneHardlinksSnapshotAndCopiesChangelog, TestPrepareFlatKVToolingClonePlacesTempDirInsideDBDir), but memiavl_open_test.go has no equivalent. Nothing currently fails if tryPrepareMemIAVLToolingClone regressed to byte-copying a multi-GB snapshot or to creating the clone outside dbDir — both would still pass every new test. A cheap assertion on os.SameFile(src, dst) for one snapshot file plus a filepath.Rel(dbDir, clone.dir) check would cover it.
  • Leaked-clone recovery is only triggered by a later invocation of the same mode on the same dir (newToolClone sweeps just its own prefix, which TestSweepStaleToolClones deliberately pins). Since a leaked clone's hardlinks pin snapshot inodes and silently stop the live node's pruning from reclaiming disk, consider printing the clone path to stderr at startup so an operator who SIGKILLs the tool can find and remove it without waiting for the next run.
  • Cursor's second-opinion pass produced no output (cursor-review.md is empty), so that perspective is missing from this synthesis. Codex reported no material issues, which matches my own read.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

}
firstNeeded := utils.NextVersion(snapshotVersion, uint32(metadata.InitialVersion))

srcChangelogDir := filepath.Join(dbDir, "changelog")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Nit: this hardcodes the memiavl changelog layout, while memiavl itself resolves it through utils.GetChangelogPath(opts.Dir) (sei-db/state_db/sc/memiavl/db.go:206). The file already imports sei-db/common/utils for NextVersion, so utils.GetChangelogPath(dbDir) here (and for dstChangelogDir below) keeps the tool inheriting the layout rather than restating it — the same rationale the PR gives for exporting SeekSnapshotName instead of reimplementing the snapshot layout.

// publish snapshots by rename and drop them wholesale, so the same
// hardlink-the-snapshot / byte-copy-the-changelog split applies.
func tryPrepareMemIAVLToolingClone(dbDir string, height int64) (*toolClone, error) {
snapshotName, snapshotVersion, err := memiavl.SeekSnapshotName(dbDir, height)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] For height == 0, SeekSnapshotName bottoms out in os.Readlink(current), so a missing or uninitialized dbDir returns an os.ErrNotExist-wrapped error. isCloneRetryableError treats that as retryable, so the operator gets three attempts and then clone aborted after 3 retries, source kept churning instead of the actual "no such file or directory". Same shape as the pre-existing FlatKV path, so not new, but it's a misleading diagnosis for the most common operator typo. Worth stat-ing dbDir/current once up front and failing fast.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No new issues found in this pass. I revisited my earlier inline comment flagging that the WAL-coverage check might mis-handle a memiavl DB bootstrapped via SetInitialVersion(N) — that concern is already resolved in the current code: tryPrepareMemIAVLToolingClone derives the WAL successor from the cloned snapshot's InitialVersion metadata (via the newly exported memiavl.ReadMetadata) instead of assuming snapshotVersion+1, and TestOpenMemiAVLReplayAfterSetInitialVersion exercises and passes that exact scenario. Given this still touches memiavl's core snapshot/WAL read path and introduces new clone-lifecycle and locking logic, I think it is worth a human pass before merging.

Extended reasoning...

Overview

This PR replaces the tooling's use of memiavl.Options{ReadOnly: true} (which still opens the changelog WAL read-write and can truncate a live node's torn tail, destroying committed versions) with a clone-based approach mirroring the existing FlatKV tooling pattern: hardlink the snapshot, byte-copy the changelog, verify WAL coverage, then operate on the private clone. It adds a shared toolClone abstraction (owner flock + stale-clone sweep) used by both the FlatKV and new memiavl clone paths, and exports memiavl.SeekSnapshotName/memiavl.ReadMetadata so the tool inherits the package's snapshot-layout rules instead of re-implementing them. Production code paths (GetLatestVersion, LoadMultiTree) are only touched by the readMetadata->`ReadMetadata" export/rename, not behaviorally changed.

Security risks

None of consequence. This is operator-invoked CLI tooling (seidb evm-logical-digest), not exposed to untrusted input. The main risk class is data-integrity (a tool corrupting a live node's changelog), which is exactly what this PR fixes; it does not introduce a new one that I could find. The new clone directories inherit the source directory's permissions and live alongside the node's own data.

Level of scrutiny

Medium-high. While scoped to tooling, the change touches memiavl's snapshot/WAL layout assumptions directly (exports two previously-private helpers) and introduces new concurrency-sensitive logic (owner-lock based stale-clone sweeping, retry-on-churn cloning) that a live node's writer can race with. The regression tests are strong — they reproduce the exact torn-tail-truncation bug this PR fixes and assert the source directory is left untouched — and I ran the full operations + memiavl test suites locally; both new memiavl_open_test.go and tool_clone_test.go pass, along with the SetInitialVersion regression test.

Other factors

Two automated reviewers (chatgpt-codex-connector and seidroid) raised concerns in earlier PR comments — non-default initial-height WAL coverage, abandoned-clone cleanup, and a moot MkdirAll — that map exactly to the ReadMetadata-derived firstNeeded, sweepStaleToolClones, and warnIfCloneRepaired mechanisms already present in the current diff, so those appear to have been addressed already (possibly in the "address review findings" commit that was part of the PR from the start). My own prior inline comment repeated the same now-resolved concern; I want to flag here that it no longer applies against the current code so it doesn't mislead a future reviewer. Two local test failures (TestWriterLoopErrors, TestSnapshotWriterErrorHandling) are pre-existing and unrelated — they fail because the sandbox runs as root, which lets writes to '/invalid/path' succeed where the test expects a permission error.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant