Skip to content

Commit b09cdaf

Browse files
authored
rpc: fix tx_search pagination with ordered results (tendermint#4437)
1 parent c680507 commit b09cdaf

File tree

3 files changed

+79
-46
lines changed

3 files changed

+79
-46
lines changed

Diff for: CHANGELOG_PENDING.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ program](https://hackerone.com/tendermint).
2121

2222
### BUG FIXES:
2323

24+
- [rpc] [\#4437](https://github.com/tendermint/tendermint/pull/4437) Fix tx_search pagination with ordered results (@erikgrinaker)
25+
2426
- [rpc] [\#4406](https://github.com/tendermint/tendermint/pull/4406) Fix issue with multiple subscriptions on the websocket (@antho1404)

Diff for: rpc/client/rpc_test.go

+45-12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/tendermint/tendermint/crypto/ed25519"
1919
"github.com/tendermint/tendermint/crypto/tmhash"
20+
tmbytes "github.com/tendermint/tendermint/libs/bytes"
2021
"github.com/tendermint/tendermint/libs/log"
2122
tmmath "github.com/tendermint/tendermint/libs/math"
2223
mempl "github.com/tendermint/tendermint/mempool"
@@ -414,14 +415,25 @@ func TestTx(t *testing.T) {
414415
}
415416

416417
func TestTxSearch(t *testing.T) {
417-
// first we broadcast a tx
418418
c := getHTTPClient()
419-
_, _, tx := MakeTxKV()
420-
bres, err := c.BroadcastTxCommit(tx)
421-
require.Nil(t, err)
422419

423-
txHeight := bres.Height
424-
txHash := bres.Hash
420+
// first we broadcast a few txs
421+
var tx []byte
422+
var txHeight int64
423+
var txHash tmbytes.HexBytes
424+
for i := 0; i < 10; i++ {
425+
_, _, tx = MakeTxKV()
426+
res, err := c.BroadcastTxCommit(tx)
427+
require.NoError(t, err)
428+
txHeight = res.Height
429+
txHash = res.Hash
430+
}
431+
432+
// Since we're not using an isolated test server, we'll have lingering transactions
433+
// from other tests as well
434+
result, err := c.TxSearch("tx.height >= 0", true, 1, 100, "asc")
435+
require.NoError(t, err)
436+
txCount := len(result.Txs)
425437

426438
anotherTxHash := types.Tx("a different tx").Hash()
427439

@@ -433,6 +445,7 @@ func TestTxSearch(t *testing.T) {
433445
result, err := c.TxSearch(fmt.Sprintf("tx.hash='%v'", txHash), true, 1, 30, "asc")
434446
require.Nil(t, err)
435447
require.Len(t, result.Txs, 1)
448+
require.Equal(t, txHash, result.Txs[0].Hash)
436449

437450
ptx := result.Txs[0]
438451
assert.EqualValues(t, txHeight, ptx.Height)
@@ -476,12 +489,7 @@ func TestTxSearch(t *testing.T) {
476489
require.Nil(t, err)
477490
require.Len(t, result.Txs, 0)
478491

479-
// broadcast another transaction to make sure we have at least two.
480-
_, _, tx2 := MakeTxKV()
481-
_, err = c.BroadcastTxCommit(tx2)
482-
require.Nil(t, err)
483-
484-
// chech sorting
492+
// check sorting
485493
result, err = c.TxSearch(fmt.Sprintf("tx.height >= 1"), false, 1, 30, "asc")
486494
require.Nil(t, err)
487495
for k := 0; k < len(result.Txs)-1; k++ {
@@ -495,6 +503,31 @@ func TestTxSearch(t *testing.T) {
495503
require.GreaterOrEqual(t, result.Txs[k].Height, result.Txs[k+1].Height)
496504
require.GreaterOrEqual(t, result.Txs[k].Index, result.Txs[k+1].Index)
497505
}
506+
507+
// check pagination
508+
seen := map[int64]bool{}
509+
maxHeight := int64(0)
510+
perPage := 3
511+
pages := txCount/perPage + 1
512+
for page := 1; page <= pages; page++ {
513+
result, err = c.TxSearch("tx.height >= 1", false, page, perPage, "asc")
514+
require.NoError(t, err)
515+
if page < pages {
516+
require.Len(t, result.Txs, perPage)
517+
} else {
518+
require.LessOrEqual(t, len(result.Txs), perPage)
519+
}
520+
require.Equal(t, txCount, result.TotalCount)
521+
for _, tx := range result.Txs {
522+
require.False(t, seen[tx.Height],
523+
"Found duplicate height %v in page %v", tx.Height, page)
524+
require.Greater(t, tx.Height, maxHeight,
525+
"Found decreasing height %v (max seen %v) in page %v", tx.Height, maxHeight, page)
526+
seen[tx.Height] = true
527+
maxHeight = tx.Height
528+
}
529+
}
530+
require.Len(t, seen, txCount)
498531
}
499532
}
500533

Diff for: rpc/core/tx.go

+32-34
Original file line numberDiff line numberDiff line change
@@ -72,56 +72,54 @@ func TxSearch(ctx *rpctypes.Context, query string, prove bool, page, perPage int
7272
return nil, err
7373
}
7474

75+
// sort results (must be done before pagination)
76+
switch orderBy {
77+
case "desc":
78+
sort.Slice(results, func(i, j int) bool {
79+
if results[i].Height == results[j].Height {
80+
return results[i].Index > results[j].Index
81+
}
82+
return results[i].Height > results[j].Height
83+
})
84+
case "asc", "":
85+
sort.Slice(results, func(i, j int) bool {
86+
if results[i].Height == results[j].Height {
87+
return results[i].Index < results[j].Index
88+
}
89+
return results[i].Height < results[j].Height
90+
})
91+
default:
92+
return nil, errors.New("expected order_by to be either `asc` or `desc` or empty")
93+
}
94+
95+
// paginate results
7596
totalCount := len(results)
7697
perPage = validatePerPage(perPage)
7798
page, err = validatePage(page, perPage, totalCount)
7899
if err != nil {
79100
return nil, err
80101
}
81102
skipCount := validateSkipCount(page, perPage)
103+
pageSize := tmmath.MinInt(perPage, totalCount-skipCount)
82104

83-
apiResults := make([]*ctypes.ResultTx, tmmath.MinInt(perPage, totalCount-skipCount))
84-
var proof types.TxProof
85-
// if there's no tx in the results array, we don't need to loop through the apiResults array
86-
for i := 0; i < len(apiResults); i++ {
87-
r := results[skipCount+i]
88-
height := r.Height
89-
index := r.Index
105+
apiResults := make([]*ctypes.ResultTx, 0, pageSize)
106+
for i := skipCount; i < skipCount+pageSize; i++ {
107+
r := results[i]
90108

109+
var proof types.TxProof
91110
if prove {
92-
block := blockStore.LoadBlock(height)
93-
proof = block.Data.Txs.Proof(int(index)) // XXX: overflow on 32-bit machines
111+
block := blockStore.LoadBlock(r.Height)
112+
proof = block.Data.Txs.Proof(int(r.Index)) // XXX: overflow on 32-bit machines
94113
}
95114

96-
apiResults[i] = &ctypes.ResultTx{
115+
apiResults = append(apiResults, &ctypes.ResultTx{
97116
Hash: r.Tx.Hash(),
98-
Height: height,
99-
Index: index,
117+
Height: r.Height,
118+
Index: r.Index,
100119
TxResult: r.Result,
101120
Tx: r.Tx,
102121
Proof: proof,
103-
}
104-
}
105-
106-
if len(apiResults) > 1 {
107-
switch orderBy {
108-
case "desc":
109-
sort.Slice(apiResults, func(i, j int) bool {
110-
if apiResults[i].Height == apiResults[j].Height {
111-
return apiResults[i].Index > apiResults[j].Index
112-
}
113-
return apiResults[i].Height > apiResults[j].Height
114-
})
115-
case "asc", "":
116-
sort.Slice(apiResults, func(i, j int) bool {
117-
if apiResults[i].Height == apiResults[j].Height {
118-
return apiResults[i].Index < apiResults[j].Index
119-
}
120-
return apiResults[i].Height < apiResults[j].Height
121-
})
122-
default:
123-
return nil, errors.New("expected order_by to be either `asc` or `desc` or empty")
124-
}
122+
})
125123
}
126124

127125
return &ctypes.ResultTxSearch{Txs: apiResults, TotalCount: totalCount}, nil

0 commit comments

Comments
 (0)