package kv
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
dbm "github.com/tendermint/tm-db"
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types"
|
|
"github.com/tendermint/tendermint/internal/pubsub/query"
|
|
"github.com/tendermint/tendermint/internal/state/indexer"
|
|
tmrand "github.com/tendermint/tendermint/libs/rand"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
func TestTxIndex(t *testing.T) {
|
|
txIndexer := NewTxIndex(dbm.NewMemDB())
|
|
|
|
tx := types.Tx("HELLO WORLD")
|
|
txResult := &abci.TxResult{
|
|
Height: 1,
|
|
Index: 0,
|
|
Tx: tx,
|
|
Result: abci.ResponseDeliverTx{
|
|
Data: []byte{0},
|
|
Code: abci.CodeTypeOK, Log: "", Events: nil,
|
|
},
|
|
}
|
|
hash := tx.Hash()
|
|
|
|
batch := indexer.NewBatch(1)
|
|
if err := batch.Add(txResult); err != nil {
|
|
t.Error(err)
|
|
}
|
|
err := txIndexer.Index(batch.Ops)
|
|
require.NoError(t, err)
|
|
|
|
loadedTxResult, err := txIndexer.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.ResponseDeliverTx{
|
|
Data: []byte{0},
|
|
Code: abci.CodeTypeOK, Log: "", Events: nil,
|
|
},
|
|
}
|
|
hash2 := tx2.Hash()
|
|
|
|
err = txIndexer.Index([]*abci.TxResult{txResult2})
|
|
require.NoError(t, err)
|
|
|
|
loadedTxResult2, err := txIndexer.Get(hash2)
|
|
require.NoError(t, err)
|
|
assert.True(t, proto.Equal(txResult2, loadedTxResult2))
|
|
}
|
|
|
|
func TestTxSearch(t *testing.T) {
|
|
indexer := NewTxIndex(dbm.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([]*abci.TxResult{txResult})
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
q string
|
|
resultsLength int
|
|
}{
|
|
// search by hash
|
|
{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'", 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},
|
|
// 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 (lower bound)
|
|
{"account.number >= 1", 1},
|
|
// search by range (upper bound)
|
|
{"account.number <= 5", 1},
|
|
// search using not allowed key
|
|
{"not_allowed = 'boom'", 0},
|
|
// search for not existing tx result
|
|
{"account.number >= 2 AND account.number <= 5", 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},
|
|
// 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},
|
|
// search using height
|
|
{"account.number = 1 AND tx.height = 1", 1},
|
|
// search using incorrect height
|
|
{"account.number = 1 AND tx.height = 3", 0},
|
|
// search using height only
|
|
{"tx.height = 1", 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 TestTxSearchWithCancelation(t *testing.T) {
|
|
indexer := NewTxIndex(dbm.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([]*abci.TxResult{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(dbm.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([]*abci.TxResult{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(KeyFromHeight(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(dbm.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}}},
|
|
})
|
|
|
|
err := indexer.Index([]*abci.TxResult{txResult})
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
|
|
results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`))
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, results, 1)
|
|
for _, txr := range results {
|
|
assert.True(t, proto.Equal(txResult, txr))
|
|
}
|
|
}
|
|
|
|
func TestTxSearchMultipleTxs(t *testing.T) {
|
|
indexer := NewTxIndex(dbm.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([]*abci.TxResult{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([]*abci.TxResult{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([]*abci.TxResult{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([]*abci.TxResult{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.ResponseDeliverTx{
|
|
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 := dbm.NewDB("tx_index", "goleveldb", dir)
|
|
require.NoError(b, err)
|
|
txIndexer := NewTxIndex(store)
|
|
|
|
batch := indexer.NewBatch(txsCount)
|
|
txIndex := uint32(0)
|
|
for i := int64(0); i < txsCount; i++ {
|
|
tx := tmrand.Bytes(250)
|
|
txResult := &abci.TxResult{
|
|
Height: 1,
|
|
Index: txIndex,
|
|
Tx: tx,
|
|
Result: abci.ResponseDeliverTx{
|
|
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 = txIndexer.Index(batch.Ops)
|
|
}
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
|
|
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) }
|