balances and bools - #4093
Conversation
PR SummaryHigh Risk Overview Balance storage is wired end-to-end: new Tests and random/composite SS paths are updated for balance-only accounts, coordinated deletes of merged account fields, and the new presence semantics (cleared/deleted slots and code read as absent, not empty). Reviewed by Cursor Bugbot for commit 792b4af. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4093 +/- ##
==========================================
- Coverage 61.31% 60.64% -0.67%
==========================================
Files 2183 2111 -72
Lines 191331 184181 -7150
==========================================
- Hits 117310 111694 -5616
+ Misses 62942 61924 -1018
+ Partials 11079 10563 -516
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 792b4af. Configure here.
| s.store.mu.RUnlock() | ||
| if ok { | ||
| return value.value | ||
| return value.value, true |
There was a problem hiding this comment.
Zero overlay balance reported present
Medium Severity
The memory-store overlay reports a balance as present for any latest write, including a zeroed or deleted value. FlatKV and the new read contract treat a zero balance as not stored, so GetBalance's ok disagrees across implementations when an account is drained or a balance is removed.
Reviewed by Cursor Bugbot for commit 792b4af. Configure here.
There was a problem hiding this comment.
Solid, well-tested groundwork: the new 0x21 balance key kind is wired consistently through FlatKV's account row (apply/merge, read, six-lane iterator, import translator, SS snapshot split, digest tool), and the (value, ok) state-view API change is mechanical with no production consumer of the bool yet. No blockers found; four non-blocking issues, mostly around contract consistency and the newly-created SS balance sub-DB on existing evm-ss-separate-dbs stores.
Findings: 0 blocking | 6 non-blocking | 4 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- [suggestion] The "a zero account field means the field is absent" rule is now restated at six independent sites:
accountFieldValue(store_read.go),flatKVStateView.GetBalanceand.GetCodeHash(state_view.go),buildAccountCodehashLaneandbuildAccountBalanceLane(store_iteration.go), andconvertFlatKVNodes(ss/composite/store.go). Adding balance required touching five of them; a seventh account field would require all six. Per AGENTS.md ("Guard at the choke point, never at each caller"), consider a singlevtype-level field-presence accessor (e.g.AccountData.Field(kind) ([]byte, bool)) that every projection path routes through, so the rule cannot be forgotten at a new call site. - 4 suggestion(s)/nit(s) flagged inline on specific lines.
- 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.
Pre-existing issues
- [suggestion]
CommitStore.GetBlockHeightModified(sei-db/state_db/sc/flatkv/store_read.go:113) already had this shape forEVMKeyCodeHashbefore this PR: an account row with a zero code hash reportsfound=true(whileGetreports the same key absent), and the height returned is the row's shared height, so a nonce-only write moves the code hash's reported "last modified" height. The PR extends the same behaviour to balance.
| return sd.GetBlockHeight(), true, nil | ||
|
|
||
| case keys.EVMKeyNonce, keys.EVMKeyCodeHash: | ||
| case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: |
There was a problem hiding this comment.
[suggestion] Adding EVMKeyBalance here inherits two behaviours that disagree with Get for the same key:
- Presence:
Getroutes throughaccountFieldValue, which reports a zero balance as absent.GetBlockHeightModifiedreportsfound=truefor a balance key on any live account row, so a nonce-only account (or one whose balance was just zeroed, i.e. deleted) answers(rowHeight, true, nil)for a balance thatGet/GetBalanceboth call missing. - Height: the height is the account row's, shared by all three merged fields, so a later nonce or code-hash write silently changes the balance's reported last-modified height.
There are no production callers of GetBlockHeightModified today (only the giga.LiveStateStore interface declaration), so this is latent rather than live. Routing the account case through the same field-presence check Get uses — returning (-1, false, nil) when the named field is zero — would keep the two reads consistent. Note the code-hash half of this predates the PR; balance is the new instance.
| StoreCode, | ||
| StoreStorage, | ||
| StoreMisc, | ||
| StoreBalance, |
There was a problem hiding this comment.
[suggestion] Adding StoreBalance to AllEVMStoreTypes() makes NewEVMStateStore open a sixth sub-DB directory (<dir>/balance) when evm-ss-separate-dbs = true. On an existing store that directory is created fresh, so its GetLatestVersion() is 0 — and EVMStateStore.GetLatestVersion() takes the min across managedDBs (store.go:174-185). RecoverCompositeStateStore (ss/composite/store.go:686-694) derives startVersion from that min, so the first startup after this change replays the whole available changelog into the EVM store, re-applying already-present versions and walking SetLatestVersion back up from the WAL's first entry.
The flag is non-default and documented as experimental (docs/migration/giga_store_migration.md), so this is a slow/noisy upgrade rather than data loss. Still worth handling explicitly — e.g. seed a newly created sub-DB's latest-version marker from the store's existing max on first open, or have recovery ignore sub-DBs that are empty.
| baseNonce := s.store.base.GetNonce(address) | ||
| // The base reader reports no presence of its own, so a missing account is indistinguishable from | ||
| // one whose nonce is zero. | ||
| return baseNonce, baseNonce != 0 |
There was a problem hiding this comment.
[suggestion] This contradicts the contract the PR just wrote for EVMStateView: "an account that exists with a nonce of 0 reads as (0, true)" (sei-db/state_db/giga/state_view.go:50-51), and the method doc "whether addr has an account". An account present only in the base reader with nonce 0 (e.g. funded but never having sent a tx) reads as (0, false) here.
AccountExists on this same type (line 387) already solves the base-presence problem by type-asserting the base reader for an AccountExists(common.Address) bool method; GetNonce could reuse it instead of inferring presence from baseNonce != 0.
The mirror-image case is GetBalance (line 422): an explicitly written zero balance returns (0, true), whereas flatKVStateView.GetBalance returns (0, false) for a stored zero. So the two implementations of one interface disagree on the zero cases in opposite directions. No caller branches on the bool yet, so nothing is broken today — but the first one that does will get implementation-dependent behaviour. Either align the implementations or state in the interface doc that a zero value may be reported as present or absent for fields whose zero encodes absence.
| codeKeyPrefix = []byte{0x07} | ||
| codeHashKeyPrefix = []byte{0x08} | ||
| nonceKeyPrefix = []byte{0x0a} | ||
| balanceKeyPrefix = []byte{0x21} |
There was a problem hiding this comment.
[suggestion] This is the second declaration of 0x21; x/evm/types.BalanceKeyPrefix is the first, and the block comment above ("mirrored from x/evm/types.go") is the only thing tying them together. TestEVMKeyPrefixesAreDistinct checks the mirrored prefixes are distinct from each other but not that they still match their originals, so a future prefix change in x/evm/types reroutes balance keys to the misc lane with a green test suite.
sei-db test packages can import x/evm/types (see sei-db/state_db/ss/composite/recovery_test.go:374 using evmtypes.NonceKeyPrefix), so a drift test asserting each EVMKeyPrefixByte(kind) equals the corresponding evmtypes.*KeyPrefix[0] is available and would cover the whole mirrored set, not just balance.
| return value, value != nil | ||
|
|
||
| case keys.EVMKeyNonce, keys.EVMKeyCodeHash: | ||
| case keys.EVMKeyNonce, keys.EVMKeyCodeHash, keys.EVMKeyBalance: |
There was a problem hiding this comment.
GetBlockHeightModified and Get disagree about whether a balance key exists.
Get on a balance key for an account whose balance is zero returns (nil, false), but GetBlockHeightModified on the same key returns (height, true).
Is that an issue?
| changesByType[keys.EVMKeyNonce], | ||
| changesByType[keys.EVMKeyCodeHash], | ||
| nil, // TODO: update this when we add a balance key! | ||
| changesByType[keys.EVMKeyBalance], |
There was a problem hiding this comment.
Before this PR the balance bytes were always zero, because we passed nil for the balance argument so "a row exists" implied "nonce ≠ 0 or codeHash ≠ 0".
This PR replaces that nil with changesByType[keys.EVMKeyBalance] (store_apply.go:129), so a balance write alone now creates a row, and "a row exists" no longer implies anything about the nonce.
Question is will this behavior change cause any issues?


Describe your changes and provide context
This PR updates FlatKV in two important ways:
ok boolreturn parameter