#26551·cosmos-sdk

`rootmulti.PruneStores` prunes IAVL versions but never deletes commit-info records (`s/<version>`) — unbounded metadata DB growth

Author: MirageFoundationCreated Jun 23, 2026Updated Jun 23, 2026

Summary

rootmulti.Store writes one CommitInfo record per height to the metadata DB under key s/<decimal version> (commitInfoKeyFmt = "s/%d") on every Commit() via flushCommitInfo. PruneStores(pruningHeight) prunes the IAVL substore versions (DeleteVersionsTo) and advances the s/earliest pointer, but it never deletes the now-stale s/<version> commit-info records. There is no deleteCommitInfo anywhere in the module. As a result the commit-info records accumulate without bound — one per block, forever — regardless of the pruning configuration.

Affected versions (verified 2026-06-23, byte-identical store/rootmulti/store.go)

  • github.com/cosmos/cosmos-sdk/store/[email protected] — the rootmulti store linked by the SDK v0.54 baseapp (this is the module we run)
  • release/v0.54.x — identical
  • main — identical (bug NOT fixed)
  • legacy cosmossdk.io/[email protected] — same bug

Note: the store/v2 module path here is the rootmulti store, not an SS/SC rewrite — there is no store/commitment or store/storage in the repo. So no released or main version of the cosmos-sdk store currently deletes commit-info on prune.

Permalinks (swap to a commit SHA when filing):

Why it usually goes unnoticed

On most chains the IAVL node data dwarfs the commit-info, so the leak hides inside normal DB growth. It becomes the dominant cost on chains where the live IAVL state is small relative to chain length (frequent pruning + modest state). The s/earliest pointer narrows the queryable range but frees no disk, which can give a false impression that pruning is reclaiming space.

Impact (real-world)

A production chain (~small live state, keep-recent pruning enabled) at ~3.1M+ blocks:

  • application.db1.84 GB, of which the IAVL state is only ~85 MB.
  • The remaining ~1.83 GB is 2.1M–2.4M commit-info records (s/<version>), one per height since DB creation, on every node (healthy and otherwise — it is deterministic).
  • Each record is ~hundreds of bytes and accrues every block, so disk grows without bound and is never reclaimed by pruning.

Reproduction

  1. Start a node (or a rootmulti store directly) with pruning enabled (e.g. keep-recent=1000).
  2. Commit many versions and call PruneStores past the retained window each round.
  3. Iterate the metadata DB over the numeric commit-info range ["s/0", "s/:") and count keys.
  4. Observe the count equals the number of committed heights (minus none) — it never decreases, even though IAVL node pruning does reclaim space and s/earliest advances.

Minimal assertion (store/v2 rootmulti package):

go
db := dbm.NewMemDB()
ms := newMultiStoreWithMounts(db, pruningtypes.NewPruningOptions(pruningtypes.PruningDefault))
require.NoError(t, ms.LoadLatestVersion())
for i := 0; i < 1000; i++ { ms.Commit() }
ms.PruneStores(950) // retains a small window of IAVL versions
// BUG: ~1000 commit-info records remain; none below 950 were deleted.

Relevant source (store/[email protected] rootmulti/store.go)

  • commitInfoKeyFmt = "s/%d"
  • flushCommitInfo(batch, version, cInfo) — writes s/<version> every Commit()
  • PruneStores(pruningHeight) — prunes only StoreTypeIAVL substores via DeleteVersionsTo, updates s/earliest, then returns. No commit-info deletion.

Expected behavior

PruneStores should also delete commit-info records for versions below the pruning height (equivalently, below s/earliest). Those heights are already unqueryable after pruning, and the app hash is derived from the current commit-info only, so deleting historical commit-info is consensus-safe.

PR

I’m preparing a PR with a bounded pruneCommitInfo fix and tests, and will link it here once opened.