Skip to content

Commit b05bc57

Browse files
authored
fix(snapshot): capture DB latest before LoadVersion in loadAndTruncate (PLA-132) (#91)
LoadVersion(n) sets lastCommitInfo.Version=n, causing LatestVersion() to return n regardless of what is in the DB. Reading LatestVersion() after LoadVersion made the rollback guard permanently false, so RollbackToVersion was never called. Fix: capture CommitMultiStore().LatestVersion() before LoadVersion, when lastCommitInfo is nil and rootmulti falls through to GetLatestVersion(db).
1 parent f83b6a7 commit b05bc57

2 files changed

Lines changed: 94 additions & 4 deletions

File tree

run/prune_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,99 @@ package run
55
import (
66
"testing"
77

8+
storetypes "cosmossdk.io/store/types"
9+
abci "github.com/cometbft/cometbft/abci/types"
810
cmttypes "github.com/cometbft/cometbft/types"
911
dbm "github.com/cosmos/cosmos-db"
1012
"github.com/cosmos/cosmos-sdk/client"
1113
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
1214
"github.com/stretchr/testify/require"
1315

16+
"github.com/altuslabsxyz/blockstm-sim/compare"
1417
"github.com/altuslabsxyz/blockstm-sim/sdkhook"
1518
)
1619

20+
// stubCMS is a minimal CommitMultiStore for testing loadAndTruncate in isolation.
21+
// It simulates the rootmulti behaviour where LatestVersion() reads from the DB
22+
// (dbLatest) before LoadVersion is called, and from lastCommitInfo (loaded) after.
23+
type stubCMS struct {
24+
storetypes.CommitMultiStore
25+
dbLatest int64 // returned before loadCalled; simulates GetLatestVersion(db)
26+
loaded int64 // returned after loadCalled; simulates lastCommitInfo.Version
27+
loadCalled bool
28+
rolledBack int64 // last RollbackToVersion target, 0 if never called
29+
}
30+
31+
func (s *stubCMS) LatestVersion() int64 {
32+
if !s.loadCalled {
33+
return s.dbLatest
34+
}
35+
return s.loaded
36+
}
37+
38+
func (s *stubCMS) RollbackToVersion(version int64) error {
39+
s.rolledBack = version
40+
s.loaded = version
41+
return nil
42+
}
43+
44+
// stubApp implements sdkhook.App using stubCMS.
45+
// Only CommitMultiStore and LoadVersion are used by loadAndTruncate.
46+
type stubApp struct{ cms *stubCMS }
47+
48+
func (a *stubApp) CommitMultiStore() storetypes.CommitMultiStore { return a.cms }
49+
func (a *stubApp) LoadVersion(v int64) error {
50+
a.cms.loadCalled = true
51+
a.cms.loaded = v
52+
return nil
53+
}
54+
func (a *stubApp) FinalizeBlock(*abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) {
55+
panic("not used")
56+
}
57+
func (a *stubApp) Commit() (*abci.ResponseCommit, error) { panic("not used") }
58+
func (a *stubApp) SetLifecycleObserver(compare.LifecycleObserver) {}
59+
func (a *stubApp) SetBlockSTMTxRunner(sdkhook.STMRunner) {}
60+
func (a *stubApp) UnsetBlockSTMTxRunner() {}
61+
func (a *stubApp) SetDisableBlockGasMeter(bool) {}
62+
func (a *stubApp) GetStoreKeys() []storetypes.StoreKey { return nil }
63+
64+
// TestLoadAndTruncate_RollsBackWhenDBAboveTarget is the regression test for
65+
// the bug where LoadVersion(n) sets LatestVersion()=n, causing the
66+
// `LatestVersion() > n` guard to always be false and skipping RollbackToVersion.
67+
func TestLoadAndTruncate_RollsBackWhenDBAboveTarget(t *testing.T) {
68+
// DB has version 100 (production node committed up to 100).
69+
// Target is version 50 (we want to truncate to 50).
70+
// Before fix: LoadVersion(50) → LatestVersion()=50 → 50>50=false → no rollback.
71+
// After fix: dbLatest=100 captured before LoadVersion → 100>50 → rollback.
72+
cms := &stubCMS{dbLatest: 100}
73+
app := &stubApp{cms: cms}
74+
75+
require.NoError(t, loadAndTruncate(app, 50))
76+
77+
require.Equal(t, int64(50), cms.rolledBack, "RollbackToVersion must be called with target version")
78+
require.Equal(t, int64(50), cms.LatestVersion())
79+
}
80+
81+
func TestLoadAndTruncate_SkipsRollbackWhenAlreadyAtTarget(t *testing.T) {
82+
// DB is pre-pruned: dbLatest already equals loadVersion.
83+
cms := &stubCMS{dbLatest: 50}
84+
app := &stubApp{cms: cms}
85+
86+
require.NoError(t, loadAndTruncate(app, 50))
87+
88+
require.Equal(t, int64(0), cms.rolledBack, "RollbackToVersion must NOT be called when DB is already at target")
89+
}
90+
91+
func TestLoadAndTruncate_SkipsRollbackForZeroVersion(t *testing.T) {
92+
// loadVersion=0 (meta.Start==1 case); rootmulti rejects RollbackToVersion(0).
93+
cms := &stubCMS{dbLatest: 0}
94+
app := &stubApp{cms: cms}
95+
96+
require.NoError(t, loadAndTruncate(app, 0))
97+
98+
require.Equal(t, int64(0), cms.rolledBack)
99+
}
100+
17101
// testPruneFactory returns an AppFactory suitable for PruneSnapshot calls in
18102
// tests. Full round-trip pruning behaviour (bootstrap → prune → reload) is
19103
// exercised by chain-side integration tests against real `blockstm-sim extract`

run/snapshot_executor.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,21 @@ func (e *SnapshotExecutor) InitFromState(snapshotDir string, meta compare.RangeM
189189
// the latest version (which is now loadVersion, since everything above it
190190
// has been deleted).
191191
func loadAndTruncate(app sdkhook.App, loadVersion int64) error {
192+
// Capture the actual DB-committed latest version BEFORE calling LoadVersion.
193+
// rootmulti.Store.LatestVersion() reads lastCommitInfo.Version, but when
194+
// lastCommitInfo is nil (pre-load) it falls through to GetLatestVersion(db)
195+
// — the true on-disk latest. After LoadVersion(n), lastCommitInfo.Version
196+
// is set to n regardless of what else is in the DB, so reading here would
197+
// always return n and make the rollback guard below permanently false.
198+
dbLatest := app.CommitMultiStore().LatestVersion()
199+
192200
if err := app.LoadVersion(loadVersion); err != nil {
193201
return fmt.Errorf("LoadVersion: %w", err)
194202
}
195203
// rootmulti.Store.RollbackToVersion rejects target <= 0. When loadVersion
196204
// is 0 (meta.Start == 1) there is nothing above to prune anyway, so skip.
197-
// Also skip when the store's latest committed version already equals
198-
// loadVersion — the DB has been pre-pruned (e.g. by PruneSnapshot) and
199-
// RollbackToVersion would be a no-op scan.
200-
if loadVersion > 0 && app.CommitMultiStore().LatestVersion() > loadVersion {
205+
// Also skip when the DB was already at loadVersion (pre-pruned by PruneSnapshot).
206+
if loadVersion > 0 && dbLatest > loadVersion {
201207
if err := app.CommitMultiStore().RollbackToVersion(loadVersion); err != nil {
202208
return fmt.Errorf("RollbackToVersion: %w", err)
203209
}

0 commit comments

Comments
 (0)