Skip to content

feature(sei-db) LTHash refactory for adding per module hash and stats#3758

Merged
yzang2019 merged 20 commits into
mainfrom
yzang/per-module-refactor
Jul 16, 2026
Merged

feature(sei-db) LTHash refactory for adding per module hash and stats#3758
yzang2019 merged 20 commits into
mainfrom
yzang/per-module-refactor

Conversation

@yzang2019

@yzang2019 yzang2019 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Refactors FlatKV's lattice-hash (LtHash) subsystem and extends per-DB metadata to track per-module hashes and storage stats. Also renames the Legacy* catch-all DB to Misc* for clarity. FlatKV is not yet in production, so no on-disk migration/backward-compat is provided (fresh stores only).

Changes

1. LegacyDBMiscDB rename

The catch-all DB (everything that isn't account/code/storage — i.e. all non-EVM cosmos modules, plus some EVM misc keys) is renamed throughout:

  • On-disk directory legacy/misc/
  • vtype.LegacyDatavtype.MiscData, keys.EVMKeyLegacykeys.EVMKeyMisc
  • All identifiers, comments, tooling, and integration-test scripts updated.

2. Per-module LtHash metadata

Each data DB now decomposes its per-DB root LtHash into additive per-module hashes, persisted under _meta/x:<module>/hash in that DB's LocalMeta:

  • account/code/storage DBs only ever carry the evm module.
  • misc DB may carry several (evm plus cosmos modules such as gov, bank).
  • The per-DB root is the homomorphic sum of its module hashes; the global store hash (and consensus AppHash) is unchanged — per-module hashes are consensus-irrelevant bookkeeping.
  • Loaded on startup, maintained on live commit, and populated by the state-sync importer + persisted in FinalizeImport.

3. LtHash computation refactor + threading model

Extracted the whole per-block LtHash pipeline into a new lthash.HashCalculator:

  • Owns a single fixed worker pool (threading.FixedPool) shared by both the live-commit path and the state-sync importer.
  • Work is split into independent, self-terminating per-chunk tasks, so it's safe to call concurrently from multiple goroutines (the importer runs one per DB) with no deadlock/oversubscription risk.
  • Reads previous values through an injected OldValueReader interface and extracts module names via an injected ModuleFunc, keeping lthash decoupled from pebbledb/ktype.
  • ApplyChangeSets is now a thin orchestrator (classify → read-old → apply EVM value semantics → compute); EVM/cosmos domain semantics stay in flatkv.

4. Per-module storage stats

Alongside each module's hash, we now accumulate key count and total bytes (physical key + serialized value = logical footprint), persisted under _meta/x:<module>/stats:

  • Add: +1 key, +key+value bytes. Update: count unchanged, byte delta only. Delete: −1 key, −key+old-value bytes. Delete-of-absent: no-op.
  • Counting predicates are gated on exactly the same conditions the hash uses to mix/unmix, so stats always track the set the hash represents.
  • Crash-recoverable (loaded on init), maintained on live commit, and produced identically by the importer.
  • Persisted per-(DB, module) only; per-DB/global totals are derivable on demand.

5. Config

  • Added LtHashThreadsPerCore (float) to size the dedicated LtHash pool: workers = int(LtHashThreadsPerCore * NumCPU), clamped to ≥ 1. Default 1.0 (LtHash is CPU-bound). Mirrors the existing reader/misc pool config convention.

6. Tests

  • lthash: ModuleStats marshal round-trip / bad-length, foldChunk add/update/delete accounting, pooled aggregation path.
  • ktype: _meta/x:<module>/hash and /stats key round-trips, mutual non-aliasing, malformed-key rejection.
  • flatkv: per-module hash & stats "incremental equals full scan" invariants, add→update→delete transitions, persistence-after-reopen, and state-sync-import-survives-restart.

* main:
  fix(evm): bound store-cache depth amplification from stacked EVM snapshots (#3713)
  Fix address distro bug (#3716)
  fix(giga): route EVM validation failures to v2 fallback (CON-368) (#3753)
  feat(seidb): composite + replay digest modes and migration-boundary omission for evm-logical-digest (#3711)
  feat(scripts): add SC read-path RPC probe (consistency check + load) (#3751)
  fix(feegrant): bound DenomsSubsetOf cost and cap allowance denoms (#3710)

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

⚠️ Code review skipped — your organization has reached its monthly code review spending cap.

An organization admin can view or raise the cap at claude.ai/admin-settings/claude-code. The cap resets at the start of the next billing period.

Once the cap resets or is raised, push a new commit or reopen this pull request to trigger a review.

@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large changes to FlatKV commit, import, and LtHash persistence on consensus-adjacent state; incorrect hashing or metadata handling could cause silent drift or unopenable stores, though load-time sum checks and empty-module guards mitigate some failure modes.

Overview
Renames FlatKV’s catch-all legacy bucket to misc end-to-end (on-disk misc/, EVMKeyMisc, MiscData, dump/digest integration scripts, and comments).

LtHash is refactored around a new lthash.HashCalculator: a dedicated worker pool (LtHashThreadsPerCore) drives chunked, parallel per-module deltas for both live ApplyChangeSets and state-sync import. Each data DB now keeps per-module hashes (_meta/x:<module>/hash) and stats (_meta/x:<module>/stats); per-DB roots remain the homomorphic sum of modules, with load-time checks that reject stores missing or inconsistent per-module metadata.

ApplyChangeSets delegates hashing to the calculator after prepareWrites; old values for unmixing use raw serialized bytes from ReadOldValues. Importers use the same per-module path instead of a single serial ComputeLtHash. Empty module names are rejected on live commit and import routing so bad physical keys cannot brick reopen.

Extensive new tests cover per-module hash/stats invariants, persistence, import/restart, and metadata validation.

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

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJul 16, 2026, 7:22 PM

Comment thread sei-db/state_db/sc/flatkv/lthash/hash_calculator.go
Comment thread sei-db/state_db/sc/flatkv/snapshot.go
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.48148% with 150 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.60%. Comparing base (de3ea92) to head (e7c7e33).

Files with missing lines Patch % Lines
sei-db/state_db/sc/flatkv/verify.go 52.68% 32 Missing and 12 partials ⚠️
sei-db/state_db/sc/flatkv/store_meta.go 77.66% 11 Missing and 12 partials ⚠️
...b/tools/cmd/seidb/operations/evm_logical_digest.go 30.30% 23 Missing ⚠️
...ei-db/state_db/sc/flatkv/lthash/hash_calculator.go 89.44% 11 Missing and 8 partials ⚠️
sei-db/state_db/sc/flatkv/store_write.go 85.50% 5 Missing and 5 partials ⚠️
sei-db/state_db/sc/flatkv/config/config.go 33.33% 3 Missing and 3 partials ⚠️
sei-db/state_db/sc/flatkv/store.go 89.09% 4 Missing and 2 partials ⚠️
sei-db/state_db/sc/flatkv/store_apply.go 94.05% 5 Missing and 1 partial ⚠️
sei-db/common/keys/evm.go 60.00% 2 Missing ⚠️
sei-db/state_db/sc/flatkv/importer.go 92.85% 1 Missing and 1 partial ⚠️
... and 7 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3758      +/-   ##
==========================================
- Coverage   59.94%   59.60%   -0.35%     
==========================================
  Files        2288     2250      -38     
  Lines      190180   186269    -3911     
==========================================
- Hits       114000   111022    -2978     
+ Misses      66025    65376     -649     
+ Partials    10155     9871     -284     
Flag Coverage Δ
sei-chain-pr 38.97% <34.88%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 77.91% <84.09%> (?)

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

Files with missing lines Coverage Δ
...db/state_db/sc/flatkv/config/flatkv_test_config.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/ktype/ktype.go 87.23% <ø> (ø)
sei-db/state_db/sc/flatkv/ktype/meta.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/lthash/stats.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/snapshot.go 69.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/store_catchup.go 65.41% <100.00%> (ø)
sei-db/state_db/sc/flatkv/vtype/misc_data.go 100.00% <100.00%> (ø)
sei-db/state_db/ss/evm/types.go 100.00% <100.00%> (ø)
...cmd/seidb/operations/import_flatkv_from_memiavl.go 41.74% <ø> (ø)
sei-db/tools/cmd/seidb/operations/state_size.go 0.00% <ø> (ø)
... and 17 more

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

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

A large but mostly mechanical Legacy→Misc rename plus a well-tested feature: per-module LtHash decomposition and per-module stats, refactored into a new shared-pool HashCalculator. The new code is sound for fresh stores (homomorphic invariants preserved, pool nesting deadlock-free); the only notable concerns are two upgrade-path correctness risks that the author has explicitly declared out of scope (FlatKV is pre-production, fresh stores only).

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Upgrade risk — per-module hash bootstrap (raised as P0 by Codex): on load, an existing store that has a per-DB root LtHash but no persisted per-module hashes gets an empty perDBModuleWorkingLtHash. On the first post-upgrade write, Compute()/importer flush replace the per-DB root with SumModuleHashes(newPerModule[dir]), which only reflects the current block's delta and silently drops all previously-committed entries, changing the consensus AppHash. This is correct for fresh stores and the PR explicitly states fresh-only/no-migration, but there is no on-load guard that enforces this — an old-format store would diverge silently. Consider refusing to load (or logging loudly) when a data DB has a non-empty per-DB root but no per-module hashes.
  • Upgrade risk — directory rename (raised as P0 by Codex): renaming the on-disk legacy/ DB dir to misc/ has no migration or fallback, so an upgraded node opens a fresh empty misc/ DB and abandons existing non-EVM/misc state. Again consistent with the stated fresh-only scope, but there is no detection of a pre-existing legacy/ directory to fail fast. Same recommendation: add a guard or a one-time rename-on-open.
  • VerifyLtHash (verifyLtHashInternal) still only recomputes and checks the per-DB roots; it does not verify the new per-module hash decomposition against a full scan. The per-module hashes are consensus-irrelevant bookkeeping and the test suite covers the invariant, but a production self-check that the per-module hashes sum to the per-DB root would catch drift in the new metadata.
  • Cursor's second-opinion review file (cursor-review.md) is empty — that pass produced no output. REVIEW_GUIDELINES.md is also empty, so the review proceeded without repo-specific guidelines.
  • Minor: in HashCalculator.Compute, the defensive branch that handles a delta whose Dir is not in c.dbDirs sets newPerDB[dir] via the touched loop but the global-sum loop only iterates c.dbDirs, so such a dir's hash would be excluded from the global hash. Currently unreachable (dirs always come from dataDBDirs), but the defensive code is subtly inconsistent.

seidroid[bot]
seidroid Bot previously requested changes Jul 15, 2026

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

A clean, well-tested refactor that renames Legacy→Misc and adds per-module LtHash/stats bookkeeping without changing the consensus AppHash for fresh stores. However, the load path silently accepts a pre-PR on-disk store (per-DB root present, no per-module metadata) and then corrupts its per-DB root / global hash on the first write, so a loud-failure guard is needed before merge.

Findings: 2 blocking | 3 non-blocking | 1 posted inline

Blockers

  • Silent AppHash corruption for pre-PR on-disk stores (Codex P1, confirmed). loadGlobalMetadata (store.go:755) accepts a store whose per-DB root (meta.LtHash) is non-identity but whose ModuleLtHashes map is empty, and HashCalculator.Compute (hash_calculator.go:205-206) then rebuilds each touched DB's root as SumModuleHashes(...) of the module map, which for such a store contains only this block's delta — discarding all pre-existing keys and producing a wrong per-DB root and global hash with no error. The PR deliberately excludes migration ("fresh stores only"), but nothing enforces that; the fix is to fail loudly on load when a per-DB root is present without per-module metadata (or reconstruct the module map via a full scan), rather than silently corrupting the consensus hash.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • The on-disk directory rename legacy/misc/ (store.go, snapshot.go) means any existing store's populated legacy/ dir is ignored and a fresh empty misc/ is created. This is part of the same no-migration story as the blocker above; a loud check on startup would cover both cases.
  • Cursor's second-opinion review (cursor-review.md) and REVIEW_GUIDELINES.md were both empty, so no additional signal was available from those sources.
  • Minor: Compute clones the per-module hash/stats maps for all four data DBs on every ApplyChangeSets call (potentially several per block), even for untouched DBs. Maps are small so impact is limited, but the clone-all-then-overwrite-touched pattern is slightly wasteful on the hot path.

} else {
s.perDBWorkingLtHash[dbDir] = lthash.New()
}
if meta != 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.

[blocker] Silent AppHash corruption for pre-PR on-disk stores (confirmed). When an existing store has a per-DB root (meta.LtHash != nil, set just above) but no per-module metadata, meta.ModuleLtHashes loads as an empty map here. On the first write, HashCalculator.Compute recomputes each touched DB's root via SumModuleHashes(newPerModule[dir]) (hash_calculator.go:205-206), which contains only this block's delta — the pre-existing keys' contribution to the per-DB root (and thus the global store hash / AppHash) is silently discarded. The PR states migration is intentionally unsupported (fresh stores only), but this path doesn't enforce that; it corrupts instead of failing. Please fail loudly when a per-DB root is present without per-module metadata (e.g. meta.LtHash is non-identity but meta.ModuleLtHashes is empty), or reconstruct the module decomposition from a full scan on load.

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

A large, well-tested refactor: a mechanical Legacy→Misc rename across FlatKV plus a new per-module LtHash/stats feature extracted into lthash.HashCalculator. The core logic (homomorphic decomposition preserving the global hash, deadlock-free shared worker pool, stats predicates matching the hash) is sound and heavily covered by tests; the only issues found are a gofmt violation that would fail CI and minor cosmetic/process notes.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Both second-opinion passes produced essentially no findings: Cursor's file was empty, and Codex reported no material issues but noted its focused tests could not run because the Go 1.25.6 toolchain download is network-blocked. I was likewise unable to execute the Go test suite in this environment, so the correctness assessment of the new lthash/stats logic is based on code reading plus the PR's (comprehensive) in-tree tests rather than a fresh test run.
  • Cosmetic only: several diagnostic printf/format strings in sei-db/tools/cmd/seidb/operations/evm_logical_digest.go were left with the old column widths after the rename (e.g. misc count=%d and FINAL_DIGEST account+code+storage+misc), so the tool's aligned stdout columns are now slightly misaligned ("misc" is shorter than "legacy"). No functional impact.
  • Optional: LtHashThreadsPerCore is added to flatkv config but (like the existing MiscPoolThreadsPerCore/ReaderThreadsPerCore) is not surfaced to any app.toml wiring, so it is only tunable via DefaultConfig(). This matches the existing convention and is not a regression, but if operators are expected to tune the LtHash pool it will need explicit config plumbing.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

"code": {lines: 1}, // 1 code
"storage": {lines: 3}, // 3 storage slots
"legacy": {lines: 1}, // 1 bank row
"misc": {lines: 1}, // 1 bank row

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] gofmt violation: after renaming "legacy": to "misc": the composite-literal values are no longer aligned — "misc": {lines: 1} has 2 spaces where gofmt requires 4 (to align with "account":/"storage":). golangci-lint's gofmt formatter lints test files too, so CI (.github/workflows/golangci.yml) will fail on this. Run gofmt -s -w on the file to fix.

@seidroid seidroid Bot dismissed their stale review July 15, 2026 07:17

Superseded: latest AI review found no blocking issues.

seidroid[bot]
seidroid Bot previously requested changes Jul 15, 2026

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

A large, well-tested refactor renaming Legacy→Misc and adding per-module LtHash/stats decomposition plus a shared HashCalculator worker pool; the core logic is sound, but a gofmt misalignment in a test file will fail the golangci-lint CI gate.

Findings: 2 blocking | 4 non-blocking | 1 posted inline

Blockers

  • sei-db/tools/cmd/seidb/operations/dump_flatkv_test.go is not gofmt-clean (the "misc": map entry is misaligned), so the golangci-lint v2.8.0 gofmt formatter in .github/workflows/golangci.yml will fail. Run gofmt -s -w / goimports -w on the touched files.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • REVIEW_GUIDELINES.md is empty and cursor-review.md produced no output; Codex reported no material issues. No repo-specific standards were available to apply beyond AGENTS.md.
  • On-disk directory rename legacy/ → misc/ has no migration path: an existing store's legacy/ dir would be silently ignored and a fresh misc/ created. This is acknowledged in the PR (FlatKV not yet in production) and is acceptable, but worth confirming no non-production store relies on it.
  • ss/evm/types.go keeps the exported constant name StoreLegacy while pointing it at commonevm.EVMKeyMisc; leaving the old public name is fine but slightly inconsistent with the rest of the rename — consider a follow-up if that symbol is meant to track the Misc naming.
  • loadModuleLtHashes and loadModuleStats each open a separate iterator over the identical _meta/x: range and filter by suffix; a single pass could populate both maps, though the current cost is negligible at startup.

Comment thread sei-db/tools/cmd/seidb/operations/dump_flatkv_test.go Outdated

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

A large, well-structured FlatKV refactor that renames the Legacy* catch-all DB to Misc*, extracts the lattice-hash pipeline into a shared HashCalculator with a fixed worker pool, and adds crash-recoverable per-module LtHash + key/byte stats decomposition. The bulk is mechanical rename churn plus new decomposition logic backed by strong "incremental == full-scan" / persistence / import-restart tests; no blocking issues found, with one hardening suggestion.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion coverage: cursor-review.md and REVIEW_GUIDELINES.md are both empty — the Cursor pass produced no output and there were no repo-specific guidelines to apply. Codex produced a single finding (incomplete per-module load validation), addressed inline.
  • The per-module root is recomputed from the module map only for DBs touched in a block (HashCalculator.ComputeSumModuleHashes(newPerModule[dir])), which makes the persisted per-DB root ↔ module-map consistency invariant load-bearing for consensus (a drifted map silently rebuilds a wrong root, hence a wrong global/AppHash on the next write). This is well guarded for the migration case and covered by VerifyLtHash, but that verifier is a full scan not run at startup — consider documenting/asserting the invariant more defensively (see inline note on store_meta.go).
  • LtHashThreadsPerCore is added to the internal flatkv config with a default of 1.0; confirm it is (or intentionally is not) surfaced in the operator-facing app config so the new pool can be tuned in production.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

if meta == nil || meta.LtHash == nil || meta.LtHash.IsZero() {
return nil
}
if len(meta.ModuleLtHashes) == 0 {

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] validatePerModuleMetadata only rejects the empty module map (the pre-per-module migration case). It does not verify that the persisted per-module hashes actually reconstruct the persisted per-DB root, i.e. SumModuleHashes(meta.ModuleLtHashes).Equal(meta.LtHash). If a subset of module /hash keys were lost while the root key survived, len(...) != 0 passes here, and the next write that touches this DB rebuilds its root as SumModuleHashes(perModule[dir]) over an incomplete map — silently dropping a module's contribution to the per-DB root and thus the global store hash / AppHash. This merges Codex's finding: because writeLocalMetaToBatch writes the root and all module hashes in one atomic Pebble batch, silent partial loss is not reachable under normal operation, so this is a hardening suggestion rather than a live bug. But the check is essentially free (SumModuleHashes already exists) and would also catch any drift bug in the new decomposition code at load time, guarding a consensus-critical invariant. Consider adding the sum-equality assertion (and optionally the same for ModuleStats) alongside the existing non-empty check.

@seidroid seidroid Bot dismissed their stale review July 15, 2026 07:44

Superseded: latest AI review found no blocking issues.

@cody-littley cody-littley self-requested a review July 15, 2026 13:23
@yzang2019 yzang2019 requested a review from blindchaser July 15, 2026 14:27

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

One big missing element is HashLogger integration. But I think it would be ok if we did the HashLogger as a follow up PR since it's not going to be a 5 line fix. If you'd prefer to do it later, we should probably file a ticket.

Comment thread sei-db/state_db/sc/flatkv/lthash/hash_calculator.go Outdated
Comment thread sei-db/state_db/sc/flatkv/lthash/hash_calculator.go Outdated
Comment thread sei-db/state_db/sc/flatkv/lthash/hash_calculator.go Outdated
Comment thread sei-db/state_db/sc/flatkv/store_iteration.go Outdated
Comment thread sei-db/state_db/sc/flatkv/store_iteration.go Outdated

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

Giving my "Approve" review now since none of my comments are blocking. Might want to give @blindchaser an opportunity to review if he wants, since I think he was the original author of the hashing logic.

Comment thread sei-db/state_db/sc/flatkv/store_meta.go
// TODO:In theory, we could offload lattice hash calculation to a work pool and get parallelism between DB operations and hash calculations. Cryptosim performance makes me think we could probably get a 2-3x speedup from this, assuming receiving data from the network isn't the bottleneck.
newHash, _ := lthash.ComputeLtHash(w.ltHash, w.ltPairs)
w.ltHash = newHash
// Per-module hashes are the primitive: distribute this batch's pairs across

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.

Not triggered by any current feeder, both the flatkv→flatkv export (RawGlobalIterator over committed Pebble rows) and the memiavl importer (translator merges accounts, injective per kind) emit each physical key once.

But the importer's LtHash accumulation is non-idempotent by construction (assumes empty target, only MixIn, never MixOut), so a duplicate key would double-mix the hash and double-count stats while Pebble keeps one row, and FinalizeImport persists it without VerifyLtHash. (not introduced by this PR, it's the existing behavior)

Suggest a cheap fail-loud dedup check in dbWorker.run() to guard the invariant against future feeders / malformed snapshots. (Pre-existing on main; the refactor widens it to per-module hashes + stats.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Under what scenario would we export duplicate keys to the snapshot? If we add a dedup logic, the concern is that we have to fit every key into memory during the import process, which could fill up memory overhead and cause OOM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If we hit a bad snapshot today which does contains duplicate keys, then the calculated lattice hash should detect the difference and error out the import at the end right?

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.

true

Comment thread sei-db/state_db/sc/flatkv/verify.go
@yzang2019 yzang2019 changed the title [Feature] LTHash refactory for adding per module hash and stats feature(seiddb) LTHash refactory for adding per module hash and stats Jul 15, 2026
@yzang2019 yzang2019 requested a review from blindchaser July 15, 2026 22:40
Comment thread sei-db/state_db/sc/flatkv/verify.go
s.phaseTimer.SetPhase("apply_change_compute_lt_hash")
res, err := s.ltCalc.Compute(pairSets, s.perDBWorkingLtHash, s.perDBModuleWorkingLtHash, s.perDBModuleWorkingStats)
if err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pending writes before LtHash compute

High Severity

ApplyChangeSets runs prepareWrites (which maps.Copys into the pending-write maps) before ltCalc.Compute. If Compute returns an error, the call exits without updating working LtHash, per-module metadata, or pendingChangeSets, but the pending maps already hold the new values. A later Commit can persist those rows while metadata still reflects the pre-failure hash, diverging AppHash from on-disk state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c0cad32. Configure here.

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

A large but carefully-executed refactor of the not-yet-production FlatKV subsystem: a complete Legacy→Misc rename plus a new per-module lattice-hash calculator and per-module stats. The consensus-relevant global hash is provably unchanged (homomorphic decomposition) and is backed by extensive invariant tests; no correctness, security, or performance defects found.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Second-opinion coverage: OpenAI Codex reported "No material issues found"; the Cursor review file (cursor-review.md) was empty, so that pass produced no output. My independent review aligns with Codex — no blockers.
  • Naming drift (nit): sei-db/state_db/ss/evm/types.go keeps the constant name StoreLegacy while repointing it to commonevm.EVMKeyMisc. Given the underlying store is now consistently called "misc" everywhere else, leaving the StoreLegacy alias may confuse future readers. Intentional/harmless, but worth a follow-up rename for consistency.
  • Config edge (nit): Config.Validate() only rejects LtHashThreadsPerCore < 0, so a value of 0 passes validation and is silently clamped to a single worker by lthashWorkerCount(). This is documented and reasonable, but a 0 value producing 1 worker (rather than an error or an explicit 'disabled' meaning) is slightly surprising.
  • Verified the rename is symbol-complete: grep found no remaining EVMKeyLegacy/LegacyData/legacyDB references in production code, and the seidb dump tool now emits a misc bucket file matching the updated integration-test digest scripts.

Comment on lines +186 to 210
func validatePerModuleMetadata(dbDir string, meta *ktype.LocalMeta) error {
if meta == nil || meta.LtHash == nil {
return nil
}
if len(meta.ModuleLtHashes) == 0 {
if meta.LtHash.IsZero() {
return nil
}
return fmt.Errorf(
"flatkv: %s carries a non-identity per-DB LtHash root but no "+
"per-module metadata; this store predates per-module hashing "+
"and cannot be opened (migration is intentionally unsupported — "+
"recreate the store from a snapshot or genesis)",
dbDir,
)
}
sum := lthash.SumModuleHashes(meta.ModuleLtHashes)
if !meta.LtHash.Equal(sum) {
return fmt.Errorf(
"flatkv: %s per-module hashes do not sum to per-DB root (root=%x sum=%x)",
dbDir, meta.LtHash.Checksum(), sum.Checksum(),
)
}
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.

🟡 validatePerModuleMetadata only guards one direction of the root/per-module invariant: when meta.LtHash == nil it returns success immediately without checking whether meta.ModuleLtHashes is non-empty/non-zero. If a DB's _meta/hash key is lost while its _meta/x:<module>/hash keys survive, the store loads with perDBWorkingLtHash reset to identity while the surviving non-zero per-module hashes are still loaded into perDBModuleWorkingLtHash — breaking the root==sum(modules) invariant silently. The next write touching that DB rebuilds the root via SumModuleHashes over the stale module map, corrupting the per-DB root and the global AppHash with no error raised. Fix: in the meta.LtHash == nil branch, also reject when lthash.SumModuleHashes(meta.ModuleLtHashes) is non-zero, mirroring the existing check for the opposite direction.

Extended reasoning...

validatePerModuleMetadata (sei-db/state_db/sc/flatkv/store_meta.go:186-210) is meant to enforce the load-time invariant that a persisted per-DB root LtHash is consistent with its per-module decomposition. It already guards one direction of this invariant, added during this same PR's review: a non-identity root with an empty/incomplete module map is rejected, and a non-empty module map that doesn't homomorphically sum to the root is rejected. However, the very first branch — if meta == nil || meta.LtHash == nil { return nil } — returns success immediately whenever the per-DB root is nil, without ever inspecting meta.ModuleLtHashes in that branch.

loadLocalMeta (store_meta.go:46-56) sets meta.LtHash only when the _meta/hash key is found on disk, while loadModuleLtHashes (lines 58-62) independently populates meta.ModuleLtHashes from whatever _meta/x:<module>/hash keys survive. These are two separate reads of two disjoint key ranges. So a partial loss of _meta/* keys — where _meta/hash is gone but one or more _meta/x:<module>/hash keys survive with non-zero values — produces exactly meta.LtHash == nil with a non-empty, non-zero ModuleLtHashes map. The current code passes this straight through with no error.

The consequence shows up in loadGlobalMetadata (store.go, ~lines 758-774): when meta.LtHash == nil it resets s.perDBWorkingLtHash[dbDir] to lthash.New() (the identity hash), but a separate if meta != nil block unconditionally still loads s.perDBModuleWorkingLtHash[dbDir] = cloneModuleHashes(meta.ModuleLtHashes) — the surviving non-zero per-module hashes. Immediately after startup, the invariant root == sum(modules) is violated in memory (root=identity, modules=non-zero), with no error raised anywhere in the load path.

Step-by-step proof of impact:

  1. A store's storageDB commits normally, giving it a non-identity per-DB root and a non-zero _meta/x:evm/hash entry (both written atomically by writeLocalMetaToBatch).
  2. Simulate corruption: delete only the _meta/hash key on disk (e.g. bit rot, a botched manual edit, or a partial write outside FlatKV's normal atomic-batch path), leaving _meta/x:evm/hash intact.
  3. On reopen, loadLocalMeta sets meta.LtHash = nil (key not found) but meta.ModuleLtHashes = {"evm": <non-zero>} (key found).
  4. validatePerModuleMetadata hits the first branch (meta.LtHash == nil) and returns nil — no error — without looking at ModuleLtHashes.
  5. loadGlobalMetadata sets perDBWorkingLtHash[storageDBDir] = lthash.New() (identity) but perDBModuleWorkingLtHash[storageDBDir] = {"evm": <non-zero>}. The invariant is now broken in memory, silently.
  6. The next ApplyChangeSets/Compute that touches storageDB recomputes its root as SumModuleHashes(perModule[storageDBDir]), which now incorporates the stale/leftover module hash that has nothing to do with the DB's actual current contents relative to the (lost) prior root — silently corrupting the per-DB root and thus the global evm_lattice store hash / consensus AppHash going forward. If that DB dir happens to never be touched again, perDBWorkingLtHash simply stays at identity forever, permanently dropping that DB's contribution from the global hash — with no error, until someone happens to run the offline VerifyLtHash tool, which is not run automatically on startup.

This is precisely the same corruption threat model (partial loss of _meta/* keys) that motivated the sibling checks added to this same function during PR review (the seidroid/cursor/blindchaser comments on this exact function) — just applied in the mirror direction (root lost vs. modules lost). writeLocalMetaToBatch writes the root and every module hash/stats key in the same atomic Pebble batch, so this genuinely cannot happen under normal FlatKV operation; it requires independent on-disk corruption of specific keys. That's the same reasoning that was already accepted as sufficient justification for the existing checks in this function, so by that same standard this is worth closing too.

Fix: in the meta.LtHash == nil branch, also reject when len(meta.ModuleLtHashes) != 0 (or more precisely, when lthash.SumModuleHashes(meta.ModuleLtHashes) is non-zero), mirroring the existing SumModuleHashes(...).Equal(root) check already present for the opposite direction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Confirmed unaddressed: store_meta.go's validatePerModuleMetadata still returns nil immediately whenever meta.LtHash == nil, without ever inspecting meta.ModuleLtHashes in that branch (store_meta.go:186-189). Verified against the current file on disk — the function body is unchanged from what's shown in the diff. cursor[bot] independently flagged the exact same gap later in the PR thread ('Nil root skips module hash validation', 2026-07-16T00:11) and it was never revised. The fix described in the original comment (reject when SumModuleHashes(meta.ModuleLtHashes) is non-zero, mirroring the existing check in the len(meta.ModuleLtHashes) == 0 branch) has not been applied.

@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 new issues in this pass, but this is a large, consensus-critical refactor of FlatKV's lattice-hash pipeline (per-module hash/stats decomposition, a new concurrent HashCalculator, across 65 files), and there's still an open, unaddressed Cursor Bugbot High-severity finding (pending-writes-before-Compute-error path) on the latest commit, so it's worth a human look.

What was reviewed: the HashCalculator's ReadOldValues/Compute/ComputeModuleHashInfos pipeline for parallel-chunk merge correctness (MixIn/addition are commutative, so chunking/order doesn't affect the result); the store_apply.go pending-write-mutation-before-Compute-error path flagged as a candidate issue this run and ruled out; and the per-module residue/orphan-stats checks in verify.go and store_meta.go from earlier review rounds.

Extended reasoning...

Overview

This PR refactors FlatKV's lattice-hash (LtHash) subsystem: it renames the LegacyDB to MiscDB, adds per-module LtHash and key/byte-stats decomposition persisted per data DB, and extracts the per-block hash pipeline into a new lthash.HashCalculator backed by a dedicated fixed worker pool shared by the live-commit path and the state-sync importer. It touches 65 files, most within sei-db/state_db/sc/flatkv, plus config, tooling (dump/digest CLIs), and integration test scripts.

Security risks

No injection/auth/data-exposure surface here -- this is internal state-storage/consensus-bookkeeping code, not request-facing. The main risk category is correctness/consensus drift: an error in the hash pipeline could silently corrupt the per-DB or per-module LtHash without affecting the global AppHash (per-module hashes are documented as consensus-irrelevant bookkeeping), or in the worst case corrupt the per-DB root that does feed into AppHash. Several such drift scenarios were already caught by other reviewers in this thread (orphan per-module stats bypassing VerifyLtHash, incomplete per-module metadata validation on load, a partial-mutation-before-error path in ApplyChangeSets) -- some fixed, one (the pending-writes-before-Compute-error path, High severity per Cursor Bugbot on the latest commit) still open without an author response.

Level of scrutiny

This warrants a high level of scrutiny: it's a substantial refactor of a consensus-adjacent subsystem (LtHash feeds the AppHash for FlatKV-enabled nodes) with new concurrency (a shared worker pool used by both live commit and a per-DB-goroutine importer) and several load-time invariant checks whose failure modes are subtle (e.g. partial per-module metadata loss surviving undetected). FlatKV is explicitly pre-production (no on-disk migration/backward-compat is provided), which lowers the blast radius somewhat, but the density of real findings already surfaced by cursor bot, seidroid, and prior review passes in this same thread indicates the code is still accumulating edge-case fixes rather than being settled.

Other factors

Test coverage is extensive (incremental-equals-full-scan invariants, add/update/delete transitions, persistence-after-reopen, state-sync-import-survives-restart, for both hashes and stats), which is reassuring, but the still-open High-severity Cursor finding on the most recent commit ("harden verification") means the review thread is not yet settled. This run's bug hunters investigated a related concern in store_apply.go (pending-write maps mutated before Compute can fail) and did not find it currently exploitable, but that is a narrower question than Cursor's finding and does not resolve it. Given the size, consensus-adjacency, and the open unresolved finding, I'm deferring rather than approving.

"flatkv: %s per-module hashes do not sum to per-DB root (root=%x sum=%x)",
dbDir, meta.LtHash.Checksum(), sum.Checksum(),
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nil root skips module hash validation

Medium Severity

validatePerModuleMetadata returns success whenever LtHash is nil, even if ModuleLtHashes is non-empty. Load then installs a zero per-DB root while still rehydrating those module hashes. The next commit can persist that split and drop the DB’s contribution from the global store hash / AppHash — the failure mode this guard was added to prevent.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4baa0b2. Configure here.

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

A large but well-structured FlatKV refactor: extracts the per-block LtHash pipeline into a shared, pool-backed HashCalculator, adds crash-recoverable per-module hashes + key/byte stats persisted in each DB's LocalMeta, and renames the Legacy* catch-all bucket to Misc*. The concurrency model is sound and thoroughly tested (incremental≡full-scan invariants, persistence-after-reopen, import-survives-restart, load-time validation guard); no blocking issues found, only minor cosmetic staleness.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Leftover cosmetic staleness in sei-db/common/keys/evm_test.go: the test-case name: strings ("EVMAddressToSeiAddress goes to Legacy", "NonceTooShort goes to Legacy", etc.) and the line-67 comment still say "Legacy" even though wantKind was updated to EVMKeyMisc in this PR. Rename to "Misc" for consistency with the rest of the rename.
  • Second-opinion reviews produced no actionable findings: the Codex pass reported "No material issues found in the PR diff" and the Cursor review file was empty. REVIEW_GUIDELINES.md was also empty, so no repo-specific standards could be applied.
  • Verified deadlock-safety of the shared fixed LtHash pool: with queueSize == workers, Submit can block, but foldChunk tasks are self-terminating and each computeDeltasParallel call buffers its result channel to the full task count, so workers never block on send and blocked submitters always drain — the importer's per-DB goroutines (which are not pool workers) can safely share one pool. No change needed; noting since it is the subtlest part of the change.
  • Optional: LtHashThreadsPerCore is only defaulted in DefaultConfig/test config; if a future caller constructs flatkv.Config directly and forgets it, the zero value clamps to a single worker (a silent performance floor, not a correctness bug). Consider documenting that on the field or logging the resolved worker count at startup.

Comment on lines +100 to +129
return nil, counts, fmt.Errorf("failed to gather account updates: %w", err)
}
newAccountValues := deriveNewAccountValues(accountWrites, accountOld, blockHeight)
accountPairs := gatherLTHashPairs(newAccountValues, accountOld)
maps.Copy(s.accountWrites, newAccountValues)
newAccounts := deriveNewAccountValues(accountUpdates, accountOld, blockHeight)
accountPairs := gatherPairs(newAccounts, oldByDB[accountDBDir])
maps.Copy(s.accountWrites, newAccounts)
counts.accountWrites = len(newAccounts)

storageWrites, err := processStorageChanges(changesByType[keys.EVMKeyStorage], blockHeight)
if err != nil {
return fmt.Errorf("failed to parse storage changes: %w", err)
return nil, counts, fmt.Errorf("failed to parse storage changes: %w", err)
}
storagePairs := gatherLTHashPairs(storageWrites, storageOld)
storagePairs := gatherPairs(storageWrites, oldByDB[storageDBDir])
maps.Copy(s.storageWrites, storageWrites)
counts.storageWrites = len(storageWrites)

codeWrites, err := processCodeChanges(changesByType[keys.EVMKeyCode], blockHeight)
if err != nil {
return fmt.Errorf("failed to parse code changes: %w", err)
return nil, counts, fmt.Errorf("failed to parse code changes: %w", err)
}
codePairs := gatherLTHashPairs(codeWrites, codeOld)
codePairs := gatherPairs(codeWrites, oldByDB[codeDBDir])
maps.Copy(s.codeWrites, codeWrites)
counts.codeWrites = len(codeWrites)

legacyWrites, err := processLegacyChanges(changesByType[keys.EVMKeyLegacy], blockHeight)
miscWrites, err := processMiscChanges(changesByType[keys.EVMKeyMisc], blockHeight)
if err != nil {
return fmt.Errorf("failed to parse legacy changes: %w", err)
return nil, counts, fmt.Errorf("failed to parse misc changes: %w", err)
}
legacyPairs := gatherLTHashPairs(legacyWrites, legacyOld)
maps.Copy(s.legacyWrites, legacyWrites)

addKVPairs(s.ctx, accountDBDir, len(newAccountValues))
addKVPairs(s.ctx, storageDBDir, len(storageWrites))
addKVPairs(s.ctx, codeDBDir, len(codeWrites))
addKVPairs(s.ctx, legacyDBDir, len(legacyWrites))
miscPairs := gatherPairs(miscWrites, oldByDB[miscDBDir])
maps.Copy(s.miscWrites, miscWrites)
counts.miscWrites = len(miscWrites)

This comment was marked as resolved.

return m
}

for _, cs := range changeSets {

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.

The empty module name isn't newly accepted here, main's classifyAndPrefix has no guard either. But on main it was harmless (just a /key row folded into the single per-DB hash).

With per-module metadata it becomes fatal: it writes _meta/x:/hash, which ParseModuleLtHashKey rejects on reload, so the new sum validation then fails and the store won't reopen. So the refactor turns a latent no-op into a store-won't-open failure. Cheap guard: reject nil changeset / empty cs.Name at the top of the loop. (Only reachable via malformed input — module names are never empty in normal operation.)

worth to add:

if cs == nil {
        return nil, fmt.Errorf("flatkv: nil changeset")
    }
    if cs.Name == "" {
        return nil, fmt.Errorf("flatkv: changeset with empty module name")
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So through the normal rootmulti → SC path, this is indeed unreachable in practice — confirming the reviewer's "module names are never empty in normal operation" note. But FlatKV could also receive changesets from other feeders that don't necessarily go through that constructor (WAL replay of a possibly-corrupted changelog entry, import/migration tooling, any future direct ApplyChangeSets caller).

Given the failure mode is "store can never be opened again" rather than an immediate loud error at the mistake's source, a cheap guard at the boundary is worth it as defense-in-depth.

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

A large mechanical Legacy→Misc rename plus a well-designed, thoroughly-tested per-module LtHash/stats feature. Consensus-relevant global hash is provably unchanged, the shared-pool concurrency is deadlock-free, and migration is rejected loudly. No blockers; one defense-in-depth suggestion and notes on the empty second-opinion inputs.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Defense-in-depth (raised by Codex as P1; downgraded here): the new guard rejects only an empty cs.Name, but a module name containing / (e.g. /foo) also mis-parses. ModulePhysicalKey + StripModulePrefix split on the first /, so such a name folds into module "", persists _meta/x:/hash (rejected by ParseModuleLtHashKey on reload), and makes the store permanently unopenable via the sum-to-root check. This is not reachable for real Cosmos store keys (hardcoded, never contain /), so it is not a blocker — but since you already guard empty names, consider also rejecting names containing / (or validating a StripModulePrefix round-trip) to make the guard complete for the whole malformed-input class.
  • REVIEW_GUIDELINES.md (base-branch review standards) is empty, so no repo-specific conventions could be applied. cursor-review.md is empty — the Cursor second-opinion pass produced no output. Only the Codex pass produced a finding.
  • Minor/no action needed: deleting all of a module's keys leaves a persisted zeroed _meta/x:<module>/hash (+/stats) entry that is never garbage-collected. Harmless given the module set is tiny and bounded, but worth being aware of for long-lived stores.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

// fails forever). Reject it up front instead; module names are
// never empty in normal operation (Cosmos SDK's NewKVStoreKey
// panics on an empty name), so this only guards malformed input.
if cs.Name == "" {

This comment was marked as low quality.

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

This PR touches consensus-critical FlatKV persistence and metadata-loading logic, and the inline finding shows the state-sync import path can still write an unopenable per-module meta key (permanently bricking the store) even after this PR's live-commit guard — that alone warrants a human look before merge.

Extended reasoning...

Overview

This PR refactors FlatKV's lattice-hash subsystem to add per-DB, per-module LtHash and storage-stats bookkeeping, and renames the LegacyDB/LegacyData catch-all to Misc. It touches the live-commit path (store_apply.go, store_write.go), the state-sync import path (importer.go), metadata load/validate (store_meta.go), and an offline verifier (verify.go), plus a large volume of mechanical rename/test churn.

Security risks

No injection/auth-bypass concerns, but this is consensus-adjacent persistence code: a wrong per-module hash/stats invariant can either corrupt the AppHash-adjacent bookkeeping or (per the inline finding) make a FlatKV store permanently unopenable after ingesting an untrusted, peer-supplied state-sync snapshot with an empty module name. That is a real availability/liveness risk for a node doing state sync, since the guard this PR added in classifyAndPrefix is not mirrored at the importer boundary.

Level of scrutiny

This warrants full human review rather than shadow-approval: it's a consensus-adjacent storage engine with a non-trivial concurrency model (shared worker pool feeding both live commit and the importer), and the PR is still under active back-and-forth with the author (yzang2019) about exactly this defense-in-depth gap as of the latest commits. FlatKV is described as pre-production, which lowers the blast radius somewhat, but the failure mode described (unrecoverable store) is severe enough to want a maintainer's explicit sign-off on the fix, not just an automated pass.

Other factors

The PR already has extensive review activity (multiple cursor[bot] and claude[bot] passes, plus human review from yzang2019 and blindchaser) and good test coverage for the new per-module hash/stats bookkeeping. The one outstanding, confirmed-live bug (importer bypassing the empty-module-name guard) is exactly the kind of finding that should block an automated approval, even though the analogous live-commit-path guard was already merged.

Comment thread sei-db/state_db/sc/flatkv/importer.go
require.NotNil(t, d)
require.Equal(t, wantKeys, d.KeyCount)
require.Equal(t, wantBytes, d.Bytes)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Parallel stats test stays serial

Low Severity

TestComputeModuleHashInfosStatsParallel claims to cover the pooled chunk-merge path, but builds only computeChunkSize*3+7 (307) pairs. That is below parallelThreshold (1000), so ComputeModuleHashInfos always takes the serial path and never exercises computeDeltasParallel.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dbf1701. Configure here.

@yzang2019 yzang2019 enabled auto-merge July 16, 2026 19:21

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

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit e7c7e33. Configure here.

dst[module] = st
}
return dst
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicate module map cloners

Low Severity

cloneModuleHashes / cloneModuleStats in store_meta.go duplicate cloneModuleMap / cloneModuleStatsMap in lthash/hash_calculator.go. Same nil-safe deep-copy behavior lives in two packages, so a future fix to one copy can easily miss the other.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e7c7e33. Configure here.

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

A large, mostly-mechanical LegacyDB→MiscDB rename plus a well-contained, well-tested per-module LtHash/stats feature (new HashCalculator over a dedicated fixed pool, load-time validation, and expanded VerifyLtHash). The refactor preserves the global/consensus LtHash for fresh stores and the concurrency design is sound; no blocking issues found, only a cosmetic nit and notes about missing second-opinion output.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • Cursor's second-opinion review file (cursor-review.md) was empty — that pass produced no output.
  • Codex reported no material issues but explicitly could not run tests (Go 1.25.6 toolchain unavailable offline); rely on CI's go test -race for the correctness-critical per-module hash/stats invariants rather than treating Codex's pass as test verification.
  • REVIEW_GUIDELINES.md is empty, so no repo-specific review standards could be applied.
  • Positive/confirmation: the consensus-relevant global LtHash is unchanged for fresh stores (per-module hashes sum homomorphically to the per-DB root and global root), and validatePerModuleMetadata correctly fails loudly on pre-per-module or drifted bookkeeping — appropriate since migration is intentionally unsupported. No action needed.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

fmt.Printf("code count=%d bucket_digest=%X\n", d.code.count, d.code.acc)
fmt.Printf("storage count=%d bucket_digest=%X\n", d.storage.count, d.storage.acc)
fmt.Printf("legacy count=%d bucket_digest=%X\n", legacyCountForCompare, legacyForCompare)
fmt.Printf("misc count=%d bucket_digest=%X\n", miscCountForCompare, miscForCompare)

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] Column misalignment: "misc count=" uses 3 trailing spaces (width 7), whereas the adjacent account/code/storage lines pad the label to width 9 (e.g. "account count=", "storage count="). The old "legacy count=" was width 9; misc should use 5 trailing spaces to keep the diagnostic output aligned. Purely cosmetic.

@yzang2019 yzang2019 added this pull request to the merge queue Jul 16, 2026
Merged via the queue into main with commit 6ab4b85 Jul 16, 2026
74 checks passed
@yzang2019 yzang2019 deleted the yzang/per-module-refactor branch July 16, 2026 20:03
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.

3 participants