Skip to content

feat(seidb): Add JSON output to evm-logical-digest and inspect a FlatKV migration in flight - #4166

Open
blindchaser wants to merge 4 commits into
mainfrom
yiren/seidb-digest-json-mode
Open

blindchaser wants to merge 4 commits into
mainfrom
yiren/seidb-digest-json-mode

Conversation

@blindchaser

Copy link
Copy Markdown
Contributor

Summary

Adds a --json flag to seidb evm-logical-digest, and extends inspect mode to
work during a FlatKV migration.

Getting JSON out required splitting rendering from accumulating: both forms now
render from one report struct per run (evmDigestJSON / evmInspectJSON), so
no number is computed twice and the prose and the JSON cannot drift. A
package-level digestSink holds the two destinations, so one assignment
redirects every line the scan helpers emit.

  • sei-db/tools/cmd/seidb/operations/evm_logical_digest.go:
    • --json emits the report as one JSON line on stdout and moves the narration
      to stderr. Storage-layer logging is raised to error level, because seilog
      writes to stdout and fixes its destination at process start. A warning fires
      when SEI_LOG_OUTPUT still points at stdout; it goes to the narration,
      never into the report.
    • marker_adjustments names the migration marker rows XORed out of the misc
      bucket. That list is the only thing distinguishing a mid-migration reading
      from a completed one, since the misc digest is adjusted in both.
    • A new zero-value census counts the populations FlatKV normalization can
      change. It is one nullable field shared by the row-level and account-level
      counters, so a half-counted census is unreachable. nil reads as "not
      measured" in both forms, not as all-zero.
    • semanticAccountDigestState records whether a code-hash row was present, so
      a stored all-zero row is distinguished from an absent one.
    • Inspect mode accepts --backend composite (FlatKV rows plus memiavl rows
      past the migration boundary) and --memiavl-open-mode=replay, reusing the
      existing no-repair read-only open. Translator normalization and --details
      storage inspect still require snapshot mode and now reject replay with their
      own messages.
    • The composite and semantic scans take a consumer, an optional account-key
      filter, and an optional progress callback, so digest and inspect share one
      scan. The filter drops out-of-shard account fragments before buffering.
    • The composite digest takes no census: its accounts are partly rebuilt from
      FlatKV rows, which cannot observe whether memiavl held a code-hash row.

Test plan

sei-db/tools/cmd/seidb/operations/evm_logical_digest_test.go

  • Output-form agreement: every bucket count and digest, the final digest, and
    the run context match between prose and JSON, for both digest and inspect
    reports. JSON output is one line and carries no prose.
  • Census: all six counters over a crafted leaf set; all-or-nothing across both
    counter levels (table over nil and non-nil); an untaken census omitted from
    both forms.
  • Marker adjustments: empty for a clean digest, named for one that consumed a
    boundary row, misc bucket equal in both, and empty encodes as [] not null.
  • JSON-mode warning: present when SEI_LOG_OUTPUT is unset, absent when it is
    redirected, and never in the report buffer.
  • Composite and filtered inspect: the memiavl tail counts only unmigrated rows,
    matching a reference accumulator; an account prefix filter skips out-of-range
    addresses.

Existing tests take the new census argument and otherwise assert unchanged
behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T21:57:51.822965Z b46b058 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cursor

cursor Bot commented Sep 14, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Changes are confined to offline seidb tooling and output formatting; replay mode explicitly avoids changelog repair so observers cannot truncate a live node's WAL.

Overview
seidb evm-logical-digest gains --json, which writes a single JSON object to stdout (digest or inspect) while progress and banners go to stderr. seidb root failures also print to stderr so machine-readable stdout is not polluted. JSON mode raises storage logging to error and warns when SEI_LOG_OUTPUT still targets stdout.

Digest and inspect reports are built from shared evmDigestJSON / evmInspectJSON structs so text and JSON stay aligned. Digest JSON adds marker_adjustments (migration rows XORed out of misc) and an optional zero_census for memiavl semantic paths (zero accounts, code-hash rows, storage slots, etc.), with codeHashRow tracking so absent vs all-zero code-hash rows differ.

Inspect now supports --backend composite (FlatKV + unmigrated memiavl tail) and --memiavl-open-mode=replay for semantic inspect; translator and storage --details inspect still require snapshot mode. Composite/semantic scans share consumer, account prefix filters, and progress hooks for digest vs inspect.

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

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 15, 2026, 8:28 AM

@blindchaser blindchaser changed the title feat(seidb): locate EVM digest mismatches during migration feat(seidb): Add JSON output to evm-logical-digest and inspect a FlatKV migration in flight Sep 14, 2026

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b46b058. Configure here.

}
account := getSemanticAccount(accounts, keyBytes)
copy(account.codeHash[:], rawVal)
account.codeHashRow = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Account filter misses balance fragments

Medium Severity

consumeSemanticMemiavlLeafFiltered applies allowAccountKey to nonce and code-hash rows but not to EVMKeyBalance. A prefix inspect of the account bucket still inserts every funded out-of-shard address into the accounts map, so the new early filter does not actually drop those fragments before buffering.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b46b058. Configure here.

@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: b46b058e69

ℹ️ 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".

fmt.Printf("key=%X logical=%X\n", physKey, logical)
entry.Meta = meta
}
a.entries = append(a.entries, entry)

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 Avoid retaining entries while streaming text output

With --list --list-limit<=0 in the existing text mode, every matching key is now converted to hex and retained in a.entries even though the entry is immediately printed and the slice is never used. Large account or storage listings therefore change from constant-memory streaming to memory proportional to the entire result and can OOM before the scan completes; only append to entries when producing the JSON object.

Useful? React with 👍 / 👎.

if acc.inspectBucket == flatkvBucketAccount {
accounts = make(map[string]*semanticAccountDigestState)
}
if err := consumeCompositeFlatKV(source.opened, acc.addLogical, accounts, acc.matchesAccountPhysicalKey, nil); 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 Exclude migration markers from composite misc shards

When --backend composite --inspect-bucket misc is run during or after migration, this FlatKV scan feeds migration/migration-boundary or migration/migration-version into the inspect accumulator. The global digest explicitly XORs those FlatKV-only rows out in miscForCompare, so identical logical state can have matching global misc digests but mismatching inspect shards, preventing the new inspection flow from locating the real discrepancy.

Useful? React with 👍 / 👎.

Comment on lines +1972 to +1973
if allowAccountKey != nil && !allowAccountKey(ktype.EVMPhysicalKey(keys.EVMKeyNonce, keyBytes)) {
return 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 Filter balance fragments before buffering accounts

For semantic account inspection with a narrow --key-prefix, the new filter is applied to nonce and code-hash fragments but not to the balance case below, which still calls getSemanticAccount for every address. On a large replay containing many out-of-prefix balance rows, the command therefore buffers nearly the full account population and can exhaust memory despite sharding; apply the account-key filter once before buffering any account fragment rather than repeating it selectively.

AGENTS.md reference: AGENTS.md:L115-L119

Useful? React with 👍 / 👎.

@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 refactor: rendering is split from accumulation so the prose and JSON forms are provably rendered from one report struct, the isZeroAccount rewrite is behavior-preserving, and the composite-source extraction closes handles on every early return. No blockers — the findings are a text-mode memory regression on unlimited --list, an account filter that balance rows bypass, an omitempty ambiguity in the new JSON contract, and a CHANGELOG entry pointing at the wrong PR.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] The package doc block (evm_logical_digest.go:112-164) still documents only the pre-PR surface: there is no usage example for --json, nor for --backend composite --inspect-bucket / --memiavl-open-mode=replay in inspect mode. Since composite inspect is the headline capability in the PR title ("locate EVM digest mismatches during migration"), the one place an operator reads to learn the tool now omits it. A # Shard the account bucket across a drain in flight: example alongside the existing inspect examples would close it.
  • [suggestion] --find-hash output never reaches the JSON report. addLogical emits FOUND-HASH through digestOut.sayf (evm_logical_digest.go:330), which is stderr narration under --json. --find-hash is the tool's primitive for pinpointing the single diverging row, so a scheduled caller that reads only stdout — exactly the caller --json exists for — gets the digest but not the located entry. Consider collecting matches into a found_hash_entries field on evmDigestJSON in addition to narrating them.
  • [suggestion] inspectCompositeMigrateEVM passes nil for both progress callbacks (evm_logical_digest.go:1252 and 1262), so a composite inspect over mainnet-sized FlatKV plus the memiavl tail emits no progress at all until it finishes, while digestCompositeMigrateEVM over the same two scans narrates every 20M rows. These are multi-hour scans; reusing the digest path's callbacks (or a shared one) would keep the two modes symmetric.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

fmt.Printf("key=%X logical=%X\n", physKey, logical)
entry.Meta = meta
}
a.entries = append(a.entries, entry)

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] a.entries is appended unconditionally, including in text mode where the very next lines stream the same pair to digestOut. Before this PR, --list was pure streaming with constant memory; now every listed entry is retained as two freshly allocated hex strings (%X of the physical key and of the logical value) for the lifetime of the run.

With the default --list-limit 1000 this is negligible, but --list-limit 0 means unlimited, and that is the documented way to dump a full bucket. A storage-bucket dump on mainnet then holds a evmInspectEntryJSON per matching row — ~200 bytes of hex per 32-byte key/value pair — with nothing bounding it.

Guard the append on the same condition that already guards the prose:

if digestOut.jsonReport != nil {
	a.entries = append(a.entries, entry)
} else if meta != "" {
	digestOut.sayf("key=%X logical=%X %s\n", physKey, logical, meta)
} else {
	digestOut.sayf("key=%X logical=%X\n", physKey, logical)
}

account := getSemanticAccount(accounts, keyBytes)
copy(account.codeHash[:], rawVal)
account.codeHashRow = true
case keys.EVMKeyBalance:

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 EVMKeyBalance case does not consult allowAccountKey, unlike the EVMKeyNonce (line 1957) and EVMKeyCodeHash (line 1972) cases just above. Every balance row therefore calls getSemanticAccount and allocates a semanticAccountDigestState in accounts, for every address in the unmigrated memiavl tail, regardless of the prefix filter.

The emitted result is still correct — finalizeSemanticAccounts routes each account through consume, and consumeLogical re-applies the same prefix test — so this is not a wrong-output bug. But the PR describes the filter as dropping "out-of-shard account fragments before buffering", and since essentially every live account carries a balance row, the filter buys almost nothing: accounts still grows to the full address space. That defeats the point of sharding an account inspect on a node too large to scan in one pass.

Applying the same two-line guard here would fix it. TestInspectAccountPrefixFilterSkipsOutOfRangeMemiavlAccounts only feeds nonce rows, so it passes either way — extending it with a balance row for the skipped address (and asserting len(accounts) == 1) would pin the buffering claim rather than just the match count.

ShardNextBytes int `json:"shard_next_bytes"`
Matched uint64 `json:"matched"`
List bool `json:"list"`
Listed int `json:"listed,omitempty"`

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] omitempty on Listed and ListLimit drops both fields when they are 0, and 0 is meaningful for each: listed: 0 means "listed nothing matched" and list_limit: 0 means "unlimited" (per the flag help, <=0 means unlimited). A consumer reading report.listed gets undefined precisely in the zero-match case, which is the interesting outcome when hunting a mismatch, and cannot tell an unlimited run from a field that was never applicable.

This also cuts against the contract the PR pins elsewhere: TestDigestJSONNamesTheMarkerAdjustmentsBehindTheMiscBucket asserts marker_adjustments encodes as [] rather than null specifically so callers need no presence check. Dropping omitempty from these two ints (they are already gated by the list boolean in the same object) would make the inspect report consistent with that.

Comment thread CHANGELOG.md Outdated
## Unreleased

### Improvements
* [#4156](https://github.com/sei-protocol/sei-chain/pull/4156) feat(seidb): `evm-logical-digest` can emit both digest and inspect reports as one JSON object on stdout, while progress and warnings go to stderr. Inspect mode now supports the mid-migration composite EVM view and semantic memiavl replay, so operators can shard and locate mismatched EVM keys during a FlatKV drain. Digest replay opens memiavl read-only without changelog repair, so an observer cannot truncate a live node's 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] Two things to fix in this entry:

  1. The link points at PR Add JSON output to evm-logical-digest and let inspect read a drain in flight #4156, which is closed; this is PR feat(seidb): Add JSON output to evm-logical-digest and inspect a FlatKV migration in flight #4166. Both the number and the URL need updating.
  2. "Digest replay opens memiavl read-only without changelog repair, so an observer cannot truncate a live node's changelog" describes behavior that already exists on the base branch — openMemiAVLReplayReadOnly and its no-repair doc comment are unchanged by this diff, and digestMemIAVLReplay already routed through it. The new part is that inspect mode can now reach that same open path. Rewording to credit only what this PR adds keeps the entry accurate for anyone bisecting the behavior later.

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 40.86957% with 204 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.54%. Comparing base (83fcf3f) to head (b64ef6b).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...b/tools/cmd/seidb/operations/evm_logical_digest.go 40.98% 203 Missing ⚠️
sei-db/tools/cmd/seidb/main.go 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4166      +/-   ##
==========================================
- Coverage   66.66%   65.54%   -1.12%     
==========================================
  Files        2200     2080     -120     
  Lines      169431   157734   -11697     
==========================================
- Hits       112954   103394    -9560     
+ Misses      56336    54199    -2137     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 35.76% <40.86%> (?)
sei-db 74.50% <ø> (ø)
sei-db-state-db ?

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

Files with missing lines Coverage Δ
sei-db/tools/cmd/seidb/main.go 0.00% <0.00%> (ø)
...b/tools/cmd/seidb/operations/evm_logical_digest.go 36.90% <40.98%> (+10.76%) ⬆️

... and 120 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.

blindchaser and others added 2 commits September 15, 2026 09:38
The --json report is a machine-readable stdout channel, but a refused run put a
bare error line there as well: cobra reports to stderr, then main duplicated it
onto stdout. A scheduled caller piping stdout to a parser saw a parse error, and
the tool's own SEI_LOG_OUTPUT warning named the wrong cause, since setting that
variable does not move this line.

main.Execute is the one point every subcommand returns through, so the stream is
corrected there rather than in the command that happens to have --json today.
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