From 5cae236f70203248b5abde6bf38addfaec70441f Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Thu, 6 Aug 2026 12:00:34 -0400 Subject: [PATCH 1/7] fix(rpc): guard TxsResults length before indexing by block tx position --- CHANGELOG.md | 1 + x/cronos/rpc/api.go | 17 ++++++++++++++ x/cronos/rpc/api_test.go | 48 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 x/cronos/rpc/api_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0b4cdb58..f0be1b1f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Bug fixes +* [#2174](https://github.com/crypto-org-chain/cronos/pull/2174) fix(rpc): guard `TxsResults` length before indexing by block tx position. * [#2155](https://github.com/crypto-org-chain/cronos/pull/2155) fix(mempool): size tx-cache-size and max-tx-bytes from mempool config directly. * [#2169](https://github.com/crypto-org-chain/cronos/pull/2169) fix(cronos): add safe multiply int check during voucher conversion to EVM coins. * [#2172](https://github.com/crypto-org-chain/cronos/pull/2172) fix(e2ee): cap the address batch in the `Keys` query. diff --git a/x/cronos/rpc/api.go b/x/cronos/rpc/api.go index e25f9990cd..c62bc2726a 100644 --- a/x/cronos/rpc/api.go +++ b/x/cronos/rpc/api.go @@ -86,6 +86,20 @@ func NewCronosAPI( } } +// checkTxsResultsLength guards against indexing blockRes.TxsResults by the position of a tx in +// resBlock.Block.Txs: resBlock and blockRes come from separate RPC calls, so a re-sync, pruning +// race, or a mismatched node behind a multiplexed setup can return results for a different set of +// txs than the block itself. +func checkTxsResultsLength(resBlock *coretypes.ResultBlock, blockRes *coretypes.ResultBlockResults) error { + if len(blockRes.TxsResults) != len(resBlock.Block.Txs) { + return fmt.Errorf( + "mismatched tx results length at height %d: block has %d txs, but got %d tx results", + resBlock.Block.Height, len(resBlock.Block.Txs), len(blockRes.TxsResults), + ) + } + return nil +} + func (api *CronosAPI) getBlockDetail(blockNrOrHash rpctypes.BlockNumberOrHash) ( resBlock *coretypes.ResultBlock, blockNumber int64, @@ -107,6 +121,9 @@ func (api *CronosAPI) getBlockDetail(blockNrOrHash rpctypes.BlockNumberOrHash) ( api.logger.Debug("failed to retrieve block results", "height", blockNum, "error", err.Error()) return resBlock, blockNumber, blockHash, blockRes, baseFee, err } + if err = checkTxsResultsLength(resBlock, blockRes); err != nil { + return resBlock, blockNumber, blockHash, blockRes, baseFee, err + } baseFee, err = api.backend.BaseFee(blockRes) if err != nil { return resBlock, blockNumber, blockHash, blockRes, baseFee, err diff --git a/x/cronos/rpc/api_test.go b/x/cronos/rpc/api_test.go new file mode 100644 index 0000000000..7a5fe32e2c --- /dev/null +++ b/x/cronos/rpc/api_test.go @@ -0,0 +1,48 @@ +package rpc + +import ( + "testing" + + abci "github.com/cometbft/cometbft/abci/types" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" +) + +func TestCheckTxsResultsLength(t *testing.T) { + newResBlock := func(numTxs int) *coretypes.ResultBlock { + txs := make(cmttypes.Txs, numTxs) + for i := range txs { + txs[i] = cmttypes.Tx{byte(i)} + } + return &coretypes.ResultBlock{ + Block: &cmttypes.Block{ + Header: cmttypes.Header{Height: 100}, + Data: cmttypes.Data{Txs: txs}, + }, + } + } + newBlockRes := func(numResults int) *coretypes.ResultBlockResults { + results := make([]*abci.ExecTxResult, numResults) + for i := range results { + results[i] = &abci.ExecTxResult{} + } + return &coretypes.ResultBlockResults{TxsResults: results} + } + + t.Run("matching lengths returns no error", func(t *testing.T) { + err := checkTxsResultsLength(newResBlock(3), newBlockRes(3)) + require.NoError(t, err) + }) + + t.Run("fewer tx results than txs returns error instead of panicking", func(t *testing.T) { + err := checkTxsResultsLength(newResBlock(3), newBlockRes(2)) + require.Error(t, err) + require.Contains(t, err.Error(), "mismatched tx results length") + }) + + t.Run("more tx results than txs returns error", func(t *testing.T) { + err := checkTxsResultsLength(newResBlock(2), newBlockRes(3)) + require.Error(t, err) + }) +} From 9a6b270341fce8ea95d8698da1087d2723fefb22 Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Thu, 6 Aug 2026 13:19:12 -0400 Subject: [PATCH 2/7] trim comment --- x/cronos/rpc/api.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x/cronos/rpc/api.go b/x/cronos/rpc/api.go index c62bc2726a..beb6b88c56 100644 --- a/x/cronos/rpc/api.go +++ b/x/cronos/rpc/api.go @@ -86,10 +86,6 @@ func NewCronosAPI( } } -// checkTxsResultsLength guards against indexing blockRes.TxsResults by the position of a tx in -// resBlock.Block.Txs: resBlock and blockRes come from separate RPC calls, so a re-sync, pruning -// race, or a mismatched node behind a multiplexed setup can return results for a different set of -// txs than the block itself. func checkTxsResultsLength(resBlock *coretypes.ResultBlock, blockRes *coretypes.ResultBlockResults) error { if len(blockRes.TxsResults) != len(resBlock.Block.Txs) { return fmt.Errorf( From 9b4022f92db6094758a903c9ae0060668f018778 Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Thu, 6 Aug 2026 13:41:32 -0400 Subject: [PATCH 3/7] fix(rpc): correct changelog PR link and unassigned blockNum in log --- CHANGELOG.md | 2 +- x/cronos/rpc/api.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0be1b1f13..a099f8cedb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Bug fixes -* [#2174](https://github.com/crypto-org-chain/cronos/pull/2174) fix(rpc): guard `TxsResults` length before indexing by block tx position. +* [#2175](https://github.com/crypto-org-chain/cronos/pull/2175) fix(rpc): guard `TxsResults` length before indexing by block tx position. * [#2155](https://github.com/crypto-org-chain/cronos/pull/2155) fix(mempool): size tx-cache-size and max-tx-bytes from mempool config directly. * [#2169](https://github.com/crypto-org-chain/cronos/pull/2169) fix(cronos): add safe multiply int check during voucher conversion to EVM coins. * [#2172](https://github.com/crypto-org-chain/cronos/pull/2172) fix(e2ee): cap the address batch in the `Keys` query. diff --git a/x/cronos/rpc/api.go b/x/cronos/rpc/api.go index beb6b88c56..204383f384 100644 --- a/x/cronos/rpc/api.go +++ b/x/cronos/rpc/api.go @@ -104,7 +104,6 @@ func (api *CronosAPI) getBlockDetail(blockNrOrHash rpctypes.BlockNumberOrHash) ( baseFee *big.Int, err error, ) { - var blockNum rpctypes.BlockNumber resBlock, err = api.getBlock(blockNrOrHash) if err != nil { api.logger.Debug("block not found", "height", blockNrOrHash, "error", err.Error()) @@ -114,7 +113,7 @@ func (api *CronosAPI) getBlockDetail(blockNrOrHash rpctypes.BlockNumberOrHash) ( blockHash = common.BytesToHash(resBlock.Block.Header.Hash()).Hex() blockRes, err = api.backend.TendermintBlockResultByNumber(&blockNumber) if err != nil { - api.logger.Debug("failed to retrieve block results", "height", blockNum, "error", err.Error()) + api.logger.Debug("failed to retrieve block results", "height", blockNumber, "error", err.Error()) return resBlock, blockNumber, blockHash, blockRes, baseFee, err } if err = checkTxsResultsLength(resBlock, blockRes); err != nil { From a3e72870a05ffcf19b8b1e6203de7c01e7efb65b Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Thu, 6 Aug 2026 13:42:44 -0400 Subject: [PATCH 4/7] test(rpc): assert full mismatched tx results error message --- x/cronos/rpc/api_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x/cronos/rpc/api_test.go b/x/cronos/rpc/api_test.go index 7a5fe32e2c..baf1fc1e37 100644 --- a/x/cronos/rpc/api_test.go +++ b/x/cronos/rpc/api_test.go @@ -37,12 +37,11 @@ func TestCheckTxsResultsLength(t *testing.T) { t.Run("fewer tx results than txs returns error instead of panicking", func(t *testing.T) { err := checkTxsResultsLength(newResBlock(3), newBlockRes(2)) - require.Error(t, err) - require.Contains(t, err.Error(), "mismatched tx results length") + require.EqualError(t, err, "mismatched tx results length at height 100: block has 3 txs, but got 2 tx results") }) t.Run("more tx results than txs returns error", func(t *testing.T) { err := checkTxsResultsLength(newResBlock(2), newBlockRes(3)) - require.Error(t, err) + require.EqualError(t, err, "mismatched tx results length at height 100: block has 2 txs, but got 3 tx results") }) } From ca8bc9aedf762187776d7fedba19002b165c0bc2 Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Thu, 6 Aug 2026 13:44:59 -0400 Subject: [PATCH 5/7] test(rpc): restructure TxsResults length test as suite.Suite table --- x/cronos/rpc/api_test.go | 94 +++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/x/cronos/rpc/api_test.go b/x/cronos/rpc/api_test.go index baf1fc1e37..e1c9861a18 100644 --- a/x/cronos/rpc/api_test.go +++ b/x/cronos/rpc/api_test.go @@ -7,41 +7,75 @@ import ( coretypes "github.com/cometbft/cometbft/rpc/core/types" cmttypes "github.com/cometbft/cometbft/types" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) -func TestCheckTxsResultsLength(t *testing.T) { - newResBlock := func(numTxs int) *coretypes.ResultBlock { - txs := make(cmttypes.Txs, numTxs) - for i := range txs { - txs[i] = cmttypes.Tx{byte(i)} - } - return &coretypes.ResultBlock{ - Block: &cmttypes.Block{ - Header: cmttypes.Header{Height: 100}, - Data: cmttypes.Data{Txs: txs}, - }, - } +func newResBlock(t *testing.T, numTxs int) *coretypes.ResultBlock { + t.Helper() + txs := make(cmttypes.Txs, numTxs) + for i := range txs { + txs[i] = cmttypes.Tx{byte(i)} } - newBlockRes := func(numResults int) *coretypes.ResultBlockResults { - results := make([]*abci.ExecTxResult, numResults) - for i := range results { - results[i] = &abci.ExecTxResult{} - } - return &coretypes.ResultBlockResults{TxsResults: results} + return &coretypes.ResultBlock{ + Block: &cmttypes.Block{ + Header: cmttypes.Header{Height: 100}, + Data: cmttypes.Data{Txs: txs}, + }, } +} - t.Run("matching lengths returns no error", func(t *testing.T) { - err := checkTxsResultsLength(newResBlock(3), newBlockRes(3)) - require.NoError(t, err) - }) +func newBlockRes(t *testing.T, numResults int) *coretypes.ResultBlockResults { + t.Helper() + results := make([]*abci.ExecTxResult, numResults) + for i := range results { + results[i] = &abci.ExecTxResult{} + } + return &coretypes.ResultBlockResults{TxsResults: results} +} - t.Run("fewer tx results than txs returns error instead of panicking", func(t *testing.T) { - err := checkTxsResultsLength(newResBlock(3), newBlockRes(2)) - require.EqualError(t, err, "mismatched tx results length at height 100: block has 3 txs, but got 2 tx results") - }) +type CheckTxsResultsLengthTestSuite struct { + suite.Suite +} - t.Run("more tx results than txs returns error", func(t *testing.T) { - err := checkTxsResultsLength(newResBlock(2), newBlockRes(3)) - require.EqualError(t, err, "mismatched tx results length at height 100: block has 2 txs, but got 3 tx results") - }) +func TestCheckTxsResultsLengthTestSuite(t *testing.T) { + suite.Run(t, new(CheckTxsResultsLengthTestSuite)) +} + +func (s *CheckTxsResultsLengthTestSuite) TestCheckTxsResultsLength() { + testCases := []struct { + name string + numTxs int + numRes int + wantErr string + }{ + { + name: "matching lengths returns no error", + numTxs: 3, + numRes: 3, + }, + { + name: "fewer tx results than txs returns error instead of panicking", + numTxs: 3, + numRes: 2, + wantErr: "mismatched tx results length at height 100: block has 3 txs, but got 2 tx results", + }, + { + name: "more tx results than txs returns error", + numTxs: 2, + numRes: 3, + wantErr: "mismatched tx results length at height 100: block has 2 txs, but got 3 tx results", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + t := s.T() + err := checkTxsResultsLength(newResBlock(t, tc.numTxs), newBlockRes(t, tc.numRes)) + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.EqualError(t, err, tc.wantErr) + }) + } } From 452a005af81bd59ab9ec173bd84105dbccba687d Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Fri, 7 Aug 2026 11:54:08 -0400 Subject: [PATCH 6/7] refactor(rpc): clarify getBlockDetail error semantics and checkTxsResultsLength naming --- x/cronos/rpc/api.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/x/cronos/rpc/api.go b/x/cronos/rpc/api.go index 204383f384..96bb3910aa 100644 --- a/x/cronos/rpc/api.go +++ b/x/cronos/rpc/api.go @@ -86,16 +86,18 @@ func NewCronosAPI( } } -func checkTxsResultsLength(resBlock *coretypes.ResultBlock, blockRes *coretypes.ResultBlockResults) error { - if len(blockRes.TxsResults) != len(resBlock.Block.Txs) { +func checkTxsResultsLength(block *coretypes.ResultBlock, blockResults *coretypes.ResultBlockResults) error { + if len(blockResults.TxsResults) != len(block.Block.Txs) { return fmt.Errorf( "mismatched tx results length at height %d: block has %d txs, but got %d tx results", - resBlock.Block.Height, len(resBlock.Block.Txs), len(blockRes.TxsResults), + block.Block.Height, len(block.Block.Txs), len(blockResults.TxsResults), ) } return nil } +// getBlockDetail returns valid, length-matched resBlock/blockRes only when err is nil; +// on error all other return values are zero. func (api *CronosAPI) getBlockDetail(blockNrOrHash rpctypes.BlockNumberOrHash) ( resBlock *coretypes.ResultBlock, blockNumber int64, @@ -107,21 +109,21 @@ func (api *CronosAPI) getBlockDetail(blockNrOrHash rpctypes.BlockNumberOrHash) ( resBlock, err = api.getBlock(blockNrOrHash) if err != nil { api.logger.Debug("block not found", "height", blockNrOrHash, "error", err.Error()) - return resBlock, blockNumber, blockHash, blockRes, baseFee, err + return nil, 0, "", nil, nil, err } blockNumber = resBlock.Block.Height blockHash = common.BytesToHash(resBlock.Block.Header.Hash()).Hex() blockRes, err = api.backend.TendermintBlockResultByNumber(&blockNumber) if err != nil { api.logger.Debug("failed to retrieve block results", "height", blockNumber, "error", err.Error()) - return resBlock, blockNumber, blockHash, blockRes, baseFee, err + return nil, 0, "", nil, nil, err } if err = checkTxsResultsLength(resBlock, blockRes); err != nil { - return resBlock, blockNumber, blockHash, blockRes, baseFee, err + return nil, 0, "", nil, nil, err } baseFee, err = api.backend.BaseFee(blockRes) if err != nil { - return resBlock, blockNumber, blockHash, blockRes, baseFee, err + return nil, 0, "", nil, nil, err } return resBlock, blockNumber, blockHash, blockRes, baseFee, err } From bb380dece73d1b2dee293c171eb98c57f9405aff Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Fri, 7 Aug 2026 12:07:33 -0400 Subject: [PATCH 7/7] refactor(rpc): drop getBlockDetail comment --- x/cronos/rpc/api.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/x/cronos/rpc/api.go b/x/cronos/rpc/api.go index 96bb3910aa..8b546b0f61 100644 --- a/x/cronos/rpc/api.go +++ b/x/cronos/rpc/api.go @@ -96,8 +96,6 @@ func checkTxsResultsLength(block *coretypes.ResultBlock, blockResults *coretypes return nil } -// getBlockDetail returns valid, length-matched resBlock/blockRes only when err is nil; -// on error all other return values are zero. func (api *CronosAPI) getBlockDetail(blockNrOrHash rpctypes.BlockNumberOrHash) ( resBlock *coretypes.ResultBlock, blockNumber int64,