`rootmulti.PruneStores` prunes IAVL versions but never deletes commit-info records (`s/<version>`) — unbounded metadata DB growth
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— identicalmain— 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):
PruneStores: https://github.com/cosmos/cosmos-sdk/blob/main/store/rootmulti/store.go#L678-L727flushCommitInfo: https://github.com/cosmos/cosmos-sdk/blob/main/store/rootmulti/store.go#L1268commitInfoKeyFmt: https://github.com/cosmos/cosmos-sdk/blob/main/store/rootmulti/store.go#L37
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.db≈ 1.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
- Start a node (or a
rootmultistore directly) with pruning enabled (e.g.keep-recent=1000). - Commit many versions and call
PruneStorespast the retained window each round. - Iterate the metadata DB over the numeric commit-info range
["s/0", "s/:")and count keys. - Observe the count equals the number of committed heights (minus none) — it never decreases, even though IAVL node pruning does reclaim space and
s/earliestadvances.
Minimal assertion (store/v2 rootmulti package):
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)— writess/<version>everyCommit()PruneStores(pruningHeight)— prunes onlyStoreTypeIAVLsubstores viaDeleteVersionsTo, updatess/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.
Source: cosmos/cosmos-sdk