github.com/devwanda/aphelion-staking@v0.33.9/state/txindex/kv/kv.go (about)

     1  package kv
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/pkg/errors"
    13  
    14  	dbm "github.com/tendermint/tm-db"
    15  
    16  	"github.com/devwanda/aphelion-staking/libs/pubsub/query"
    17  	tmstring "github.com/devwanda/aphelion-staking/libs/strings"
    18  	"github.com/devwanda/aphelion-staking/state/txindex"
    19  	"github.com/devwanda/aphelion-staking/types"
    20  )
    21  
    22  const (
    23  	tagKeySeparator = "/"
    24  )
    25  
    26  var _ txindex.TxIndexer = (*TxIndex)(nil)
    27  
    28  // TxIndex is the simplest possible indexer, backed by key-value storage (levelDB).
    29  type TxIndex struct {
    30  	store                dbm.DB
    31  	compositeKeysToIndex []string
    32  	indexAllEvents       bool
    33  }
    34  
    35  // NewTxIndex creates new KV indexer.
    36  func NewTxIndex(store dbm.DB, options ...func(*TxIndex)) *TxIndex {
    37  	txi := &TxIndex{store: store, compositeKeysToIndex: make([]string, 0), indexAllEvents: false}
    38  	for _, o := range options {
    39  		o(txi)
    40  	}
    41  	return txi
    42  }
    43  
    44  // IndexEvents is an option for setting which composite keys to index.
    45  func IndexEvents(compositeKeys []string) func(*TxIndex) {
    46  	return func(txi *TxIndex) {
    47  		txi.compositeKeysToIndex = compositeKeys
    48  	}
    49  }
    50  
    51  // IndexAllEvents is an option for indexing all events.
    52  func IndexAllEvents() func(*TxIndex) {
    53  	return func(txi *TxIndex) {
    54  		txi.indexAllEvents = true
    55  	}
    56  }
    57  
    58  // Get gets transaction from the TxIndex storage and returns it or nil if the
    59  // transaction is not found.
    60  func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) {
    61  	if len(hash) == 0 {
    62  		return nil, txindex.ErrorEmptyHash
    63  	}
    64  
    65  	rawBytes, err := txi.store.Get(hash)
    66  	if err != nil {
    67  		panic(err)
    68  	}
    69  	if rawBytes == nil {
    70  		return nil, nil
    71  	}
    72  
    73  	txResult := new(types.TxResult)
    74  	err = cdc.UnmarshalBinaryBare(rawBytes, &txResult)
    75  	if err != nil {
    76  		return nil, fmt.Errorf("error reading TxResult: %v", err)
    77  	}
    78  
    79  	return txResult, nil
    80  }
    81  
    82  // AddBatch indexes a batch of transactions using the given list of events. Each
    83  // key that indexed from the tx's events is a composite of the event type and
    84  // the respective attribute's key delimited by a "." (eg. "account.number").
    85  // Any event with an empty type is not indexed.
    86  func (txi *TxIndex) AddBatch(b *txindex.Batch) error {
    87  	storeBatch := txi.store.NewBatch()
    88  	defer storeBatch.Close()
    89  
    90  	for _, result := range b.Ops {
    91  		hash := result.Tx.Hash()
    92  
    93  		// index tx by events
    94  		txi.indexEvents(result, hash, storeBatch)
    95  
    96  		// index tx by height
    97  		if txi.indexAllEvents || tmstring.StringInSlice(types.TxHeightKey, txi.compositeKeysToIndex) {
    98  			storeBatch.Set(keyForHeight(result), hash)
    99  		}
   100  
   101  		// index tx by hash
   102  		rawBytes, err := cdc.MarshalBinaryBare(result)
   103  		if err != nil {
   104  			return err
   105  		}
   106  		storeBatch.Set(hash, rawBytes)
   107  	}
   108  
   109  	storeBatch.WriteSync()
   110  	return nil
   111  }
   112  
   113  // Index indexes a single transaction using the given list of events. Each key
   114  // that indexed from the tx's events is a composite of the event type and the
   115  // respective attribute's key delimited by a "." (eg. "account.number").
   116  // Any event with an empty type is not indexed.
   117  func (txi *TxIndex) Index(result *types.TxResult) error {
   118  	b := txi.store.NewBatch()
   119  	defer b.Close()
   120  
   121  	hash := result.Tx.Hash()
   122  
   123  	// index tx by events
   124  	txi.indexEvents(result, hash, b)
   125  
   126  	// index tx by height
   127  	if txi.indexAllEvents || tmstring.StringInSlice(types.TxHeightKey, txi.compositeKeysToIndex) {
   128  		b.Set(keyForHeight(result), hash)
   129  	}
   130  
   131  	// index tx by hash
   132  	rawBytes, err := cdc.MarshalBinaryBare(result)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	b.Set(hash, rawBytes)
   138  	b.WriteSync()
   139  
   140  	return nil
   141  }
   142  
   143  func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.SetDeleter) {
   144  	for _, event := range result.Result.Events {
   145  		// only index events with a non-empty type
   146  		if len(event.Type) == 0 {
   147  			continue
   148  		}
   149  
   150  		for _, attr := range event.Attributes {
   151  			if len(attr.Key) == 0 {
   152  				continue
   153  			}
   154  
   155  			compositeTag := fmt.Sprintf("%s.%s", event.Type, string(attr.Key))
   156  			if txi.indexAllEvents || tmstring.StringInSlice(compositeTag, txi.compositeKeysToIndex) {
   157  				store.Set(keyForEvent(compositeTag, attr.Value, result), hash)
   158  			}
   159  		}
   160  	}
   161  }
   162  
   163  // Search performs a search using the given query.
   164  //
   165  // It breaks the query into conditions (like "tx.height > 5"). For each
   166  // condition, it queries the DB index. One special use cases here: (1) if
   167  // "tx.hash" is found, it returns tx result for it (2) for range queries it is
   168  // better for the client to provide both lower and upper bounds, so we are not
   169  // performing a full scan. Results from querying indexes are then intersected
   170  // and returned to the caller, in no particular order.
   171  //
   172  // Search will exit early and return any result fetched so far,
   173  // when a message is received on the context chan.
   174  func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*types.TxResult, error) {
   175  	// Potentially exit early.
   176  	select {
   177  	case <-ctx.Done():
   178  		results := make([]*types.TxResult, 0)
   179  		return results, nil
   180  	default:
   181  	}
   182  
   183  	var hashesInitialized bool
   184  	filteredHashes := make(map[string][]byte)
   185  
   186  	// get a list of conditions (like "tx.height > 5")
   187  	conditions, err := q.Conditions()
   188  	if err != nil {
   189  		return nil, errors.Wrap(err, "error during parsing conditions from query")
   190  	}
   191  
   192  	// if there is a hash condition, return the result immediately
   193  	hash, ok, err := lookForHash(conditions)
   194  	if err != nil {
   195  		return nil, errors.Wrap(err, "error during searching for a hash in the query")
   196  	} else if ok {
   197  		res, err := txi.Get(hash)
   198  		switch {
   199  		case err != nil:
   200  			return []*types.TxResult{}, errors.Wrap(err, "error while retrieving the result")
   201  		case res == nil:
   202  			return []*types.TxResult{}, nil
   203  		default:
   204  			return []*types.TxResult{res}, nil
   205  		}
   206  	}
   207  
   208  	// conditions to skip because they're handled before "everything else"
   209  	skipIndexes := make([]int, 0)
   210  
   211  	// extract ranges
   212  	// if both upper and lower bounds exist, it's better to get them in order not
   213  	// no iterate over kvs that are not within range.
   214  	ranges, rangeIndexes := lookForRanges(conditions)
   215  	if len(ranges) > 0 {
   216  		skipIndexes = append(skipIndexes, rangeIndexes...)
   217  
   218  		for _, r := range ranges {
   219  			if !hashesInitialized {
   220  				filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, true)
   221  				hashesInitialized = true
   222  
   223  				// Ignore any remaining conditions if the first condition resulted
   224  				// in no matches (assuming implicit AND operand).
   225  				if len(filteredHashes) == 0 {
   226  					break
   227  				}
   228  			} else {
   229  				filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, false)
   230  			}
   231  		}
   232  	}
   233  
   234  	// if there is a height condition ("tx.height=3"), extract it
   235  	height := lookForHeight(conditions)
   236  
   237  	// for all other conditions
   238  	for i, c := range conditions {
   239  		if intInSlice(i, skipIndexes) {
   240  			continue
   241  		}
   242  
   243  		if !hashesInitialized {
   244  			filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, true)
   245  			hashesInitialized = true
   246  
   247  			// Ignore any remaining conditions if the first condition resulted
   248  			// in no matches (assuming implicit AND operand).
   249  			if len(filteredHashes) == 0 {
   250  				break
   251  			}
   252  		} else {
   253  			filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, false)
   254  		}
   255  	}
   256  
   257  	results := make([]*types.TxResult, 0, len(filteredHashes))
   258  	for _, h := range filteredHashes {
   259  		res, err := txi.Get(h)
   260  		if err != nil {
   261  			return nil, errors.Wrapf(err, "failed to get Tx{%X}", h)
   262  		}
   263  		results = append(results, res)
   264  
   265  		// Potentially exit early.
   266  		select {
   267  		case <-ctx.Done():
   268  			break
   269  		default:
   270  		}
   271  	}
   272  
   273  	return results, nil
   274  }
   275  
   276  func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error) {
   277  	for _, c := range conditions {
   278  		if c.CompositeKey == types.TxHashKey {
   279  			decoded, err := hex.DecodeString(c.Operand.(string))
   280  			return decoded, true, err
   281  		}
   282  	}
   283  	return
   284  }
   285  
   286  // lookForHeight returns a height if there is an "height=X" condition.
   287  func lookForHeight(conditions []query.Condition) (height int64) {
   288  	for _, c := range conditions {
   289  		if c.CompositeKey == types.TxHeightKey && c.Op == query.OpEqual {
   290  			return c.Operand.(int64)
   291  		}
   292  	}
   293  	return 0
   294  }
   295  
   296  // special map to hold range conditions
   297  // Example: account.number => queryRange{lowerBound: 1, upperBound: 5}
   298  type queryRanges map[string]queryRange
   299  
   300  type queryRange struct {
   301  	lowerBound        interface{} // int || time.Time
   302  	upperBound        interface{} // int || time.Time
   303  	key               string
   304  	includeLowerBound bool
   305  	includeUpperBound bool
   306  }
   307  
   308  func (r queryRange) lowerBoundValue() interface{} {
   309  	if r.lowerBound == nil {
   310  		return nil
   311  	}
   312  
   313  	if r.includeLowerBound {
   314  		return r.lowerBound
   315  	}
   316  
   317  	switch t := r.lowerBound.(type) {
   318  	case int64:
   319  		return t + 1
   320  	case time.Time:
   321  		return t.Unix() + 1
   322  	default:
   323  		panic("not implemented")
   324  	}
   325  }
   326  
   327  func (r queryRange) AnyBound() interface{} {
   328  	if r.lowerBound != nil {
   329  		return r.lowerBound
   330  	}
   331  
   332  	return r.upperBound
   333  }
   334  
   335  func (r queryRange) upperBoundValue() interface{} {
   336  	if r.upperBound == nil {
   337  		return nil
   338  	}
   339  
   340  	if r.includeUpperBound {
   341  		return r.upperBound
   342  	}
   343  
   344  	switch t := r.upperBound.(type) {
   345  	case int64:
   346  		return t - 1
   347  	case time.Time:
   348  		return t.Unix() - 1
   349  	default:
   350  		panic("not implemented")
   351  	}
   352  }
   353  
   354  func lookForRanges(conditions []query.Condition) (ranges queryRanges, indexes []int) {
   355  	ranges = make(queryRanges)
   356  	for i, c := range conditions {
   357  		if isRangeOperation(c.Op) {
   358  			r, ok := ranges[c.CompositeKey]
   359  			if !ok {
   360  				r = queryRange{key: c.CompositeKey}
   361  			}
   362  			switch c.Op {
   363  			case query.OpGreater:
   364  				r.lowerBound = c.Operand
   365  			case query.OpGreaterEqual:
   366  				r.includeLowerBound = true
   367  				r.lowerBound = c.Operand
   368  			case query.OpLess:
   369  				r.upperBound = c.Operand
   370  			case query.OpLessEqual:
   371  				r.includeUpperBound = true
   372  				r.upperBound = c.Operand
   373  			}
   374  			ranges[c.CompositeKey] = r
   375  			indexes = append(indexes, i)
   376  		}
   377  	}
   378  	return ranges, indexes
   379  }
   380  
   381  func isRangeOperation(op query.Operator) bool {
   382  	switch op {
   383  	case query.OpGreater, query.OpGreaterEqual, query.OpLess, query.OpLessEqual:
   384  		return true
   385  	default:
   386  		return false
   387  	}
   388  }
   389  
   390  // match returns all matching txs by hash that meet a given condition and start
   391  // key. An already filtered result (filteredHashes) is provided such that any
   392  // non-intersecting matches are removed.
   393  //
   394  // NOTE: filteredHashes may be empty if no previous condition has matched.
   395  func (txi *TxIndex) match(
   396  	ctx context.Context,
   397  	c query.Condition,
   398  	startKeyBz []byte,
   399  	filteredHashes map[string][]byte,
   400  	firstRun bool,
   401  ) map[string][]byte {
   402  	// A previous match was attempted but resulted in no matches, so we return
   403  	// no matches (assuming AND operand).
   404  	if !firstRun && len(filteredHashes) == 0 {
   405  		return filteredHashes
   406  	}
   407  
   408  	tmpHashes := make(map[string][]byte)
   409  
   410  	switch {
   411  	case c.Op == query.OpEqual:
   412  		it, err := dbm.IteratePrefix(txi.store, startKeyBz)
   413  		if err != nil {
   414  			panic(err)
   415  		}
   416  		defer it.Close()
   417  
   418  		for ; it.Valid(); it.Next() {
   419  			tmpHashes[string(it.Value())] = it.Value()
   420  
   421  			// Potentially exit early.
   422  			select {
   423  			case <-ctx.Done():
   424  				break
   425  			default:
   426  			}
   427  		}
   428  
   429  	case c.Op == query.OpContains:
   430  		// XXX: startKey does not apply here.
   431  		// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
   432  		// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
   433  		it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey))
   434  		if err != nil {
   435  			panic(err)
   436  		}
   437  		defer it.Close()
   438  
   439  		for ; it.Valid(); it.Next() {
   440  			if !isTagKey(it.Key()) {
   441  				continue
   442  			}
   443  
   444  			if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) {
   445  				tmpHashes[string(it.Value())] = it.Value()
   446  			}
   447  
   448  			// Potentially exit early.
   449  			select {
   450  			case <-ctx.Done():
   451  				break
   452  			default:
   453  			}
   454  		}
   455  	default:
   456  		panic("other operators should be handled already")
   457  	}
   458  
   459  	if len(tmpHashes) == 0 || firstRun {
   460  		// Either:
   461  		//
   462  		// 1. Regardless if a previous match was attempted, which may have had
   463  		// results, but no match was found for the current condition, then we
   464  		// return no matches (assuming AND operand).
   465  		//
   466  		// 2. A previous match was not attempted, so we return all results.
   467  		return tmpHashes
   468  	}
   469  
   470  	// Remove/reduce matches in filteredHashes that were not found in this
   471  	// match (tmpHashes).
   472  	for k := range filteredHashes {
   473  		if tmpHashes[k] == nil {
   474  			delete(filteredHashes, k)
   475  
   476  			// Potentially exit early.
   477  			select {
   478  			case <-ctx.Done():
   479  				break
   480  			default:
   481  			}
   482  		}
   483  	}
   484  
   485  	return filteredHashes
   486  }
   487  
   488  // matchRange returns all matching txs by hash that meet a given queryRange and
   489  // start key. An already filtered result (filteredHashes) is provided such that
   490  // any non-intersecting matches are removed.
   491  //
   492  // NOTE: filteredHashes may be empty if no previous condition has matched.
   493  func (txi *TxIndex) matchRange(
   494  	ctx context.Context,
   495  	r queryRange,
   496  	startKey []byte,
   497  	filteredHashes map[string][]byte,
   498  	firstRun bool,
   499  ) map[string][]byte {
   500  	// A previous match was attempted but resulted in no matches, so we return
   501  	// no matches (assuming AND operand).
   502  	if !firstRun && len(filteredHashes) == 0 {
   503  		return filteredHashes
   504  	}
   505  
   506  	tmpHashes := make(map[string][]byte)
   507  	lowerBound := r.lowerBoundValue()
   508  	upperBound := r.upperBoundValue()
   509  
   510  	it, err := dbm.IteratePrefix(txi.store, startKey)
   511  	if err != nil {
   512  		panic(err)
   513  	}
   514  	defer it.Close()
   515  
   516  LOOP:
   517  	for ; it.Valid(); it.Next() {
   518  		if !isTagKey(it.Key()) {
   519  			continue
   520  		}
   521  
   522  		if _, ok := r.AnyBound().(int64); ok {
   523  			v, err := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64)
   524  			if err != nil {
   525  				continue LOOP
   526  			}
   527  
   528  			include := true
   529  			if lowerBound != nil && v < lowerBound.(int64) {
   530  				include = false
   531  			}
   532  
   533  			if upperBound != nil && v > upperBound.(int64) {
   534  				include = false
   535  			}
   536  
   537  			if include {
   538  				tmpHashes[string(it.Value())] = it.Value()
   539  			}
   540  
   541  			// XXX: passing time in a ABCI Events is not yet implemented
   542  			// case time.Time:
   543  			// 	v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64)
   544  			// 	if v == r.upperBound {
   545  			// 		break
   546  			// 	}
   547  		}
   548  
   549  		// Potentially exit early.
   550  		select {
   551  		case <-ctx.Done():
   552  			break
   553  		default:
   554  		}
   555  	}
   556  
   557  	if len(tmpHashes) == 0 || firstRun {
   558  		// Either:
   559  		//
   560  		// 1. Regardless if a previous match was attempted, which may have had
   561  		// results, but no match was found for the current condition, then we
   562  		// return no matches (assuming AND operand).
   563  		//
   564  		// 2. A previous match was not attempted, so we return all results.
   565  		return tmpHashes
   566  	}
   567  
   568  	// Remove/reduce matches in filteredHashes that were not found in this
   569  	// match (tmpHashes).
   570  	for k := range filteredHashes {
   571  		if tmpHashes[k] == nil {
   572  			delete(filteredHashes, k)
   573  
   574  			// Potentially exit early.
   575  			select {
   576  			case <-ctx.Done():
   577  				break
   578  			default:
   579  			}
   580  		}
   581  	}
   582  
   583  	return filteredHashes
   584  }
   585  
   586  ///////////////////////////////////////////////////////////////////////////////
   587  // Keys
   588  
   589  func isTagKey(key []byte) bool {
   590  	return strings.Count(string(key), tagKeySeparator) == 3
   591  }
   592  
   593  func extractValueFromKey(key []byte) string {
   594  	parts := strings.SplitN(string(key), tagKeySeparator, 3)
   595  	return parts[1]
   596  }
   597  
   598  func keyForEvent(key string, value []byte, result *types.TxResult) []byte {
   599  	return []byte(fmt.Sprintf("%s/%s/%d/%d",
   600  		key,
   601  		value,
   602  		result.Height,
   603  		result.Index,
   604  	))
   605  }
   606  
   607  func keyForHeight(result *types.TxResult) []byte {
   608  	return []byte(fmt.Sprintf("%s/%d/%d/%d",
   609  		types.TxHeightKey,
   610  		result.Height,
   611  		result.Height,
   612  		result.Index,
   613  	))
   614  }
   615  
   616  func startKeyForCondition(c query.Condition, height int64) []byte {
   617  	if height > 0 {
   618  		return startKey(c.CompositeKey, c.Operand, height)
   619  	}
   620  	return startKey(c.CompositeKey, c.Operand)
   621  }
   622  
   623  func startKey(fields ...interface{}) []byte {
   624  	var b bytes.Buffer
   625  	for _, f := range fields {
   626  		b.Write([]byte(fmt.Sprintf("%v", f) + tagKeySeparator))
   627  	}
   628  	return b.Bytes()
   629  }