package kv import ( "context" "fmt" "os" "testing" "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" db "git.cw.tr/mukan-network/mukan-consensus-db" abci "git.cw.tr/mukan-network/mukan-consensus/abci/types" "git.cw.tr/mukan-network/mukan-consensus/libs/pubsub/query" cmtrand "git.cw.tr/mukan-network/mukan-consensus/libs/rand" "git.cw.tr/mukan-network/mukan-consensus/state/txindex" "git.cw.tr/mukan-network/mukan-consensus/types" ) func TestTxIndex(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) tx := types.Tx("HELLO WORLD") txResult := &abci.TxResult{ Height: 1, Index: 0, Tx: tx, Result: abci.ExecTxResult{ Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Events: nil, }, } hash := tx.Hash() batch := txindex.NewBatch(1) if err := batch.Add(txResult); err != nil { t.Error(err) } err := indexer.AddBatch(batch) require.NoError(t, err) loadedTxResult, err := indexer.Get(hash) require.NoError(t, err) assert.True(t, proto.Equal(txResult, loadedTxResult)) tx2 := types.Tx("BYE BYE WORLD") txResult2 := &abci.TxResult{ Height: 1, Index: 0, Tx: tx2, Result: abci.ExecTxResult{ Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Events: nil, }, } hash2 := tx2.Hash() err = indexer.Index(txResult2) require.NoError(t, err) loadedTxResult2, err := indexer.Get(hash2) require.NoError(t, err) assert.True(t, proto.Equal(txResult2, loadedTxResult2)) } func TestTxSearch(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) txResult := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "/Ivan/", Index: true}}}, {Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}}, }) hash := types.Tx(txResult.Tx).Hash() err := indexer.Index(txResult) require.NoError(t, err) testCases := []struct { q string resultsLength int }{ // search by hash {fmt.Sprintf("tx.hash = '%X'", hash), 1}, // search by hash (lower) {fmt.Sprintf("tx.hash = '%x'", hash), 1}, // search by exact match (one key) {"account.number = 1", 1}, // search by exact match (two keys) {"account.number = 1 AND account.owner = 'Ivan'", 0}, {"account.owner = 'Ivan' AND account.number = 1", 0}, {"account.owner = '/Ivan/'", 1}, // search by exact match (two keys) {"account.number = 1 AND account.owner = 'Vlad'", 0}, {"account.owner = 'Vlad' AND account.number = 1", 0}, {"account.number >= 1 AND account.owner = 'Vlad'", 0}, {"account.owner = 'Vlad' AND account.number >= 1", 0}, {"account.number <= 0", 0}, {"account.number <= 0 AND account.owner = 'Ivan'", 0}, {"account.number < 10000 AND account.owner = 'Ivan'", 0}, // search using a prefix of the stored value {"account.owner = 'Iv'", 0}, // search by range {"account.number >= 1 AND account.number <= 5", 1}, // search by range and another key {"account.number >= 1 AND account.owner = 'Ivan' AND account.number <= 5", 0}, // search by range (lower bound) {"account.number >= 1", 1}, // search by range (upper bound) {"account.number <= 5", 1}, {"account.number <= 1", 1}, // search using not allowed key {"not_allowed = 'boom'", 0}, {"not_allowed = 'Vlad'", 0}, // search for not existing tx result {"account.number >= 2 AND account.number <= 5 AND tx.height > 0", 0}, // search using not existing key {"account.date >= TIME 2013-05-03T14:45:00Z", 0}, // search using CONTAINS {"account.owner CONTAINS 'an'", 1}, // search for non existing value using CONTAINS {"account.owner CONTAINS 'Vlad'", 0}, {"account.owner CONTAINS 'Ivann'", 0}, {"account.owner CONTAINS 'IIvan'", 0}, {"account.owner CONTAINS 'Iva n'", 0}, {"account.owner CONTAINS ' Ivan'", 0}, {"account.owner CONTAINS 'Ivan '", 0}, // search using the wrong key (of numeric type) using CONTAINS {"account.number CONTAINS 'Iv'", 0}, // search using EXISTS {"account.number EXISTS", 1}, // search using EXISTS for non existing key {"account.date EXISTS", 0}, {"not_allowed EXISTS", 0}, } ctx := context.Background() for _, tc := range testCases { tc := tc t.Run(tc.q, func(t *testing.T) { results, err := indexer.Search(ctx, query.MustCompile(tc.q)) assert.NoError(t, err) assert.Len(t, results, tc.resultsLength) if tc.resultsLength > 0 { for _, txr := range results { assert.True(t, proto.Equal(txResult, txr)) } } }) } } func TestTxSearchEventMatch(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) txResult := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}, {Key: "owner", Value: "Ana", Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "2", Index: true}, {Key: "owner", Value: "/Ivan/.test", Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "3", Index: false}, {Key: "owner", Value: "Mickey", Index: false}}}, {Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}}, }) err := indexer.Index(txResult) require.NoError(t, err) testCases := map[string]struct { q string resultsLength int }{ "Return all events from a height": { q: "tx.height = 1", resultsLength: 1, }, "Don't match non-indexed events": { q: "account.number = 3 AND account.owner = 'Mickey'", resultsLength: 0, }, "Return all events from a height with range": { q: "tx.height > 0", resultsLength: 1, }, "Return all events from a height with range 2": { q: "tx.height <= 1", resultsLength: 1, }, "Return all events from a height (deduplicate height)": { q: "tx.height = 1 AND tx.height = 1", resultsLength: 1, }, "Match attributes with height range and event": { q: "tx.height < 2 AND tx.height > 0 AND account.number > 0 AND account.number <= 1 AND account.owner CONTAINS 'Ana'", resultsLength: 1, }, "Match attributes with multiple CONTAIN and height range": { q: "tx.height < 2 AND tx.height > 0 AND account.number = 1 AND account.owner CONTAINS 'Ana' AND account.owner CONTAINS 'An'", resultsLength: 1, }, "Match attributes with height range and event - no match": { q: "tx.height < 2 AND tx.height > 0 AND account.number = 2 AND account.owner = 'Ana'", resultsLength: 0, }, "Match attributes with event": { q: "account.number = 2 AND account.owner = 'Ana' AND tx.height = 1", resultsLength: 0, }, "Deduplication test - should return nothing if attribute repeats multiple times": { q: "tx.height < 2 AND account.number = 3 AND account.number = 2 AND account.number = 5", resultsLength: 0, }, " Match range with special character": { q: "account.number < 2 AND account.owner = '/Ivan/.test'", resultsLength: 0, }, " Match range with special character 2": { q: "account.number <= 2 AND account.owner = '/Ivan/.test' AND tx.height > 0", resultsLength: 1, }, " Match range with contains with multiple items": { q: "account.number <= 2 AND account.owner CONTAINS '/Iv' AND account.owner CONTAINS 'an' AND tx.height = 1", resultsLength: 1, }, " Match range with contains": { q: "account.number <= 2 AND account.owner CONTAINS 'an' AND tx.height > 0", resultsLength: 1, }, } ctx := context.Background() for _, tc := range testCases { tc := tc t.Run(tc.q, func(t *testing.T) { results, err := indexer.Search(ctx, query.MustCompile(tc.q)) assert.NoError(t, err) assert.Len(t, results, tc.resultsLength) if tc.resultsLength > 0 { for _, txr := range results { assert.True(t, proto.Equal(txResult, txr)) } } }) } } func TestTxSearchEventMatchByHeight(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) txResult := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}, {Key: "owner", Value: "Ana", Index: true}}}, }) err := indexer.Index(txResult) require.NoError(t, err) txResult10 := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}, {Key: "owner", Value: "/Ivan/.test", Index: true}}}, }) txResult10.Tx = types.Tx("HELLO WORLD 10") txResult10.Height = 10 err = indexer.Index(txResult10) require.NoError(t, err) testCases := map[string]struct { q string resultsLength int }{ "Return all events from a height 1": { q: "tx.height = 1", resultsLength: 1, }, "Return all events from a height 10": { q: "tx.height = 10", resultsLength: 1, }, "Return all events from a height 5": { q: "tx.height = 5", resultsLength: 0, }, "Return all events from a height in [2; 5]": { q: "tx.height >= 2 AND tx.height <= 5", resultsLength: 0, }, "Return all events from a height in [1; 5]": { q: "tx.height >= 1 AND tx.height <= 5", resultsLength: 1, }, "Return all events from a height in [1; 10]": { q: "tx.height >= 1 AND tx.height <= 10", resultsLength: 2, }, "Return all events from a height in [1; 5] by account.number": { q: "tx.height >= 1 AND tx.height <= 5 AND account.number=1", resultsLength: 1, }, "Return all events from a height in [1; 10] by account.number 2": { q: "tx.height >= 1 AND tx.height <= 10 AND account.number=1", resultsLength: 2, }, } ctx := context.Background() for _, tc := range testCases { tc := tc t.Run(tc.q, func(t *testing.T) { results, err := indexer.Search(ctx, query.MustCompile(tc.q)) assert.NoError(t, err) assert.Len(t, results, tc.resultsLength) if tc.resultsLength > 0 { for _, txr := range results { switch txr.Height { case 1: assert.True(t, proto.Equal(txResult, txr)) case 10: assert.True(t, proto.Equal(txResult10, txr)) default: assert.True(t, false) } } } }) } } func TestTxSearchWithCancelation(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) txResult := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "Ivan", Index: true}}}, {Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}}, }) err := indexer.Index(txResult) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) cancel() results, err := indexer.Search(ctx, query.MustCompile(`account.number = 1`)) assert.NoError(t, err) assert.Empty(t, results) } func TestTxSearchDeprecatedIndexing(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) // index tx using events indexing (composite key) txResult1 := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}}, }) hash1 := types.Tx(txResult1.Tx).Hash() err := indexer.Index(txResult1) require.NoError(t, err) // index tx also using deprecated indexing (event as key) txResult2 := txResultWithEvents(nil) txResult2.Tx = types.Tx("HELLO WORLD 2") hash2 := types.Tx(txResult2.Tx).Hash() b := indexer.store.NewBatch() rawBytes, err := proto.Marshal(txResult2) require.NoError(t, err) depKey := []byte(fmt.Sprintf("%s/%s/%d/%d", "sender", "addr1", txResult2.Height, txResult2.Index, )) err = b.Set(depKey, hash2) require.NoError(t, err) err = b.Set(keyForHeight(txResult2), hash2) require.NoError(t, err) err = b.Set(hash2, rawBytes) require.NoError(t, err) err = b.Write() require.NoError(t, err) testCases := []struct { q string results []*abci.TxResult }{ // search by hash {fmt.Sprintf("tx.hash = '%X'", hash1), []*abci.TxResult{txResult1}}, // search by hash {fmt.Sprintf("tx.hash = '%X'", hash2), []*abci.TxResult{txResult2}}, // search by exact match (one key) {"account.number = 1", []*abci.TxResult{txResult1}}, {"account.number >= 1 AND account.number <= 5", []*abci.TxResult{txResult1}}, // search by range (lower bound) {"account.number >= 1", []*abci.TxResult{txResult1}}, // search by range (upper bound) {"account.number <= 5", []*abci.TxResult{txResult1}}, // search using not allowed key {"not_allowed = 'boom'", []*abci.TxResult{}}, // search for not existing tx result {"account.number >= 2 AND account.number <= 5", []*abci.TxResult{}}, // search using not existing key {"account.date >= TIME 2013-05-03T14:45:00Z", []*abci.TxResult{}}, // search by deprecated key {"sender = 'addr1'", []*abci.TxResult{txResult2}}, } ctx := context.Background() for _, tc := range testCases { tc := tc t.Run(tc.q, func(t *testing.T) { results, err := indexer.Search(ctx, query.MustCompile(tc.q)) require.NoError(t, err) for _, txr := range results { for _, tr := range tc.results { assert.True(t, proto.Equal(tr, txr)) } } }) } } func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) txResult := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "2", Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "3", Index: false}}}, }) err := indexer.Index(txResult) require.NoError(t, err) testCases := []struct { name string q string found bool }{ { q: "account.number >= 1", found: true, }, { q: "account.number > 2", found: false, }, { q: "account.number >= 1 AND tx.height = 3 AND tx.height > 0", found: true, }, { q: "account.number >= 1 AND tx.height > 0 AND tx.height = 3", found: true, }, { q: "account.number >= 1 AND tx.height = 1 AND tx.height = 2 AND tx.height = 3", found: true, }, { q: "account.number >= 1 AND tx.height = 3 AND tx.height = 2 AND tx.height = 1", found: false, }, { q: "account.number >= 1 AND tx.height = 3", found: false, }, { q: "account.number > 1 AND tx.height < 2", found: true, }, { q: "account.number >= 2", found: true, }, { q: "account.number <= 1", found: true, }, { q: "account.number = 'something'", found: false, }, { q: "account.number CONTAINS 'bla'", found: false, }, } ctx := context.Background() for _, tc := range testCases { results, err := indexer.Search(ctx, query.MustCompile(tc.q)) assert.NoError(t, err) n := 0 if tc.found { n = 1 } assert.Len(t, results, n) assert.True(t, !tc.found || proto.Equal(txResult, results[0])) } } func TestTxIndexDuplicatePreviouslySuccessful(t *testing.T) { mockTx := types.Tx("MOCK_TX_HASH") testCases := []struct { name string tx1 *abci.TxResult tx2 *abci.TxResult expOverwrite bool // do we expect the second tx to overwrite the first tx }{ { "don't overwrite as a non-zero code was returned and the previous tx was successful", &abci.TxResult{ Height: 1, Index: 0, Tx: mockTx, Result: abci.ExecTxResult{ Code: abci.CodeTypeOK, }, }, &abci.TxResult{ Height: 2, Index: 0, Tx: mockTx, Result: abci.ExecTxResult{ Code: abci.CodeTypeOK + 1, }, }, false, }, { "overwrite as the previous tx was also unsuccessful", &abci.TxResult{ Height: 1, Index: 0, Tx: mockTx, Result: abci.ExecTxResult{ Code: abci.CodeTypeOK + 1, }, }, &abci.TxResult{ Height: 2, Index: 0, Tx: mockTx, Result: abci.ExecTxResult{ Code: abci.CodeTypeOK + 1, }, }, true, }, { "overwrite as the most recent tx was successful", &abci.TxResult{ Height: 1, Index: 0, Tx: mockTx, Result: abci.ExecTxResult{ Code: abci.CodeTypeOK, }, }, &abci.TxResult{ Height: 2, Index: 0, Tx: mockTx, Result: abci.ExecTxResult{ Code: abci.CodeTypeOK, }, }, true, }, } hash := mockTx.Hash() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) // index the first tx err := indexer.Index(tc.tx1) require.NoError(t, err) // index the same tx with different results err = indexer.Index(tc.tx2) require.NoError(t, err) res, err := indexer.Get(hash) require.NoError(t, err) if tc.expOverwrite { require.Equal(t, tc.tx2, res) } else { require.Equal(t, tc.tx1, res) } }) } } func TestTxSearchMultipleTxs(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) // indexed first, but bigger height (to test the order of transactions) txResult := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}}, }) txResult.Tx = types.Tx("Bob's account") txResult.Height = 2 txResult.Index = 1 err := indexer.Index(txResult) require.NoError(t, err) // indexed second, but smaller height (to test the order of transactions) txResult2 := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "2", Index: true}}}, }) txResult2.Tx = types.Tx("Alice's account") txResult2.Height = 1 txResult2.Index = 2 err = indexer.Index(txResult2) require.NoError(t, err) // indexed third (to test the order of transactions) txResult3 := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "3", Index: true}}}, }) txResult3.Tx = types.Tx("Jack's account") txResult3.Height = 1 txResult3.Index = 1 err = indexer.Index(txResult3) require.NoError(t, err) // indexed fourth (to test we don't include txs with similar events) // https://github.com/tendermint/tendermint/issues/2908 txResult4 := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number.id", Value: "1", Index: true}}}, }) txResult4.Tx = types.Tx("Mike's account") txResult4.Height = 2 txResult4.Index = 2 err = indexer.Index(txResult4) require.NoError(t, err) ctx := context.Background() results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`)) assert.NoError(t, err) require.Len(t, results, 3) } func txResultWithEvents(events []abci.Event) *abci.TxResult { tx := types.Tx("HELLO WORLD") return &abci.TxResult{ Height: 1, Index: 0, Tx: tx, Result: abci.ExecTxResult{ Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Events: events, }, } } func benchmarkTxIndex(txsCount int64, b *testing.B) { dir, err := os.MkdirTemp("", "tx_index_db") require.NoError(b, err) defer os.RemoveAll(dir) store, err := db.NewDB("tx_index", "goleveldb", dir) require.NoError(b, err) indexer := NewTxIndex(store) batch := txindex.NewBatch(txsCount) txIndex := uint32(0) for i := int64(0); i < txsCount; i++ { tx := cmtrand.Bytes(250) txResult := &abci.TxResult{ Height: 1, Index: txIndex, Tx: tx, Result: abci.ExecTxResult{ Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Events: []abci.Event{}, }, } if err := batch.Add(txResult); err != nil { b.Fatal(err) } txIndex++ } b.ResetTimer() for n := 0; n < b.N; n++ { err = indexer.AddBatch(batch) } if err != nil { b.Fatal(err) } } func TestBigInt(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) bigInt := "10000000000000000000" bigIntPlus1 := "10000000000000000001" bigFloat := bigInt + ".76" bigFloatLower := bigInt + ".1" bigFloatSmaller := "9999999999999999999" + ".1" bigIntSmaller := "9999999999999999999" txResult := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigInt, Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigFloatSmaller, Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigIntPlus1, Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigFloatLower, Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "/Ivan/", Index: true}}}, {Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}}, }) hash := types.Tx(txResult.Tx).Hash() err := indexer.Index(txResult) require.NoError(t, err) txResult2 := txResultWithEvents([]abci.Event{ {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigFloat, Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigFloat, Index: true}, {Key: "amount", Value: "5", Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigIntSmaller, Index: true}}}, {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: bigInt, Index: true}, {Key: "amount", Value: "3", Index: true}}}}) txResult2.Tx = types.Tx("NEW TX") txResult2.Height = 2 txResult2.Index = 2 hash2 := types.Tx(txResult2.Tx).Hash() err = indexer.Index(txResult2) require.NoError(t, err) testCases := []struct { q string txRes *abci.TxResult resultsLength int }{ // search by hash {fmt.Sprintf("tx.hash = '%X'", hash), txResult, 1}, // search by hash (lower) {fmt.Sprintf("tx.hash = '%x'", hash), txResult, 1}, {fmt.Sprintf("tx.hash = '%x'", hash2), txResult2, 1}, // search by exact match (one key) - bigint {"account.number >= " + bigInt, nil, 2}, // search by exact match (one key) - bigint range {"account.number >= " + bigInt + " AND tx.height > 0", nil, 2}, {"account.number >= " + bigInt + " AND tx.height > 0 AND account.owner = '/Ivan/'", nil, 0}, // Floats are not parsed {"account.number >= " + bigInt + " AND tx.height > 0 AND account.amount > 4", txResult2, 1}, {"account.number >= " + bigInt + " AND tx.height > 0 AND account.amount = 5", txResult2, 1}, {"account.number >= " + bigInt + " AND account.amount <= 5", txResult2, 1}, {"account.number > " + bigFloatSmaller + " AND account.amount = 3", txResult2, 1}, {"account.number < " + bigInt + " AND tx.height >= 1", nil, 2}, {"account.number < " + bigInt + " AND tx.height = 1", nil, 1}, {"account.number < " + bigInt + " AND tx.height = 2", nil, 1}, } ctx := context.Background() for _, tc := range testCases { tc := tc t.Run(tc.q, func(t *testing.T) { results, err := indexer.Search(ctx, query.MustCompile(tc.q)) assert.NoError(t, err) assert.Len(t, results, tc.resultsLength) if tc.resultsLength > 0 && tc.txRes != nil { assert.True(t, proto.Equal(results[0], tc.txRes)) } }) } } func BenchmarkTxIndex1(b *testing.B) { benchmarkTxIndex(1, b) } func BenchmarkTxIndex500(b *testing.B) { benchmarkTxIndex(500, b) } func BenchmarkTxIndex1000(b *testing.B) { benchmarkTxIndex(1000, b) } func BenchmarkTxIndex2000(b *testing.B) { benchmarkTxIndex(2000, b) } func BenchmarkTxIndex10000(b *testing.B) { benchmarkTxIndex(10000, b) }