Skip to content

feat(flatkv): implements Account Row GC for FlatKV accountDB#3084

Merged
blindchaser merged 7 commits into
mainfrom
yiren/flatkv-acc-delete
Mar 23, 2026
Merged

feat(flatkv): implements Account Row GC for FlatKV accountDB#3084
blindchaser merged 7 commits into
mainfrom
yiren/flatkv-acc-delete

Conversation

@blindchaser

Copy link
Copy Markdown
Contributor

Summary

Implements Account Row GC for FlatKV accountDB. Field deletes for nonce and codehash now collapse to a physical row delete when the full AccountValue becomes zero, so pending reads, committed state, LtHash updates, WAL replay, and rollback all treat an all-zero account as absent rather than as a persisted zero-value row.

  • keys.go: Replaces field-specific account helpers with AccountValue.IsEmpty(), which enforces the row-deletion invariant across balance, nonce, and codehash. Simplifies EncodeAccountValue() to write directly into a fixed-size output buffer without changing the 40-byte EOA / 72-byte contract encoding contract.
  • store.go: Extends pendingAccountWrite with isDelete, making row deletion an explicit pending-state transition rather than an implicit encoding convention.
  • store_read.go: Get() now returns (nil, false) for pending or committed all-zero account rows, aligning pre-commit reads with post-commit storage. Consolidates storage/code/legacy read paths through a shared KV helper while preserving delete-vs-not-found behavior.
  • store_write.go: ApplyChangeSets() flips isDelete on full account clears and clears it on any subsequent write, which guarantees delete-then-recreate ordering within the same block. LtHash baselines capture nil once a row is logically deleted, preventing phantom MixOut during cross-apply recreation. commitBatches() now issues batch.Delete() for deleted account rows and shares local-meta encoding across all DB batch builders. The combined LtHash pair slice is preallocated to avoid append aliasing across DB-specific pair lists.

Test plan

lthash_correctness_test.go:

  • AccountRowDelete: Full nonce+codehash delete removes the row and keeps LtHash equal to a full scan.
  • AccountDeleteThenRecreate: Delete in one ApplyChangeSets, recreate in a later ApplyChangeSets in the same block, and verify LtHash uses nil as the baseline after the logical delete.
  • AccountPartialDeletePreservesRow: Delete only codehash and verify the row remains encoded as an EOA.
  • AccountPendingReadPartialDelete: Confirms partial deletes keep isDelete=false and pending reads still expose surviving fields.
  • AccountRowDeleteGetBeforeCommit: Confirms full deletes return not-found from Get() and Has() before commit.

store_write_test.go:

  • Updates existing delete-semantics and cross-apply ordering tests to assert row absence rather than persisted zero nonce rows.
  • AccountRowDeletedWhenAllFieldsZero: End-to-end row GC after commit.
  • AccountRowPersistsWhenPartiallyZero: Partial field zeroing preserves the row.
  • AccountRowDeleteThenRecreate: Delete in one block and recreate in a later block.

snapshot_test.go:

  • Updates reopen expectations so deleted all-zero accounts remain absent after restart.
  • AccountRowDeletePersistsAfterReopen: Reopen preserves both deletion and LtHash.
  • AccountRowDeleteSurvivesWALReplay: Simulates a crash after per-DB writes but before the global watermark advances, then verifies WAL catchup replays the delete correctly.
  • AccountRowDeleteAfterSnapshotRollback: Rolling back to a pre-delete snapshot restores the row.

keys_test.go:

  • Replaces removed helper coverage with AccountValueIsEmpty, covering zero, partial, and fully populated account values.

@github-actions

github-actions Bot commented Mar 18, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedMar 22, 2026, 9:49 PM

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

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

Comment on lines 353 to +356
for _, paw := range s.accountWrites {
key := AccountKey(paw.addr)
encoded := EncodeAccountValue(paw.value)
if err := batch.Set(key, encoded); err != nil {
return fmt.Errorf("accountDB set: %w", err)
}
}

// Update local meta atomically with data (same batch)
newLocalMeta := &LocalMeta{
CommittedVersion: version,
}
if err := batch.Set(DBLocalMetaKey, MarshalLocalMeta(newLocalMeta)); err != nil {
return fmt.Errorf("accountDB local meta set: %w", err)
}
pending = append(pending, pendingCommit{accountDBDir, batch})
}

// Commit to codeDB
if len(s.codeWrites) > 0 || version > s.localMeta[codeDBDir].CommittedVersion {
s.phaseTimer.SetPhase("commit_code_db_prepare")
batch := s.codeDB.NewBatch()
defer func() { _ = batch.Close() }()

for _, pw := range s.codeWrites {
if pw.isDelete {
if err := batch.Delete(pw.key); err != nil {
return fmt.Errorf("codeDB delete: %w", err)
if paw.isDelete {
if err := batch.Delete(key); 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.

P1 Badge Add a migration for pre-existing all-zero account rows

Prior FlatKV versions persisted account deletes as 40-byte zero rows, and this branch only removes rows that are present in s.accountWrites during a new commit. Because LoadVersion/WAL catchup never sweep already-committed accountDB entries, an in-place upgrade leaves those historical tombstones intact forever: Get(nonce) still returns found=true for old deletions, and the upgraded node keeps a different LtHash/root hash than a node rebuilt from history under the new deletion rules. This needs an upgrade-time cleanup or compatibility read path, not just the new batch.Delete on freshly touched addresses.

Useful? React with 👍 / 👎.

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.

👎

There is no pre-existing state in production environments in FlatKV, so no extra migration needed.

@codecov

codecov Bot commented Mar 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.74699% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.55%. Comparing base (b1cb904) to head (0c2c5a1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/state_db/sc/flatkv/store_write.go 80.95% 5 Missing and 3 partials ⚠️
sei-db/state_db/sc/flatkv/store_read.go 90.32% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3084      +/-   ##
==========================================
- Coverage   58.59%   58.55%   -0.05%     
==========================================
  Files        2096     2090       -6     
  Lines      173408   171845    -1563     
==========================================
- Hits       101610   100621     -989     
+ Misses      62753    62287     -466     
+ Partials     9045     8937     -108     
Flag Coverage Δ
sei-chain-pr 68.75% <86.74%> (?)
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/state_db/sc/flatkv/keys.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/store.go 72.55% <ø> (ø)
sei-db/state_db/sc/flatkv/store_read.go 69.91% <90.32%> (+5.57%) ⬆️
sei-db/state_db/sc/flatkv/store_write.go 83.64% <80.95%> (+3.35%) ⬆️

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

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

Overall, this looks to achieve the goals described in the description, so I'm a 👍 on merging.

In the slightly longer term, I think it might be worth thinking about whether this code can be generalized. For example, do we also want to do deletions in the other DBs if we see all-zero values? If we generalized, it would probably look something like this:

  • the pebbleDB wrapper checks each value to see if it is all zeros, and instead does a deletion if it sees this
  • the cache layer treats values that are all zeros as deleted values
  • when passing a changeset to the part of the code that computes lattice hashes, it treats key deletions equivalently to setting the value to all zeros (i.e. both are an equivalent deletion)

Comment on lines 353 to +356
for _, paw := range s.accountWrites {
key := AccountKey(paw.addr)
encoded := EncodeAccountValue(paw.value)
if err := batch.Set(key, encoded); err != nil {
return fmt.Errorf("accountDB set: %w", err)
}
}

// Update local meta atomically with data (same batch)
newLocalMeta := &LocalMeta{
CommittedVersion: version,
}
if err := batch.Set(DBLocalMetaKey, MarshalLocalMeta(newLocalMeta)); err != nil {
return fmt.Errorf("accountDB local meta set: %w", err)
}
pending = append(pending, pendingCommit{accountDBDir, batch})
}

// Commit to codeDB
if len(s.codeWrites) > 0 || version > s.localMeta[codeDBDir].CommittedVersion {
s.phaseTimer.SetPhase("commit_code_db_prepare")
batch := s.codeDB.NewBatch()
defer func() { _ = batch.Close() }()

for _, pw := range s.codeWrites {
if pw.isDelete {
if err := batch.Delete(pw.key); err != nil {
return fmt.Errorf("codeDB delete: %w", err)
if paw.isDelete {
if err := batch.Delete(key); err != 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.

👎

There is no pre-existing state in production environments in FlatKV, so no extra migration needed.

@blindchaser blindchaser requested a review from yzang2019 March 20, 2026 17:28
@blindchaser blindchaser enabled auto-merge March 22, 2026 21:48
@blindchaser blindchaser disabled auto-merge March 22, 2026 21:48
@blindchaser blindchaser added this pull request to the merge queue Mar 22, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 22, 2026
@blindchaser blindchaser added this pull request to the merge queue Mar 23, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Mar 23, 2026
@blindchaser blindchaser added this pull request to the merge queue Mar 23, 2026
Merged via the queue into main with commit 9dc8d6e Mar 23, 2026
38 checks passed
@blindchaser blindchaser deleted the yiren/flatkv-acc-delete branch March 23, 2026 17:39
blindchaser added a commit that referenced this pull request Apr 17, 2026
## Summary

Implements Account Row GC for FlatKV `accountDB`. Field deletes for
nonce and codehash now collapse to a physical row delete when the full
`AccountValue` becomes zero, so pending reads, committed state, LtHash
updates, WAL replay, and rollback all treat an all-zero account as
absent rather than as a persisted zero-value row.

- `keys.go`: Replaces field-specific account helpers with
`AccountValue.IsEmpty()`, which enforces the row-deletion invariant
across balance, nonce, and codehash. Simplifies `EncodeAccountValue()`
to write directly into a fixed-size output buffer without changing the
40-byte EOA / 72-byte contract encoding contract.
- `store.go`: Extends `pendingAccountWrite` with `isDelete`, making row
deletion an explicit pending-state transition rather than an implicit
encoding convention.
- `store_read.go`: `Get()` now returns `(nil, false)` for pending or
committed all-zero account rows, aligning pre-commit reads with
post-commit storage. Consolidates storage/code/legacy read paths through
a shared KV helper while preserving delete-vs-not-found behavior.
- `store_write.go`: `ApplyChangeSets()` flips `isDelete` on full account
clears and clears it on any subsequent write, which guarantees
delete-then-recreate ordering within the same block. LtHash baselines
capture `nil` once a row is logically deleted, preventing phantom MixOut
during cross-apply recreation. `commitBatches()` now issues
`batch.Delete()` for deleted account rows and shares local-meta encoding
across all DB batch builders. The combined LtHash pair slice is
preallocated to avoid append aliasing across DB-specific pair lists.

## Test plan

`lthash_correctness_test.go`:
- **AccountRowDelete**: Full nonce+codehash delete removes the row and
keeps LtHash equal to a full scan.
- **AccountDeleteThenRecreate**: Delete in one `ApplyChangeSets`,
recreate in a later `ApplyChangeSets` in the same block, and verify
LtHash uses `nil` as the baseline after the logical delete.
- **AccountPartialDeletePreservesRow**: Delete only codehash and verify
the row remains encoded as an EOA.
- **AccountPendingReadPartialDelete**: Confirms partial deletes keep
`isDelete=false` and pending reads still expose surviving fields.
- **AccountRowDeleteGetBeforeCommit**: Confirms full deletes return
not-found from `Get()` and `Has()` before commit.

`store_write_test.go`:
- Updates existing delete-semantics and cross-apply ordering tests to
assert row absence rather than persisted zero nonce rows.
- **AccountRowDeletedWhenAllFieldsZero**: End-to-end row GC after
commit.
- **AccountRowPersistsWhenPartiallyZero**: Partial field zeroing
preserves the row.
- **AccountRowDeleteThenRecreate**: Delete in one block and recreate in
a later block.

`snapshot_test.go`:
- Updates reopen expectations so deleted all-zero accounts remain absent
after restart.
- **AccountRowDeletePersistsAfterReopen**: Reopen preserves both
deletion and LtHash.
- **AccountRowDeleteSurvivesWALReplay**: Simulates a crash after per-DB
writes but before the global watermark advances, then verifies WAL
catchup replays the delete correctly.
- **AccountRowDeleteAfterSnapshotRollback**: Rolling back to a
pre-delete snapshot restores the row.

`keys_test.go`:
- Replaces removed helper coverage with **AccountValueIsEmpty**,
covering zero, partial, and fully populated account values.
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