Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dc48552
feat(grpc): add earliest/latest block height to GetSyncing response
Cordtus Dec 5, 2025
117246f
Merge branch 'main' into feat/earliest-block-height-grpc
aljo242 Dec 8, 2025
816e19e
Merge branch 'main' into feat/earliest-block-height-grpc
aljo242 Dec 8, 2025
faec5c1
fix: ensure testnetApp cleanup on error in testnetify (#25660)
MrEeeeet111 Dec 9, 2025
6019eac
build(deps): Bump golang.org/x/crypto from 0.45.0 to 0.46.0 (#25659)
dependabot[bot] Dec 9, 2025
f1b2e5d
test(systemtests): add gRPC systemtest for GetSyncing block heights
Cordtus Dec 10, 2025
4b70a11
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Dec 10, 2025
854e787
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Dec 16, 2025
24a40a4
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Dec 17, 2025
b3df0b6
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Dec 18, 2025
6c26fb8
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Jan 7, 2026
27996bb
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Jan 7, 2026
7df959e
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Jan 11, 2026
2c4b53b
Merge branch 'main' into feat/earliest-block-height-grpc
Cordtus Jan 21, 2026
91ad3c7
Merge branch 'main' into feat/earliest-block-height-grpc
aljo242 Jan 23, 2026
0c189ac
Merge branch 'main' into feat/earliest-block-height-grpc
technicallyty Jan 23, 2026
70720a4
docs: add changelog entry for GetSyncing block height fields
Cordtus Jan 24, 2026
326eccd
chore: go mod tidy
Cordtus Jan 24, 2026
551eec6
Merge branch 'main' into feat/earliest-block-height-grpc
aljo242 Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (blockstm) [#25600](https://github.com/cosmos/cosmos-sdk/pull/25600) Allow dynamic retrieval of the coin denomination from multi store at runtime.
* [#25516](https://github.com/cosmos/cosmos-sdk/pull/25516) Support automatic configuration of OpenTelemetry via [OpenTelemetry declarative configuration](https://pkg.go.dev/go.opentelemetry.io/contrib/otelconf) and add OpenTelemetry instrumentation of `BaseApp`.
* [#25745](https://github.com/cosmos/cosmos-sdk/pull/25745) Add DiskIO telemetry via gopsutil.
* (grpc) [#25648](https://github.com/cosmos/cosmos-sdk/pull/25648) Add `earliest_block_height` and `latest_block_height` fields to `GetSyncingResponse`.

### Improvements

Expand Down
259 changes: 166 additions & 93 deletions client/grpc/cmtservice/query.pb.go

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion client/grpc/cmtservice/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ func (s queryServer) GetSyncing(ctx context.Context, _ *GetSyncingRequest) (*Get
}

return &GetSyncingResponse{
Syncing: status.SyncInfo.CatchingUp,
Syncing: status.SyncInfo.CatchingUp,
EarliestBlockHeight: status.SyncInfo.EarliestBlockHeight,
LatestBlockHeight: status.SyncInfo.LatestBlockHeight,
}, nil
}

Expand Down
38 changes: 38 additions & 0 deletions client/grpc/cmtservice/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cmtservice

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestGetSyncingResponseFields(t *testing.T) {
// Verify the struct has the expected fields
resp := &GetSyncingResponse{
Syncing: true,
EarliestBlockHeight: 1000,
LatestBlockHeight: 2000,
}

require.True(t, resp.GetSyncing())
require.Equal(t, int64(1000), resp.GetEarliestBlockHeight())
require.Equal(t, int64(2000), resp.GetLatestBlockHeight())
}

func TestGetSyncingResponseDefaults(t *testing.T) {
// Verify zero values work correctly
resp := &GetSyncingResponse{}

require.False(t, resp.GetSyncing())
require.Equal(t, int64(0), resp.GetEarliestBlockHeight())
require.Equal(t, int64(0), resp.GetLatestBlockHeight())
}

func TestGetSyncingResponseNil(t *testing.T) {
// Verify nil receiver doesn't panic
var resp *GetSyncingResponse

require.False(t, resp.GetSyncing())
require.Equal(t, int64(0), resp.GetEarliestBlockHeight())
require.Equal(t, int64(0), resp.GetLatestBlockHeight())
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
cosmossdk.io/core v0.11.3
cosmossdk.io/depinject v1.2.1
cosmossdk.io/errors v1.0.2
cosmossdk.io/log v1.3.1
cosmossdk.io/log/v2 v2.0.0
cosmossdk.io/math v1.5.3
cosmossdk.io/store v1.3.0-beta.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ cosmossdk.io/depinject v1.2.1 h1:eD6FxkIjlVaNZT+dXTQuwQTKZrFZ4UrfCq1RKgzyhMw=
cosmossdk.io/depinject v1.2.1/go.mod h1:lqQEycz0H2JXqvOgVwTsjEdMI0plswI7p6KX+MVqFOM=
cosmossdk.io/errors v1.0.2 h1:wcYiJz08HThbWxd/L4jObeLaLySopyyuUFB5w4AGpCo=
cosmossdk.io/errors v1.0.2/go.mod h1:0rjgiHkftRYPj//3DrD6y8hcm40HcPv/dR4R/4efr0k=
cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI=
cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM=
cosmossdk.io/math v1.5.3 h1:WH6tu6Z3AUCeHbeOSHg2mt9rnoiUWVWaQ2t6Gkll96U=
cosmossdk.io/math v1.5.3/go.mod h1:uqcZv7vexnhMFJF+6zh9EWdm/+Ylyln34IvPnBauPCQ=
cosmossdk.io/schema v1.1.0 h1:mmpuz3dzouCoyjjcMcA/xHBEmMChN+EHh8EHxHRHhzE=
Expand Down
4 changes: 4 additions & 0 deletions proto/cosmos/base/tendermint/v1beta1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ message GetSyncingRequest {}
// GetSyncingResponse is the response type for the Query/GetSyncing RPC method.
message GetSyncingResponse {
bool syncing = 1;
// earliest_block_height is the earliest block height available on this node.
int64 earliest_block_height = 2;
// latest_block_height is the latest block height available on this node.
int64 latest_block_height = 3;
}

// GetNodeInfoRequest is the request type for the Query/GetNodeInfo RPC method.
Expand Down
233 changes: 233 additions & 0 deletions tests/systemtests/cometbft_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//go:build system_test

package systemtests

import (
"context"
"fmt"
"path/filepath"
"strconv"
"testing"

"github.com/creachadair/tomledit"
"github.com/creachadair/tomledit/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/sjson"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"cosmossdk.io/systemtests"

"github.com/cosmos/cosmos-sdk/client/grpc/cmtservice"
)

// TestCometBFTGetSyncingGRPC tests the GetSyncing gRPC endpoint
// to verify it returns the expected earliest_block_height and latest_block_height fields.
// This test validates the feature added in PR #25647.
func TestCometBFTGetSyncingGRPC(t *testing.T) {
sut := systemtests.Sut
sut.ResetChain(t)

sut.StartChain(t)

// Wait for a few blocks to be produced
sut.AwaitNBlocks(t, 3)

// Connect to gRPC endpoint
grpcAddr := fmt.Sprintf("localhost:%d", 9090) // DefaultGrpcPort
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
defer conn.Close()

queryClient := cmtservice.NewServiceClient(conn)

// Test that the GetSyncing gRPC endpoint returns all expected fields
t.Run("gRPC GetSyncing returns block heights", func(t *testing.T) {
resp, err := queryClient.GetSyncing(context.Background(), &cmtservice.GetSyncingRequest{})
require.NoError(t, err)
require.NotNil(t, resp)

t.Logf("gRPC GetSyncing response: syncing=%v, earliest=%d, latest=%d",
resp.Syncing, resp.EarliestBlockHeight, resp.LatestBlockHeight)

// Verify earliest_block_height is a valid height (>= 1)
assert.GreaterOrEqual(t, resp.EarliestBlockHeight, int64(1),
"earliest_block_height should be >= 1, got %d", resp.EarliestBlockHeight)

// Verify latest_block_height is a valid height (>= 1)
assert.GreaterOrEqual(t, resp.LatestBlockHeight, int64(1),
"latest_block_height should be >= 1, got %d", resp.LatestBlockHeight)

// Verify latest >= earliest (invariant)
assert.GreaterOrEqual(t, resp.LatestBlockHeight, resp.EarliestBlockHeight,
"latest_block_height (%d) should be >= earliest_block_height (%d)",
resp.LatestBlockHeight, resp.EarliestBlockHeight)
})

// Test that latest_block_height increases as chain progresses
t.Run("gRPC latest height increases over time", func(t *testing.T) {
// Get initial height
resp1, err := queryClient.GetSyncing(context.Background(), &cmtservice.GetSyncingRequest{})
require.NoError(t, err)
initialLatest := resp1.LatestBlockHeight
t.Logf("Initial latest_block_height: %d", initialLatest)

// Wait for more blocks
sut.AwaitNBlocks(t, 2)

// Get new height
resp2, err := queryClient.GetSyncing(context.Background(), &cmtservice.GetSyncingRequest{})
require.NoError(t, err)
newLatest := resp2.LatestBlockHeight
t.Logf("New latest_block_height: %d", newLatest)

assert.Greater(t, newLatest, initialLatest,
"latest_block_height should increase over time (was %d, now %d)",
initialLatest, newLatest)
})

// Test that earliest_block_height remains stable on an unpruned chain
t.Run("gRPC earliest height stable on unpruned chain", func(t *testing.T) {
// Get initial earliest height
resp1, err := queryClient.GetSyncing(context.Background(), &cmtservice.GetSyncingRequest{})
require.NoError(t, err)
initialEarliest := resp1.EarliestBlockHeight
t.Logf("Initial earliest_block_height: %d", initialEarliest)

// Wait for more blocks
sut.AwaitNBlocks(t, 2)

// Get earliest height again
resp2, err := queryClient.GetSyncing(context.Background(), &cmtservice.GetSyncingRequest{})
require.NoError(t, err)
currentEarliest := resp2.EarliestBlockHeight
t.Logf("Current earliest_block_height: %d", currentEarliest)

// On an unpruned chain, earliest should remain at 1 or the initial value
assert.Equal(t, initialEarliest, currentEarliest,
"earliest_block_height should remain stable on unpruned chain (was %d, now %d)",
initialEarliest, currentEarliest)
})
}

// TestCometBFTGetSyncingWithBlockRetention tests that earliest_block_height
// increases when block retention (min-retain-blocks) is configured.
// This test configures aggressive pruning settings to verify the feature works.
func TestCometBFTGetSyncingWithBlockRetention(t *testing.T) {
const minRetainBlocks = 5

sut := systemtests.Sut

// Modify genesis to set a low max_age_num_blocks for evidence
// Block pruning retention height = min(commitHeight-minRetainBlocks, commitHeight-maxAgeNumBlocks, ...)
// Default max_age_num_blocks is 100000, which would prevent pruning in tests
sut.ModifyGenesisJSON(t, func(genesis []byte) []byte {
state, err := sjson.Set(string(genesis), "consensus.params.evidence.max_age_num_blocks", strconv.Itoa(minRetainBlocks))
require.NoError(t, err)
return []byte(state)
})

// Configure min-retain-blocks and pruning in app.toml before starting the chain
configureBlockPruning(t, sut, minRetainBlocks)

sut.StartChain(t)

// Connect to gRPC endpoint
grpcAddr := fmt.Sprintf("localhost:%d", 9090)
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
defer conn.Close()

queryClient := cmtservice.NewServiceClient(conn)

// Get initial state
resp, err := queryClient.GetSyncing(context.Background(), &cmtservice.GetSyncingRequest{})
require.NoError(t, err)
initialEarliest := resp.EarliestBlockHeight
initialLatest := resp.LatestBlockHeight
t.Logf("Initial state: earliest=%d, latest=%d", initialEarliest, initialLatest)

// Verify the response structure is correct
require.GreaterOrEqual(t, initialEarliest, int64(1), "earliest_block_height should be >= 1")
require.GreaterOrEqual(t, initialLatest, int64(1), "latest_block_height should be >= 1")
require.GreaterOrEqual(t, initialLatest, initialEarliest, "latest >= earliest invariant")

// Wait for enough blocks that pruning should definitely occur
// Need to wait for more than minRetainBlocks to trigger pruning
blocksToWait := minRetainBlocks * 3
t.Logf("Waiting for %d blocks to trigger block pruning (minRetainBlocks=%d)...", blocksToWait, minRetainBlocks)
sut.AwaitNBlocks(t, int64(blocksToWait))

// Check state after waiting - pruning should have occurred
resp, err = queryClient.GetSyncing(context.Background(), &cmtservice.GetSyncingRequest{})
require.NoError(t, err)
newEarliest := resp.EarliestBlockHeight
newLatest := resp.LatestBlockHeight
t.Logf("After %d blocks: earliest=%d, latest=%d", blocksToWait, newEarliest, newLatest)

// Verify latest has increased (this should always be true)
assert.Greater(t, newLatest, initialLatest,
"latest_block_height should have increased")

// Verify earliest has increased due to block pruning
// With minRetainBlocks=5 and maxAgeNumBlocks=5, pruning should occur after ~5 blocks
assert.Greater(t, newEarliest, initialEarliest,
"earliest_block_height should increase when blocks are pruned (minRetainBlocks=%d, was %d, now %d)",
minRetainBlocks, initialEarliest, newEarliest)

// Verify the block range is bounded by our retention settings
blockDiff := newLatest - newEarliest
t.Logf("Block range: %d (latest %d - earliest %d)", blockDiff, newLatest, newEarliest)

// The block range should be close to minRetainBlocks (with some tolerance)
// Allow extra tolerance since pruning timing isn't exact
maxExpectedRange := int64(minRetainBlocks + 5)
assert.LessOrEqual(t, blockDiff, maxExpectedRange,
"block range should be close to minRetainBlocks (%d), got %d",
minRetainBlocks, blockDiff)

// Verify invariant still holds
assert.GreaterOrEqual(t, newLatest, newEarliest,
"latest >= earliest invariant should hold after pruning")
}

// configureBlockPruning edits the app.toml for all nodes to enable block pruning
// This requires setting min-retain-blocks, state pruning, and disabling state sync snapshots
func configureBlockPruning(t *testing.T, sut *systemtests.SystemUnderTest, minRetainBlocks int) {
t.Helper()

// Edit app.toml for each node
for i := 0; i < sut.NodesCount(); i++ {
// NodeDir already includes WorkDir, so just append config/app.toml
appTomlPath := filepath.Join(sut.NodeDir(i), "config", "app.toml")
systemtests.EditToml(appTomlPath, func(doc *tomledit.Document) {
// Set min-retain-blocks for CometBFT block pruning
setInt(doc, minRetainBlocks, "min-retain-blocks")
// Use "everything" pruning which is the most aggressive and doesn't require
// custom interval settings (pruning-interval minimum is 10)
setString(doc, "everything", "pruning")
// Disable state sync snapshots (0 = disabled) - required for "everything" pruning
setInt(doc, 0, "state-sync", "snapshot-interval")
})
t.Logf("Configured block pruning (min-retain-blocks=%d, pruning=everything) in %s", minRetainBlocks, appTomlPath)
}
}

// setInt sets an integer value in a toml document
func setInt(doc *tomledit.Document, newVal int, xpath ...string) {
e := doc.First(xpath...)
if e == nil {
panic(fmt.Sprintf("not found: %v", xpath))
}
e.Value = parser.MustValue(strconv.Itoa(newVal))
}

// setString sets a quoted string value in a toml document
func setString(doc *tomledit.Document, newVal string, xpath ...string) {
e := doc.First(xpath...)
if e == nil {
panic(fmt.Sprintf("not found: %v", xpath))
}
e.Value = parser.MustValue(fmt.Sprintf("%q", newVal))
}
4 changes: 2 additions & 2 deletions tests/systemtests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ require (
cosmossdk.io/math v1.5.3
cosmossdk.io/systemtests v1.2.1
github.com/cosmos/cosmos-sdk v0.54.0-beta.0
github.com/creachadair/tomledit v0.0.29
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
google.golang.org/grpc v1.78.0
)

require (
Expand Down Expand Up @@ -58,7 +60,6 @@ require (
github.com/cosmos/iavl v1.2.6 // indirect
github.com/cosmos/ics23/go v0.11.0 // indirect
github.com/cosmos/ledger-cosmos-go v1.0.0 // indirect
github.com/creachadair/tomledit v0.0.29 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
Expand Down Expand Up @@ -203,7 +204,6 @@ require (
google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
Expand Down
Loading