Skip to content

feat(flatkv): introduce module-prefix physical keys across all FlatKV DBs#3229

Merged
blindchaser merged 7 commits intomainfrom
yiren/flatkv-prefix
Apr 14, 2026
Merged

feat(flatkv): introduce module-prefix physical keys across all FlatKV DBs#3229
blindchaser merged 7 commits intomainfrom
yiren/flatkv-prefix

Conversation

@blindchaser
Copy link
Copy Markdown
Contributor

@blindchaser blindchaser commented Apr 10, 2026

Summary

Introduces module-prefixed physical keys across all FlatKV data DBs, enabling non-EVM Cosmos modules to be stored in legacyDB and preparing for future DB consolidation by ensuring key uniqueness across namespaces.

Physical key format: "module/" + type_prefix + stripped_key. EVM keys become "evm/" + kind_byte + addr_or_slot, non-EVM Cosmos keys become "<module>/" + original_key.

  • ktype/ktype.go (new package): Defines physical key encoding — ModulePhysicalKey, EVMPhysicalKey, StripModulePrefix, StripEVMPhysicalKey, PrefixEnd. Defines EVMKeyAccount as the canonical kind for merged account rows. Replaces the old keys.go in the flatkv package.
  • common/evm/keys.go: Adds EVMKeyPrefixByte helper. BuildMemIAVLEVMKey refactored to use it, eliminating a redundant allocation.
  • store_apply.go: ApplyChangeSets now splits incoming changesets into EVM vs non-EVM; non-EVM modules are routed into legacyDB under their module prefix. classifyAndPrefix replaces the former two-pass sortChangeSets+prefixModuleKeys, converting keys to physical format via ktype helpers in a single pass.
  • store_read.go: Extracts generic readFromDB that checks pending writes then falls back to DB, replacing four per-DB getter implementations. getLegacyData/getLegacyValue now require a moduleName parameter. getAccountData, getStorageData, getCodeData validate key length before constructing physical keys.
  • store_write.go: Replaces prepareBatchCodeDB, prepareBatchStorageDB, prepareBatchLegacyDB with a single generic prepareBatch[T]. Extracts collectPendingReads and deserializeBatchResults generics to deduplicate batchReadOldValues. Commit ordering fix: global metadata is now persisted to metadataDB before updating in-memory committedVersion/committedLtHash, so a metadata write failure cannot leave the store in an inconsistent state.
  • api.go: Store interface updated — Get, Has, GetBlockHeightModified now take a moduleName parameter. Iterator docs updated to reflect physical key format for Key() and dual-format acceptance for SeekGE/SeekLT.
  • vtype/legacy_data.go: Replaces value-length inference for deletion with an explicit MarkDeleted() flag and isDelete field. The previous implementation treated len(value) == 0 as a deletion, but Cosmos SDK modules (bank, staking, etc.) legitimately write empty values ([]byte{}) to the store — this is standard Cosmos convention and not a deletion. Now that non-EVM modules are routed into legacyDB, the old heuristic would silently drop valid empty-value writes. MarkDeleted() makes the delete intent explicit, and SetValue (even with nil or []byte{}) clears the delete flag, preserving Cosmos semantics.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 10, 2026

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedApr 13, 2026, 10:45 PM

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 84.92308% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.26%. Comparing base (21433ce) to head (45c39c8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/state_db/sc/flatkv/store_write.go 83.80% 8 Missing and 9 partials ⚠️
sei-db/state_db/sc/flatkv/exporter.go 64.51% 6 Missing and 5 partials ⚠️
sei-db/state_db/sc/flatkv/store_read.go 75.00% 5 Missing and 4 partials ⚠️
sei-db/state_db/sc/flatkv/iterator.go 85.71% 2 Missing and 3 partials ⚠️
sei-db/state_db/sc/flatkv/ktype/ktype.go 91.48% 2 Missing and 2 partials ⚠️
sei-db/state_db/sc/flatkv/store_apply.go 93.47% 1 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #3229   +/-   ##
=======================================
  Coverage   59.26%   59.26%           
=======================================
  Files        2068     2069    +1     
  Lines      169695   169663   -32     
=======================================
- Hits       100572   100556   -16     
+ Misses      60334    60319   -15     
+ Partials     8789     8788    -1     
Flag Coverage Δ
sei-chain-pr 76.60% <84.92%> (?)
sei-db 70.41% <ø> (ø)

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

Files with missing lines Coverage Δ
sei-db/common/evm/keys.go 92.00% <100.00%> (+0.51%) ⬆️
sei-db/state_db/sc/flatkv/importer.go 72.09% <100.00%> (ø)
sei-db/state_db/sc/flatkv/keys.go 100.00% <ø> (ø)
sei-db/state_db/sc/flatkv/store.go 73.27% <ø> (ø)
sei-db/state_db/sc/flatkv/vtype/legacy_data.go 93.61% <100.00%> (+0.93%) ⬆️
sei-db/state_db/sc/flatkv/store_apply.go 88.50% <93.47%> (-0.87%) ⬇️
sei-db/state_db/sc/flatkv/ktype/ktype.go 91.48% <91.48%> (ø)
sei-db/state_db/sc/flatkv/iterator.go 78.75% <85.71%> (+1.50%) ⬆️
sei-db/state_db/sc/flatkv/store_read.go 61.64% <75.00%> (-3.14%) ⬇️
sei-db/state_db/sc/flatkv/exporter.go 73.18% <64.51%> (-7.94%) ⬇️
... and 1 more
🚀 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.

Copy link
Copy Markdown

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

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: 172fcfa983

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

// prefixModuleKeys, avoiding double-prefixing.
return []*types.SnapshotNode{{
Key: key,
Key: originalKey,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve module identity in exported legacy nodes

This now strips the <module>/ namespace and emits only originalKey, but importer writes all incoming nodes under the evm changeset, so non-EVM legacy entries (for example bank/foo) reimport as evm/foo. That can overwrite real EVM legacy keys with the same raw key and corrupt state-sync/import results for any module data newly routed into legacyDB.

Useful? React with 👍 / 👎.

if len(keyBytes) != AddressLen {
return nil, nil
}
return readFromDB(EVMPhysicalKey(EVMKeyAccount, keyBytes), s.accountWrites, s.accountDB, vtype.DeserializeAccountData, "accountDB")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add compatibility path for pre-prefix physical keys

The read path now always looks up account rows using the new prefixed physical key format (evm/...), but this commit does not include a migration or fallback for existing databases that still store rows in the old unprefixed layout. On upgrade, previously committed entries become unreadable until rewritten, which changes Get/Has behavior and can produce incorrect execution on live state.

Useful? React with 👍 / 👎.

Comment on lines +101 to +102
// Clears the delete flag — an explicit SetValue is a write, not a deletion,
// even when value is empty ([]byte{} is a valid Cosmos module value).
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.

Ah, I didn't know this. Good thing to catch!

Comment on lines +257 to +259
// Emit the original key (without module prefix). The importer feeds nodes
// back through ApplyChangeSets which re-applies the prefix via
// prefixModuleKeys, avoiding double-prefixing.
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 a request to change this PR, since this preserves the existing behavior. But we should discuss import/export as a larger group.

I think we need to change the way we export data. Instead of exporting the top level application key-value pairs, I think we need to export the raw keys-value pairs including all of the metadata. This is because we need to pass metadata fields such as block height modified and serialization version during a state sync if we want to calculate the same state hash, and these values are not currently contained within the data we currently export.

Comment on lines +120 to +121
// and original key. Returns ok=false if no "/" separator is found.
func StripModulePrefix(physicalKey []byte) (moduleName string, originalKey []byte, ok bool) {
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.

Is there a reason why we don't return an error if we don't find a "/" character? Should this be something that bubbles up as an error to the user if there is a malformed key?

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.

good call. changed to return error instead of ok bool

// StripEVMPhysicalKey extracts the EVM key kind and stripped key from a
// physical DB key. This is the inverse of EVMPhysicalKey for export paths.
// For account keys the returned kind is EVMKeyAccount (evm.EVMKeyNonce).
func StripEVMPhysicalKey(physicalKey []byte) (kind evm.EVMKeyKind, strippedKey []byte, ok bool) {
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.

If you promote StripModulePrefix() to return an error, then you should also promote this function to return an error.

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.

done. propagates the error from StripModulePrefix

Comment on lines +26 to +46
for _, cs := range changeSets {
if cs.Name == evm.EVMStoreKey {
evmChangeSets = append(evmChangeSets, cs)
} else {
if cs.Changeset.Pairs == nil {
continue
}
modMap, ok := nonEVMByModule[cs.Name]
if !ok {
modMap = make(map[string][]byte, len(cs.Changeset.Pairs))
nonEVMByModule[cs.Name] = modMap
}
for _, pair := range cs.Changeset.Pairs {
if pair.Delete {
modMap[string(pair.Key)] = nil
} else {
modMap[string(pair.Key)] = pair.Value
}
}
}
}
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.

Could we pull this logic into sortChangeSets()? Perhaps it would make sense to also add modules to keys, so we'd want to rename the function to sortAndAddPrefix() or something.

Comment on lines +248 to +249
if len(keyBytes) != AddressLen {
return nil, nil
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.

we shouldn't ignore malformed keys

}
}
if err = deserializeBatchResults(storageBatch, storageOld, vtype.DeserializeStorageData, "storageDB"); err != nil {
return nil, nil, nil, nil, err
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.

nit, perhaps wrap error

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.

agreed. getAccountData, getStorageData, and getCodeData now return fmt.Errorf with expected vs actual key length instead of nil, nil

}
}
if err = deserializeBatchResults(accountBatch, accountOld, vtype.DeserializeAccountData, "accountDB"); err != nil {
return nil, nil, nil, nil, err
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.

nit, perhaps wrap error

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.

wrapped all four deserializeBatchResults calls with context

}
}
if err = deserializeBatchResults(codeBatch, codeOld, vtype.DeserializeCodeData, "codeDB"); err != nil {
return nil, nil, nil, nil, err
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.

nit, perhaps wrap error

}
}
if err = deserializeBatchResults(legacyBatch, legacyOld, vtype.DeserializeLegacyData, "legacyDB"); err != nil {
return nil, nil, nil, nil, err
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.

nit, perhaps wrap error

@yzang2019
Copy link
Copy Markdown
Contributor

Some suggestions:

  1. Follow the vtype Package Pattern — Create a ktype Package
    The repo already has a vtype/ package that cleanly encapsulates value-type serialization and deserialization (AccountData, StorageData, LegacyData, etc.). The key serialization logic introduced in this PR — ModulePhysicalKey, StripModulePrefix, EVMPhysicalKey, StripEVMPhysicalKey, PrefixEnd, plus the Address, Slot, and LocalMeta types — would be better housed in an analogous ktype/ package. Right now these are all in keys.go in the root flatkv package alongside the store implementation. Following Cody's pattern with vtype would keep the codebase consistent and make the key format a first-class abstraction with its own tests.

  2. Consolidate Sorting and Prefixing
    As cody-littley suggested, sortChangeSets and prefixModuleKeys could be merged into a single pass. Currently sortChangeSets classifies EVM pairs by kind using memiavl keys, and then prefixModuleKeys iterates again to prepend the prefix. Doing the prefix during classification avoids the extra iteration and the temporary non-prefixed maps.

nonEVMByModule map[string]map[string][]byte,
) {
evmPrefix := evm.EVMStoreKey + "/"
for kind, m := range changesByType {
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.

This allocates a new map and string-concatenates every key on every block. For storage-heavy blocks with tens of thousands of entries, this is a non-trivial allocation. Consider prepending the prefix earlier in the pipeline (inside sortChangeSets) to avoid the extra copy pass, or using a key builder that avoids repeated string concatenation.

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.

good point

@blindchaser blindchaser added this pull request to the merge queue Apr 14, 2026
Merged via the queue into main with commit dfbea10 Apr 14, 2026
41 checks passed
@blindchaser blindchaser deleted the yiren/flatkv-prefix branch April 14, 2026 02:11
yzang2019 added a commit that referenced this pull request Apr 14, 2026
* main:
  feat(flatkv): introduce module-prefix physical keys across all FlatKV DBs (#3229)
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