Skip to content

Commit ae80efb

Browse files
Cordtustechnicallytyaljo242
authored
fix(grpc): return actual earliest_store_height in node.Status endpoint (#25647)
Co-authored-by: Tyler <48813565+technicallyty@users.noreply.github.com> Co-authored-by: Alex | Cosmos Labs <alex@cosmoslabs.io>
1 parent 9e75197 commit ae80efb

11 files changed

Lines changed: 275 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
9090

9191
### Bug Fixes
9292

93+
* (grpc) [#25647](https://github.com/cosmos/cosmos-sdk/pull/25647) Return actual `earliest_store_height` in `node.Status` gRPC endpoint instead of hardcoded `0`.
9394
* (types/query) [#25665](https://github.com/cosmos/cosmos-sdk/issues/25665) Fix pagination offset when querying a collection with predicate function.
9495
* (x/staking) [#25649](https://github.com/cosmos/cosmos-sdk/pull/25649) Add missing `defer iterator.Close()` calls in `IterateDelegatorRedelegations` and `GetRedelegations` to prevent resource leaks.
9596
* (mempool) [#25563](https://github.com/cosmos/cosmos-sdk/pull/25563) Cleanup sender indices in case of tx replacement.

client/grpc/node/service.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
)
1313

1414
// RegisterNodeService registers the node gRPC service on the provided gRPC router.
15-
func RegisterNodeService(clientCtx client.Context, server gogogrpc.Server, cfg config.Config) {
16-
RegisterServiceServer(server, NewQueryServer(clientCtx, cfg))
15+
func RegisterNodeService(clientCtx client.Context, server gogogrpc.Server, cfg config.Config, earliestStoreHeightFn func() int64) {
16+
RegisterServiceServer(server, NewQueryServer(clientCtx, cfg, earliestStoreHeightFn))
1717
}
1818

1919
// RegisterGRPCGatewayRoutes mounts the node gRPC service's GRPC-gateway routes
@@ -25,14 +25,16 @@ func RegisterGRPCGatewayRoutes(clientConn gogogrpc.ClientConn, mux *runtime.Serv
2525
var _ ServiceServer = queryServer{}
2626

2727
type queryServer struct {
28-
clientCtx client.Context
29-
cfg config.Config
28+
clientCtx client.Context
29+
cfg config.Config
30+
earliestStoreHeightFn func() int64
3031
}
3132

32-
func NewQueryServer(clientCtx client.Context, cfg config.Config) ServiceServer {
33+
func NewQueryServer(clientCtx client.Context, cfg config.Config, earliestStoreHeightFn func() int64) ServiceServer {
3334
return queryServer{
34-
clientCtx: clientCtx,
35-
cfg: cfg,
35+
clientCtx: clientCtx,
36+
cfg: cfg,
37+
earliestStoreHeightFn: earliestStoreHeightFn,
3638
}
3739
}
3840

@@ -53,13 +55,10 @@ func (s queryServer) Status(ctx context.Context, _ *StatusRequest) (*StatusRespo
5355
blockTime := sdkCtx.BlockTime()
5456

5557
return &StatusResponse{
56-
// TODO: Get earliest version from store.
57-
//
58-
// Ref: ...
59-
// EarliestStoreHeight: sdkCtx.MultiStore(),
60-
Height: uint64(sdkCtx.BlockHeight()),
61-
Timestamp: &blockTime,
62-
AppHash: sdkCtx.BlockHeader().AppHash,
63-
ValidatorHash: sdkCtx.BlockHeader().NextValidatorsHash,
58+
EarliestStoreHeight: uint64(s.earliestStoreHeightFn()),
59+
Height: uint64(sdkCtx.BlockHeight()),
60+
Timestamp: &blockTime,
61+
AppHash: sdkCtx.BlockHeader().AppHash,
62+
ValidatorHash: sdkCtx.BlockHeader().NextValidatorsHash,
6463
}, nil
6564
}

client/grpc/node/service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestServiceServer_Config(t *testing.T) {
1515
defaultCfg.PruningKeepRecent = "2000"
1616
defaultCfg.PruningInterval = "10"
1717
defaultCfg.HaltHeight = 100
18-
svr := NewQueryServer(client.Context{}, *defaultCfg)
18+
svr := NewQueryServer(client.Context{}, *defaultCfg, func() int64 { return 1 })
1919
ctx := sdk.Context{}.WithMinGasPrices(sdk.NewDecCoins(sdk.NewInt64DecCoin("stake", 15)))
2020

2121
resp, err := svr.Config(ctx, &ConfigRequest{})

enterprise/poa/simapp/app.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,5 +475,8 @@ func (app *SimApp) RegisterTendermintService(clientCtx client.Context) {
475475
}
476476

477477
func (app *SimApp) RegisterNodeService(clientCtx client.Context, cfg config.Config) {
478-
nodeservice.RegisterNodeService(clientCtx, app.GRPCQueryRouter(), cfg)
478+
earliestHeightFn := func() int64 {
479+
return app.CommitMultiStore().EarliestVersion()
480+
}
481+
nodeservice.RegisterNodeService(clientCtx, app.GRPCQueryRouter(), cfg, earliestHeightFn)
479482
}

runtime/app.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,9 @@ func (a *App) RegisterTendermintService(clientCtx client.Context) {
225225

226226
// RegisterNodeService registers the node gRPC service on the app gRPC router.
227227
func (a *App) RegisterNodeService(clientCtx client.Context, cfg config.Config) {
228-
nodeservice.RegisterNodeService(clientCtx, a.GRPCQueryRouter(), cfg)
228+
nodeservice.RegisterNodeService(clientCtx, a.GRPCQueryRouter(), cfg, func() int64 {
229+
return a.CommitMultiStore().EarliestVersion()
230+
})
229231
}
230232

231233
// Configurator returns the app's configurator.

server/mock/store.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ func (ms multiStore) LatestVersion() int64 {
172172
panic("not implemented")
173173
}
174174

175+
func (ms multiStore) EarliestVersion() int64 {
176+
panic("not implemented")
177+
}
178+
175179
func (ms multiStore) WorkingHash() []byte {
176180
panic("not implemented")
177181
}

simapp/app.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,9 @@ func (app *SimApp) RegisterTendermintService(clientCtx client.Context) {
828828
}
829829

830830
func (app *SimApp) RegisterNodeService(clientCtx client.Context, cfg config.Config) {
831-
nodeservice.RegisterNodeService(clientCtx, app.GRPCQueryRouter(), cfg)
831+
nodeservice.RegisterNodeService(clientCtx, app.GRPCQueryRouter(), cfg, func() int64 {
832+
return app.CommitMultiStore().EarliestVersion()
833+
})
832834
}
833835

834836
// GetMaccPerms returns a copy of the module account permissions

store/rootmulti/store.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ import (
3535
)
3636

3737
const (
38-
latestVersionKey = "s/latest"
39-
commitInfoKeyFmt = "s/%d" // s/<version>
38+
latestVersionKey = "s/latest"
39+
earliestVersionKey = "s/earliest"
40+
commitInfoKeyFmt = "s/%d" // s/<version>
4041
)
4142

4243
const iavlDisablefastNodeDefault = false
@@ -455,6 +456,11 @@ func (rs *Store) LatestVersion() int64 {
455456
return rs.LastCommitID().Version
456457
}
457458

459+
// EarliestVersion returns the earliest version in the store
460+
func (rs *Store) EarliestVersion() int64 {
461+
return GetEarliestVersion(rs.db)
462+
}
463+
458464
// LastCommitID implements Committer/CommitStore.
459465
func (rs *Store) LastCommitID() types.CommitID {
460466
info := rs.lastCommitInfo.Load()
@@ -752,6 +758,23 @@ func (rs *Store) PruneStores(pruningHeight int64) (err error) {
752758

753759
rs.logger.Error("failed to prune store", "key", key, "err", err)
754760
}
761+
762+
// Update earliest version after successful pruning.
763+
// The new earliest available version is pruningHeight + 1.
764+
// Only persist if newer than current earliest - this handles state sync
765+
// scenarios and avoids issues if pruning config changes result in a
766+
// lower pruning height than previously persisted.
767+
newEarliest := pruningHeight + 1
768+
currentEarliest := GetEarliestVersion(rs.db)
769+
if newEarliest > currentEarliest {
770+
batch := rs.db.NewBatch()
771+
defer batch.Close()
772+
flushEarliestVersion(batch, newEarliest)
773+
if err := batch.WriteSync(); err != nil {
774+
rs.logger.Error("failed to persist earliest version", "err", err)
775+
}
776+
}
777+
755778
return nil
756779
}
757780

@@ -1222,6 +1245,36 @@ func GetLatestVersion(db dbm.DB) int64 {
12221245
return latestVersion
12231246
}
12241247

1248+
// GetEarliestVersion returns the earliest version stored in the database.
1249+
// Returns 1 if no earliest version has been explicitly set (unpruned chain).
1250+
func GetEarliestVersion(db dbm.DB) int64 {
1251+
bz, err := db.Get([]byte(earliestVersionKey))
1252+
if err != nil {
1253+
panic(err)
1254+
} else if bz == nil {
1255+
return 1 // default to 1 for unpruned chains
1256+
}
1257+
1258+
var earliestVersion int64
1259+
1260+
if err := gogotypes.StdInt64Unmarshal(&earliestVersion, bz); err != nil {
1261+
panic(err)
1262+
}
1263+
1264+
return earliestVersion
1265+
}
1266+
1267+
func flushEarliestVersion(batch dbm.Batch, version int64) {
1268+
bz, err := gogotypes.StdInt64Marshal(version)
1269+
if err != nil {
1270+
panic(err)
1271+
}
1272+
1273+
if err := batch.Set([]byte(earliestVersionKey), bz); err != nil {
1274+
panic(err)
1275+
}
1276+
}
1277+
12251278
// commitStores commits each store and returns a new commitInfo.
12261279
func commitStores(version int64, storeMap map[types.StoreKey]types.CommitStore, removalMap map[types.StoreKey]bool) *types.CommitInfo {
12271280
storeInfos := make([]types.StoreInfo, 0, len(storeMap))

store/rootmulti/store_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,3 +1169,77 @@ func TestCommitStores(t *testing.T) {
11691169
})
11701170
}
11711171
}
1172+
1173+
func TestEarliestVersion(t *testing.T) {
1174+
db := dbm.NewMemDB()
1175+
ms := newMultiStoreWithMounts(db, pruningtypes.NewPruningOptions(pruningtypes.PruningNothing))
1176+
require.NoError(t, ms.LoadLatestVersion())
1177+
1178+
// Initially, earliest version should be 1 (default for unpruned chains)
1179+
require.Equal(t, int64(1), ms.EarliestVersion())
1180+
1181+
// Commit some versions
1182+
for i := 0; i < 5; i++ {
1183+
ms.Commit()
1184+
}
1185+
1186+
// Earliest version should still be 1
1187+
require.Equal(t, int64(1), ms.EarliestVersion())
1188+
require.Equal(t, int64(5), ms.LatestVersion())
1189+
}
1190+
1191+
func TestEarliestVersionWithPruning(t *testing.T) {
1192+
db := dbm.NewMemDB()
1193+
// keepRecent=2, interval=1 means prune aggressively
1194+
ms := newMultiStoreWithMounts(db, pruningtypes.NewCustomPruningOptions(2, 1))
1195+
require.NoError(t, ms.LoadLatestVersion())
1196+
1197+
// Initially, earliest version should be 1
1198+
require.Equal(t, int64(1), ms.EarliestVersion())
1199+
1200+
// Commit enough versions to trigger pruning
1201+
for i := 0; i < 10; i++ {
1202+
ms.Commit()
1203+
}
1204+
1205+
// Wait for async pruning to complete and check earliest version is updated
1206+
checkEarliest := func() bool {
1207+
return ms.EarliestVersion() > 1
1208+
}
1209+
require.Eventually(t, checkEarliest, 1*time.Second, 10*time.Millisecond,
1210+
"expected earliest version to be updated after pruning")
1211+
1212+
// Earliest version should now be greater than 1 (pruned heights + 1)
1213+
earliest := ms.EarliestVersion()
1214+
require.Greater(t, earliest, int64(1), "earliest version should be updated after pruning")
1215+
1216+
// Latest should still be 10
1217+
require.Equal(t, int64(10), ms.LatestVersion())
1218+
}
1219+
1220+
func TestEarliestVersionPersistence(t *testing.T) {
1221+
db := dbm.NewMemDB()
1222+
ms := newMultiStoreWithMounts(db, pruningtypes.NewCustomPruningOptions(2, 1))
1223+
require.NoError(t, ms.LoadLatestVersion())
1224+
1225+
// Commit and prune
1226+
for i := 0; i < 10; i++ {
1227+
ms.Commit()
1228+
}
1229+
1230+
// Wait for pruning
1231+
checkEarliest := func() bool {
1232+
return ms.EarliestVersion() > 1
1233+
}
1234+
require.Eventually(t, checkEarliest, 1*time.Second, 10*time.Millisecond)
1235+
1236+
earliestBeforeRestart := ms.EarliestVersion()
1237+
1238+
// "Restart" by creating new store with same db
1239+
ms2 := newMultiStoreWithMounts(db, pruningtypes.NewCustomPruningOptions(2, 1))
1240+
require.NoError(t, ms2.LoadLatestVersion())
1241+
1242+
// Earliest version should be persisted and restored
1243+
require.Equal(t, earliestBeforeRestart, ms2.EarliestVersion(),
1244+
"earliest version should persist across restarts")
1245+
}

store/types/store.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ type CommitMultiStore interface {
159159
MultiStore
160160
snapshottypes.Snapshotter
161161

162+
// EarliestVersion returns the earliest version in the store
163+
EarliestVersion() int64
164+
162165
// Mount a store of type using the given db.
163166
// If db == nil, the new store will use the CommitMultiStore db.
164167
MountStoreWithDB(key StoreKey, typ StoreType, db dbm.DB)

0 commit comments

Comments
 (0)