|
@ -1,7 +1,6 @@ |
|
|
package kv |
|
|
package kv |
|
|
|
|
|
|
|
|
import ( |
|
|
import ( |
|
|
"bytes" |
|
|
|
|
|
"context" |
|
|
"context" |
|
|
"encoding/hex" |
|
|
"encoding/hex" |
|
|
"fmt" |
|
|
"fmt" |
|
@ -10,6 +9,7 @@ import ( |
|
|
"time" |
|
|
"time" |
|
|
|
|
|
|
|
|
"github.com/gogo/protobuf/proto" |
|
|
"github.com/gogo/protobuf/proto" |
|
|
|
|
|
"github.com/google/orderedcode" |
|
|
dbm "github.com/tendermint/tm-db" |
|
|
dbm "github.com/tendermint/tm-db" |
|
|
|
|
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types" |
|
|
abci "github.com/tendermint/tendermint/abci/types" |
|
@ -18,13 +18,12 @@ import ( |
|
|
"github.com/tendermint/tendermint/types" |
|
|
"github.com/tendermint/tendermint/types" |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
|
tagKeySeparator = "/" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
var _ txindex.TxIndexer = (*TxIndex)(nil) |
|
|
var _ txindex.TxIndexer = (*TxIndex)(nil) |
|
|
|
|
|
|
|
|
// TxIndex is the simplest possible indexer, backed by key-value storage (levelDB).
|
|
|
|
|
|
|
|
|
// TxIndex is the simplest possible indexer
|
|
|
|
|
|
// It is backed by two kv stores:
|
|
|
|
|
|
// 1. txhash - result (primary key)
|
|
|
|
|
|
// 2. event - txhash (secondary key)
|
|
|
type TxIndex struct { |
|
|
type TxIndex struct { |
|
|
store dbm.DB |
|
|
store dbm.DB |
|
|
} |
|
|
} |
|
@ -43,7 +42,7 @@ func (txi *TxIndex) Get(hash []byte) (*abci.TxResult, error) { |
|
|
return nil, txindex.ErrorEmptyHash |
|
|
return nil, txindex.ErrorEmptyHash |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
rawBytes, err := txi.store.Get(hash) |
|
|
|
|
|
|
|
|
rawBytes, err := txi.store.Get(primaryKey(hash)) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
panic(err) |
|
|
panic(err) |
|
|
} |
|
|
} |
|
@ -78,7 +77,7 @@ func (txi *TxIndex) AddBatch(b *txindex.Batch) error { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// index by height (always)
|
|
|
// index by height (always)
|
|
|
err = storeBatch.Set(keyForHeight(result), hash) |
|
|
|
|
|
|
|
|
err = storeBatch.Set(keyFromHeight(result), hash) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return err |
|
|
return err |
|
|
} |
|
|
} |
|
@ -88,7 +87,7 @@ func (txi *TxIndex) AddBatch(b *txindex.Batch) error { |
|
|
return err |
|
|
return err |
|
|
} |
|
|
} |
|
|
// index by hash (always)
|
|
|
// index by hash (always)
|
|
|
err = storeBatch.Set(hash, rawBytes) |
|
|
|
|
|
|
|
|
err = storeBatch.Set(primaryKey(hash), rawBytes) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return err |
|
|
return err |
|
|
} |
|
|
} |
|
@ -114,7 +113,7 @@ func (txi *TxIndex) Index(result *abci.TxResult) error { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// index by height (always)
|
|
|
// index by height (always)
|
|
|
err = b.Set(keyForHeight(result), hash) |
|
|
|
|
|
|
|
|
err = b.Set(keyFromHeight(result), hash) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return err |
|
|
return err |
|
|
} |
|
|
} |
|
@ -124,7 +123,7 @@ func (txi *TxIndex) Index(result *abci.TxResult) error { |
|
|
return err |
|
|
return err |
|
|
} |
|
|
} |
|
|
// index by hash (always)
|
|
|
// index by hash (always)
|
|
|
err = b.Set(hash, rawBytes) |
|
|
|
|
|
|
|
|
err = b.Set(primaryKey(hash), rawBytes) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return err |
|
|
return err |
|
|
} |
|
|
} |
|
@ -146,8 +145,12 @@ func (txi *TxIndex) indexEvents(result *abci.TxResult, hash []byte, store dbm.Ba |
|
|
|
|
|
|
|
|
// index if `index: true` is set
|
|
|
// index if `index: true` is set
|
|
|
compositeTag := fmt.Sprintf("%s.%s", event.Type, string(attr.Key)) |
|
|
compositeTag := fmt.Sprintf("%s.%s", event.Type, string(attr.Key)) |
|
|
|
|
|
// ensure event does not conflict with a reserved prefix key
|
|
|
|
|
|
if compositeTag == types.TxHashKey || compositeTag == types.TxHeightKey { |
|
|
|
|
|
return fmt.Errorf("event type and attribute key \"%s\" is reserved. Please use a different key", compositeTag) |
|
|
|
|
|
} |
|
|
if attr.GetIndex() { |
|
|
if attr.GetIndex() { |
|
|
err := store.Set(keyForEvent(compositeTag, attr.Value, result), hash) |
|
|
|
|
|
|
|
|
err := store.Set(keyFromEvent(compositeTag, attr.Value, result), hash) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return err |
|
|
return err |
|
|
} |
|
|
} |
|
@ -215,7 +218,7 @@ func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResul |
|
|
|
|
|
|
|
|
for _, r := range ranges { |
|
|
for _, r := range ranges { |
|
|
if !hashesInitialized { |
|
|
if !hashesInitialized { |
|
|
filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, true) |
|
|
|
|
|
|
|
|
filteredHashes = txi.matchRange(ctx, r, prefixFromCompositeKey(r.key), filteredHashes, true) |
|
|
hashesInitialized = true |
|
|
hashesInitialized = true |
|
|
|
|
|
|
|
|
// Ignore any remaining conditions if the first condition resulted
|
|
|
// Ignore any remaining conditions if the first condition resulted
|
|
@ -224,7 +227,7 @@ func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResul |
|
|
break |
|
|
break |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, false) |
|
|
|
|
|
|
|
|
filteredHashes = txi.matchRange(ctx, r, prefixFromCompositeKey(r.key), filteredHashes, false) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -239,7 +242,7 @@ func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResul |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if !hashesInitialized { |
|
|
if !hashesInitialized { |
|
|
filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, true) |
|
|
|
|
|
|
|
|
filteredHashes = txi.match(ctx, c, prefixForCondition(c, height), filteredHashes, true) |
|
|
hashesInitialized = true |
|
|
hashesInitialized = true |
|
|
|
|
|
|
|
|
// Ignore any remaining conditions if the first condition resulted
|
|
|
// Ignore any remaining conditions if the first condition resulted
|
|
@ -248,7 +251,7 @@ func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResul |
|
|
break |
|
|
break |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, false) |
|
|
|
|
|
|
|
|
filteredHashes = txi.match(ctx, c, prefixForCondition(c, height), filteredHashes, false) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -430,7 +433,7 @@ func (txi *TxIndex) match( |
|
|
case c.Op == query.OpExists: |
|
|
case c.Op == query.OpExists: |
|
|
// XXX: can't use startKeyBz here because c.Operand is nil
|
|
|
// XXX: can't use startKeyBz here because c.Operand is nil
|
|
|
// (e.g. "account.owner/<nil>/" won't match w/ a single row)
|
|
|
// (e.g. "account.owner/<nil>/" won't match w/ a single row)
|
|
|
it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey)) |
|
|
|
|
|
|
|
|
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey)) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
panic(err) |
|
|
panic(err) |
|
|
} |
|
|
} |
|
@ -454,18 +457,18 @@ func (txi *TxIndex) match( |
|
|
// XXX: startKey does not apply here.
|
|
|
// XXX: startKey does not apply here.
|
|
|
// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
|
|
|
// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
|
|
|
// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
|
|
|
// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
|
|
|
it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey)) |
|
|
|
|
|
|
|
|
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey)) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
panic(err) |
|
|
panic(err) |
|
|
} |
|
|
} |
|
|
defer it.Close() |
|
|
defer it.Close() |
|
|
|
|
|
|
|
|
for ; it.Valid(); it.Next() { |
|
|
for ; it.Valid(); it.Next() { |
|
|
if !isTagKey(it.Key()) { |
|
|
|
|
|
|
|
|
value, err := parseValueFromKey(it.Key()) |
|
|
|
|
|
if err != nil { |
|
|
continue |
|
|
continue |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) { |
|
|
|
|
|
|
|
|
if strings.Contains(value, c.Operand.(string)) { |
|
|
tmpHashes[string(it.Value())] = it.Value() |
|
|
tmpHashes[string(it.Value())] = it.Value() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -542,12 +545,12 @@ func (txi *TxIndex) matchRange( |
|
|
|
|
|
|
|
|
LOOP: |
|
|
LOOP: |
|
|
for ; it.Valid(); it.Next() { |
|
|
for ; it.Valid(); it.Next() { |
|
|
if !isTagKey(it.Key()) { |
|
|
|
|
|
|
|
|
value, err := parseValueFromKey(it.Key()) |
|
|
|
|
|
if err != nil { |
|
|
continue |
|
|
continue |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if _, ok := r.AnyBound().(int64); ok { |
|
|
if _, ok := r.AnyBound().(int64); ok { |
|
|
v, err := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) |
|
|
|
|
|
|
|
|
v, err := strconv.ParseInt(value, 10, 64) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
continue LOOP |
|
|
continue LOOP |
|
|
} |
|
|
} |
|
@ -613,46 +616,102 @@ LOOP: |
|
|
return filteredHashes |
|
|
return filteredHashes |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Keys
|
|
|
|
|
|
|
|
|
// ########################## Keys #############################
|
|
|
|
|
|
//
|
|
|
|
|
|
// The indexer has two types of kv stores:
|
|
|
|
|
|
// 1. txhash - result (primary key)
|
|
|
|
|
|
// 2. event - txhash (secondary key)
|
|
|
|
|
|
//
|
|
|
|
|
|
// The event key can be decomposed into 4 parts.
|
|
|
|
|
|
// 1. A composite key which can be any string.
|
|
|
|
|
|
// Usually something like "tx.height" or "account.owner"
|
|
|
|
|
|
// 2. A value. That corresponds to the key. In the above
|
|
|
|
|
|
// example the value could be "5" or "Ivan"
|
|
|
|
|
|
// 3. The height of the Tx that aligns with the key and value.
|
|
|
|
|
|
// 4. The index of the Tx that aligns with the key and value
|
|
|
|
|
|
|
|
|
|
|
|
// the hash/primary key
|
|
|
|
|
|
func primaryKey(hash []byte) []byte { |
|
|
|
|
|
key, err := orderedcode.Append( |
|
|
|
|
|
nil, |
|
|
|
|
|
types.TxHashKey, |
|
|
|
|
|
string(hash), |
|
|
|
|
|
) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
|
|
|
} |
|
|
|
|
|
return key |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// The event/secondary key
|
|
|
|
|
|
func secondaryKey(compositeKey, value string, height int64, index uint32) []byte { |
|
|
|
|
|
key, err := orderedcode.Append( |
|
|
|
|
|
nil, |
|
|
|
|
|
compositeKey, |
|
|
|
|
|
value, |
|
|
|
|
|
height, |
|
|
|
|
|
int64(index), |
|
|
|
|
|
) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
|
|
|
} |
|
|
|
|
|
return key |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
func isTagKey(key []byte) bool { |
|
|
|
|
|
return strings.Count(string(key), tagKeySeparator) == 3 |
|
|
|
|
|
|
|
|
// parseValueFromKey parses an event key and extracts out the value, returning an error if one arises.
|
|
|
|
|
|
// This will also involve ensuring that the key has the correct format.
|
|
|
|
|
|
// CONTRACT: function doesn't check that the prefix is correct. This should have already been done by the iterator
|
|
|
|
|
|
func parseValueFromKey(key []byte) (string, error) { |
|
|
|
|
|
var ( |
|
|
|
|
|
compositeKey, value string |
|
|
|
|
|
height, index int64 |
|
|
|
|
|
) |
|
|
|
|
|
remaining, err := orderedcode.Parse(string(key), &compositeKey, &value, &height, &index) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return "", err |
|
|
|
|
|
} |
|
|
|
|
|
if len(remaining) != 0 { |
|
|
|
|
|
return "", fmt.Errorf("unexpected remainder in key: %s", remaining) |
|
|
|
|
|
} |
|
|
|
|
|
return value, nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func extractValueFromKey(key []byte) string { |
|
|
|
|
|
parts := strings.SplitN(string(key), tagKeySeparator, 3) |
|
|
|
|
|
return parts[1] |
|
|
|
|
|
|
|
|
func keyFromEvent(compositeKey string, value []byte, result *abci.TxResult) []byte { |
|
|
|
|
|
return secondaryKey(compositeKey, string(value), result.Height, result.Index) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func keyForEvent(key string, value []byte, result *abci.TxResult) []byte { |
|
|
|
|
|
return []byte(fmt.Sprintf("%s/%s/%d/%d", |
|
|
|
|
|
key, |
|
|
|
|
|
value, |
|
|
|
|
|
result.Height, |
|
|
|
|
|
result.Index, |
|
|
|
|
|
)) |
|
|
|
|
|
|
|
|
func keyFromHeight(result *abci.TxResult) []byte { |
|
|
|
|
|
return secondaryKey(types.TxHeightKey, fmt.Sprintf("%d", result.Height), result.Height, result.Index) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func keyForHeight(result *abci.TxResult) []byte { |
|
|
|
|
|
return []byte(fmt.Sprintf("%s/%d/%d/%d", |
|
|
|
|
|
types.TxHeightKey, |
|
|
|
|
|
result.Height, |
|
|
|
|
|
result.Height, |
|
|
|
|
|
result.Index, |
|
|
|
|
|
)) |
|
|
|
|
|
|
|
|
// Prefixes: these represent an initial part of the key and are used by iterators to iterate over a small
|
|
|
|
|
|
// section of the kv store during searches.
|
|
|
|
|
|
|
|
|
|
|
|
func prefixFromCompositeKey(compositeKey string) []byte { |
|
|
|
|
|
key, err := orderedcode.Append(nil, compositeKey) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
|
|
|
} |
|
|
|
|
|
return key |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func startKeyForCondition(c query.Condition, height int64) []byte { |
|
|
|
|
|
if height > 0 { |
|
|
|
|
|
return startKey(c.CompositeKey, c.Operand, height) |
|
|
|
|
|
|
|
|
func prefixFromCompositeKeyAndValue(compositeKey, value string) []byte { |
|
|
|
|
|
key, err := orderedcode.Append(nil, compositeKey, value) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
} |
|
|
} |
|
|
return startKey(c.CompositeKey, c.Operand) |
|
|
|
|
|
|
|
|
return key |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func startKey(fields ...interface{}) []byte { |
|
|
|
|
|
var b bytes.Buffer |
|
|
|
|
|
for _, f := range fields { |
|
|
|
|
|
b.Write([]byte(fmt.Sprintf("%v", f) + tagKeySeparator)) |
|
|
|
|
|
|
|
|
// a small utility function for getting a keys prefix based on a condition and a height
|
|
|
|
|
|
func prefixForCondition(c query.Condition, height int64) []byte { |
|
|
|
|
|
key := prefixFromCompositeKeyAndValue(c.CompositeKey, fmt.Sprintf("%v", c.Operand)) |
|
|
|
|
|
if height > 0 { |
|
|
|
|
|
var err error |
|
|
|
|
|
key, err = orderedcode.Append(key, height) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
panic(err) |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
return b.Bytes() |
|
|
|
|
|
|
|
|
return key |
|
|
} |
|
|
} |