github.com/aakash4dev/cometbft@v0.38.2/state/txindex/kv/kv.go (about)

     1  package kv
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"math/big"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/aakash4dev/cometbft/libs/log"
    13  
    14  	"github.com/cosmos/gogoproto/proto"
    15  
    16  	dbm "github.com/aakash4dev/cometbft-db"
    17  
    18  	abci "github.com/aakash4dev/cometbft/abci/types"
    19  	idxutil "github.com/aakash4dev/cometbft/internal/indexer"
    20  	"github.com/aakash4dev/cometbft/libs/pubsub/query"
    21  	"github.com/aakash4dev/cometbft/libs/pubsub/query/syntax"
    22  	"github.com/aakash4dev/cometbft/state/indexer"
    23  	"github.com/aakash4dev/cometbft/state/txindex"
    24  	"github.com/aakash4dev/cometbft/types"
    25  )
    26  
    27  const (
    28  	tagKeySeparator   = "/"
    29  	eventSeqSeparator = "$es$"
    30  )
    31  
    32  var _ txindex.TxIndexer = (*TxIndex)(nil)
    33  
    34  // TxIndex is the simplest possible indexer, backed by key-value storage (levelDB).
    35  type TxIndex struct {
    36  	store dbm.DB
    37  	// Number the events in the event list
    38  	eventSeq int64
    39  
    40  	log log.Logger
    41  }
    42  
    43  // NewTxIndex creates new KV indexer.
    44  func NewTxIndex(store dbm.DB) *TxIndex {
    45  	return &TxIndex{
    46  		store: store,
    47  	}
    48  }
    49  
    50  func (txi *TxIndex) SetLogger(l log.Logger) {
    51  	txi.log = l
    52  }
    53  
    54  // Get gets transaction from the TxIndex storage and returns it or nil if the
    55  // transaction is not found.
    56  func (txi *TxIndex) Get(hash []byte) (*abci.TxResult, error) {
    57  	if len(hash) == 0 {
    58  		return nil, txindex.ErrorEmptyHash
    59  	}
    60  
    61  	rawBytes, err := txi.store.Get(hash)
    62  	if err != nil {
    63  		panic(err)
    64  	}
    65  	if rawBytes == nil {
    66  		return nil, nil
    67  	}
    68  
    69  	txResult := new(abci.TxResult)
    70  	err = proto.Unmarshal(rawBytes, txResult)
    71  	if err != nil {
    72  		return nil, fmt.Errorf("error reading TxResult: %v", err)
    73  	}
    74  
    75  	return txResult, nil
    76  }
    77  
    78  // AddBatch indexes a batch of transactions using the given list of events. Each
    79  // key that indexed from the tx's events is a composite of the event type and
    80  // the respective attribute's key delimited by a "." (eg. "account.number").
    81  // Any event with an empty type is not indexed.
    82  func (txi *TxIndex) AddBatch(b *txindex.Batch) error {
    83  	storeBatch := txi.store.NewBatch()
    84  	defer storeBatch.Close()
    85  
    86  	for _, result := range b.Ops {
    87  		hash := types.Tx(result.Tx).Hash()
    88  
    89  		// index tx by events
    90  		err := txi.indexEvents(result, hash, storeBatch)
    91  		if err != nil {
    92  			return err
    93  		}
    94  
    95  		// index by height (always)
    96  		err = storeBatch.Set(keyForHeight(result), hash)
    97  		if err != nil {
    98  			return err
    99  		}
   100  
   101  		rawBytes, err := proto.Marshal(result)
   102  		if err != nil {
   103  			return err
   104  		}
   105  		// index by hash (always)
   106  		err = storeBatch.Set(hash, rawBytes)
   107  		if err != nil {
   108  			return err
   109  		}
   110  	}
   111  
   112  	return storeBatch.WriteSync()
   113  }
   114  
   115  // Index indexes a single transaction using the given list of events. Each key
   116  // that indexed from the tx's events is a composite of the event type and the
   117  // respective attribute's key delimited by a "." (eg. "account.number").
   118  // Any event with an empty type is not indexed.
   119  //
   120  // If a transaction is indexed with the same hash as a previous transaction, it will
   121  // be overwritten unless the tx result was NOT OK and the prior result was OK i.e.
   122  // more transactions that successfully executed overwrite transactions that failed
   123  // or successful yet older transactions.
   124  func (txi *TxIndex) Index(result *abci.TxResult) error {
   125  	b := txi.store.NewBatch()
   126  	defer b.Close()
   127  
   128  	hash := types.Tx(result.Tx).Hash()
   129  
   130  	if !result.Result.IsOK() {
   131  		oldResult, err := txi.Get(hash)
   132  		if err != nil {
   133  			return err
   134  		}
   135  
   136  		// if the new transaction failed and it's already indexed in an older block and was successful
   137  		// we skip it as we want users to get the older successful transaction when they query.
   138  		if oldResult != nil && oldResult.Result.Code == abci.CodeTypeOK {
   139  			return nil
   140  		}
   141  	}
   142  
   143  	// index tx by events
   144  	err := txi.indexEvents(result, hash, b)
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	// index by height (always)
   150  	err = b.Set(keyForHeight(result), hash)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	rawBytes, err := proto.Marshal(result)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	// index by hash (always)
   160  	err = b.Set(hash, rawBytes)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	return b.WriteSync()
   166  }
   167  
   168  func (txi *TxIndex) indexEvents(result *abci.TxResult, hash []byte, store dbm.Batch) error {
   169  	for _, event := range result.Result.Events {
   170  		txi.eventSeq = txi.eventSeq + 1
   171  		// only index events with a non-empty type
   172  		if len(event.Type) == 0 {
   173  			continue
   174  		}
   175  
   176  		for _, attr := range event.Attributes {
   177  			if len(attr.Key) == 0 {
   178  				continue
   179  			}
   180  
   181  			// index if `index: true` is set
   182  			compositeTag := fmt.Sprintf("%s.%s", event.Type, attr.Key)
   183  			// ensure event does not conflict with a reserved prefix key
   184  			if compositeTag == types.TxHashKey || compositeTag == types.TxHeightKey {
   185  				return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeTag)
   186  			}
   187  			if attr.GetIndex() {
   188  				err := store.Set(keyForEvent(compositeTag, attr.Value, result, txi.eventSeq), hash)
   189  				if err != nil {
   190  					return err
   191  				}
   192  			}
   193  		}
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  // Search performs a search using the given query.
   200  //
   201  // It breaks the query into conditions (like "tx.height > 5"). For each
   202  // condition, it queries the DB index. One special use cases here: (1) if
   203  // "tx.hash" is found, it returns tx result for it (2) for range queries it is
   204  // better for the client to provide both lower and upper bounds, so we are not
   205  // performing a full scan. Results from querying indexes are then intersected
   206  // and returned to the caller, in no particular order.
   207  //
   208  // Search will exit early and return any result fetched so far,
   209  // when a message is received on the context chan.
   210  func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
   211  	select {
   212  	case <-ctx.Done():
   213  		return make([]*abci.TxResult, 0), nil
   214  
   215  	default:
   216  	}
   217  
   218  	var hashesInitialized bool
   219  	filteredHashes := make(map[string][]byte)
   220  
   221  	// get a list of conditions (like "tx.height > 5")
   222  	conditions := q.Syntax()
   223  
   224  	// if there is a hash condition, return the result immediately
   225  	hash, ok, err := lookForHash(conditions)
   226  	if err != nil {
   227  		return nil, fmt.Errorf("error during searching for a hash in the query: %w", err)
   228  	} else if ok {
   229  		res, err := txi.Get(hash)
   230  		switch {
   231  		case err != nil:
   232  			return []*abci.TxResult{}, fmt.Errorf("error while retrieving the result: %w", err)
   233  		case res == nil:
   234  			return []*abci.TxResult{}, nil
   235  		default:
   236  			return []*abci.TxResult{res}, nil
   237  		}
   238  	}
   239  
   240  	// conditions to skip because they're handled before "everything else"
   241  	skipIndexes := make([]int, 0)
   242  	var heightInfo HeightInfo
   243  
   244  	// If we are not matching events and tx.height = 3 occurs more than once, the later value will
   245  	// overwrite the first one.
   246  	conditions, heightInfo = dedupHeight(conditions)
   247  
   248  	if !heightInfo.onlyHeightEq {
   249  		skipIndexes = append(skipIndexes, heightInfo.heightEqIdx)
   250  	}
   251  
   252  	// extract ranges
   253  	// if both upper and lower bounds exist, it's better to get them in order not
   254  	// no iterate over kvs that are not within range.
   255  	ranges, rangeIndexes, heightRange := indexer.LookForRangesWithHeight(conditions)
   256  	heightInfo.heightRange = heightRange
   257  	if len(ranges) > 0 {
   258  		skipIndexes = append(skipIndexes, rangeIndexes...)
   259  
   260  		for _, qr := range ranges {
   261  
   262  			// If we have a query range over height and want to still look for
   263  			// specific event values we do not want to simply return all
   264  			// transactios in this height range. We remember the height range info
   265  			// and pass it on to match() to take into account when processing events.
   266  			if qr.Key == types.TxHeightKey && !heightInfo.onlyHeightRange {
   267  				continue
   268  			}
   269  			if !hashesInitialized {
   270  				filteredHashes = txi.matchRange(ctx, qr, startKey(qr.Key), filteredHashes, true, heightInfo)
   271  				hashesInitialized = true
   272  
   273  				// Ignore any remaining conditions if the first condition resulted
   274  				// in no matches (assuming implicit AND operand).
   275  				if len(filteredHashes) == 0 {
   276  					break
   277  				}
   278  			} else {
   279  				filteredHashes = txi.matchRange(ctx, qr, startKey(qr.Key), filteredHashes, false, heightInfo)
   280  			}
   281  		}
   282  	}
   283  
   284  	// if there is a height condition ("tx.height=3"), extract it
   285  
   286  	// for all other conditions
   287  	for i, c := range conditions {
   288  		if intInSlice(i, skipIndexes) {
   289  			continue
   290  		}
   291  
   292  		if !hashesInitialized {
   293  			filteredHashes = txi.match(ctx, c, startKeyForCondition(c, heightInfo.height), filteredHashes, true, heightInfo)
   294  			hashesInitialized = true
   295  
   296  			// Ignore any remaining conditions if the first condition resulted
   297  			// in no matches (assuming implicit AND operand).
   298  			if len(filteredHashes) == 0 {
   299  				break
   300  			}
   301  		} else {
   302  			filteredHashes = txi.match(ctx, c, startKeyForCondition(c, heightInfo.height), filteredHashes, false, heightInfo)
   303  		}
   304  	}
   305  
   306  	results := make([]*abci.TxResult, 0, len(filteredHashes))
   307  	resultMap := make(map[string]struct{})
   308  RESULTS_LOOP:
   309  	for _, h := range filteredHashes {
   310  
   311  		res, err := txi.Get(h)
   312  		if err != nil {
   313  			return nil, fmt.Errorf("failed to get Tx{%X}: %w", h, err)
   314  		}
   315  		hashString := string(h)
   316  		if _, ok := resultMap[hashString]; !ok {
   317  			resultMap[hashString] = struct{}{}
   318  			results = append(results, res)
   319  		}
   320  		// Potentially exit early.
   321  		select {
   322  		case <-ctx.Done():
   323  			break RESULTS_LOOP
   324  		default:
   325  		}
   326  	}
   327  
   328  	return results, nil
   329  }
   330  
   331  func lookForHash(conditions []syntax.Condition) (hash []byte, ok bool, err error) {
   332  	for _, c := range conditions {
   333  		if c.Tag == types.TxHashKey {
   334  			decoded, err := hex.DecodeString(c.Arg.Value())
   335  			return decoded, true, err
   336  		}
   337  	}
   338  	return
   339  }
   340  
   341  func (txi *TxIndex) setTmpHashes(tmpHeights map[string][]byte, it dbm.Iterator) {
   342  	eventSeq := extractEventSeqFromKey(it.Key())
   343  	tmpHeights[string(it.Value())+eventSeq] = it.Value()
   344  }
   345  
   346  // match returns all matching txs by hash that meet a given condition and start
   347  // key. An already filtered result (filteredHashes) is provided such that any
   348  // non-intersecting matches are removed.
   349  //
   350  // NOTE: filteredHashes may be empty if no previous condition has matched.
   351  func (txi *TxIndex) match(
   352  	ctx context.Context,
   353  	c syntax.Condition,
   354  	startKeyBz []byte,
   355  	filteredHashes map[string][]byte,
   356  	firstRun bool,
   357  	heightInfo HeightInfo,
   358  ) map[string][]byte {
   359  	// A previous match was attempted but resulted in no matches, so we return
   360  	// no matches (assuming AND operand).
   361  	if !firstRun && len(filteredHashes) == 0 {
   362  		return filteredHashes
   363  	}
   364  
   365  	tmpHashes := make(map[string][]byte)
   366  
   367  	switch {
   368  	case c.Op == syntax.TEq:
   369  		it, err := dbm.IteratePrefix(txi.store, startKeyBz)
   370  		if err != nil {
   371  			panic(err)
   372  		}
   373  		defer it.Close()
   374  
   375  	EQ_LOOP:
   376  		for ; it.Valid(); it.Next() {
   377  
   378  			// If we have a height range in a query, we need only transactions
   379  			// for this height
   380  			keyHeight, err := extractHeightFromKey(it.Key())
   381  			if err != nil {
   382  				txi.log.Error("failure to parse height from key:", err)
   383  				continue
   384  			}
   385  			withinBounds, err := checkHeightConditions(heightInfo, keyHeight)
   386  			if err != nil {
   387  				txi.log.Error("failure checking for height bounds:", err)
   388  				continue
   389  			}
   390  			if !withinBounds {
   391  				continue
   392  			}
   393  			txi.setTmpHashes(tmpHashes, it)
   394  			// Potentially exit early.
   395  			select {
   396  			case <-ctx.Done():
   397  				break EQ_LOOP
   398  			default:
   399  			}
   400  		}
   401  		if err := it.Error(); err != nil {
   402  			panic(err)
   403  		}
   404  
   405  	case c.Op == syntax.TExists:
   406  		// XXX: can't use startKeyBz here because c.Operand is nil
   407  		// (e.g. "account.owner/<nil>/" won't match w/ a single row)
   408  		it, err := dbm.IteratePrefix(txi.store, startKey(c.Tag))
   409  		if err != nil {
   410  			panic(err)
   411  		}
   412  		defer it.Close()
   413  
   414  	EXISTS_LOOP:
   415  		for ; it.Valid(); it.Next() {
   416  			keyHeight, err := extractHeightFromKey(it.Key())
   417  			if err != nil {
   418  				txi.log.Error("failure to parse height from key:", err)
   419  				continue
   420  			}
   421  			withinBounds, err := checkHeightConditions(heightInfo, keyHeight)
   422  			if err != nil {
   423  				txi.log.Error("failure checking for height bounds:", err)
   424  				continue
   425  			}
   426  			if !withinBounds {
   427  				continue
   428  			}
   429  			txi.setTmpHashes(tmpHashes, it)
   430  
   431  			// Potentially exit early.
   432  			select {
   433  			case <-ctx.Done():
   434  				break EXISTS_LOOP
   435  			default:
   436  			}
   437  		}
   438  		if err := it.Error(); err != nil {
   439  			panic(err)
   440  		}
   441  
   442  	case c.Op == syntax.TContains:
   443  		// XXX: startKey does not apply here.
   444  		// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
   445  		// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
   446  		it, err := dbm.IteratePrefix(txi.store, startKey(c.Tag))
   447  		if err != nil {
   448  			panic(err)
   449  		}
   450  		defer it.Close()
   451  
   452  	CONTAINS_LOOP:
   453  		for ; it.Valid(); it.Next() {
   454  			if !isTagKey(it.Key()) {
   455  				continue
   456  			}
   457  
   458  			if strings.Contains(extractValueFromKey(it.Key()), c.Arg.Value()) {
   459  				keyHeight, err := extractHeightFromKey(it.Key())
   460  				if err != nil {
   461  					txi.log.Error("failure to parse height from key:", err)
   462  					continue
   463  				}
   464  				withinBounds, err := checkHeightConditions(heightInfo, keyHeight)
   465  				if err != nil {
   466  					txi.log.Error("failure checking for height bounds:", err)
   467  					continue
   468  				}
   469  				if !withinBounds {
   470  					continue
   471  				}
   472  				txi.setTmpHashes(tmpHashes, it)
   473  			}
   474  
   475  			// Potentially exit early.
   476  			select {
   477  			case <-ctx.Done():
   478  				break CONTAINS_LOOP
   479  			default:
   480  			}
   481  		}
   482  		if err := it.Error(); err != nil {
   483  			panic(err)
   484  		}
   485  	default:
   486  		panic("other operators should be handled already")
   487  	}
   488  
   489  	if len(tmpHashes) == 0 || firstRun {
   490  		// Either:
   491  		//
   492  		// 1. Regardless if a previous match was attempted, which may have had
   493  		// results, but no match was found for the current condition, then we
   494  		// return no matches (assuming AND operand).
   495  		//
   496  		// 2. A previous match was not attempted, so we return all results.
   497  		return tmpHashes
   498  	}
   499  
   500  	// Remove/reduce matches in filteredHashes that were not found in this
   501  	// match (tmpHashes).
   502  REMOVE_LOOP:
   503  	for k, v := range filteredHashes {
   504  		tmpHash := tmpHashes[k]
   505  		if tmpHash == nil || !bytes.Equal(tmpHash, v) {
   506  			delete(filteredHashes, k)
   507  
   508  			// Potentially exit early.
   509  			select {
   510  			case <-ctx.Done():
   511  				break REMOVE_LOOP
   512  			default:
   513  			}
   514  		}
   515  	}
   516  
   517  	return filteredHashes
   518  }
   519  
   520  // matchRange returns all matching txs by hash that meet a given queryRange and
   521  // start key. An already filtered result (filteredHashes) is provided such that
   522  // any non-intersecting matches are removed.
   523  //
   524  // NOTE: filteredHashes may be empty if no previous condition has matched.
   525  func (txi *TxIndex) matchRange(
   526  	ctx context.Context,
   527  	qr indexer.QueryRange,
   528  	startKey []byte,
   529  	filteredHashes map[string][]byte,
   530  	firstRun bool,
   531  	heightInfo HeightInfo,
   532  ) map[string][]byte {
   533  	// A previous match was attempted but resulted in no matches, so we return
   534  	// no matches (assuming AND operand).
   535  	if !firstRun && len(filteredHashes) == 0 {
   536  		return filteredHashes
   537  	}
   538  
   539  	tmpHashes := make(map[string][]byte)
   540  
   541  	it, err := dbm.IteratePrefix(txi.store, startKey)
   542  	if err != nil {
   543  		panic(err)
   544  	}
   545  	defer it.Close()
   546  
   547  LOOP:
   548  	for ; it.Valid(); it.Next() {
   549  		if !isTagKey(it.Key()) {
   550  			continue
   551  		}
   552  
   553  		if _, ok := qr.AnyBound().(*big.Float); ok {
   554  			v := new(big.Int)
   555  			v, ok := v.SetString(extractValueFromKey(it.Key()), 10)
   556  			var vF *big.Float
   557  			if !ok {
   558  				vF, _, err = big.ParseFloat(extractValueFromKey(it.Key()), 10, 125, big.ToNearestEven)
   559  				if err != nil {
   560  					continue LOOP
   561  				}
   562  
   563  			}
   564  			if qr.Key != types.TxHeightKey {
   565  				keyHeight, err := extractHeightFromKey(it.Key())
   566  				if err != nil {
   567  					txi.log.Error("failure to parse height from key:", err)
   568  					continue
   569  				}
   570  				withinBounds, err := checkHeightConditions(heightInfo, keyHeight)
   571  				if err != nil {
   572  					txi.log.Error("failure checking for height bounds:", err)
   573  					continue
   574  				}
   575  				if !withinBounds {
   576  					continue
   577  				}
   578  			}
   579  			var withinBounds bool
   580  			var err error
   581  			if !ok {
   582  				withinBounds, err = idxutil.CheckBounds(qr, vF)
   583  			} else {
   584  				withinBounds, err = idxutil.CheckBounds(qr, v)
   585  			}
   586  			if err != nil {
   587  				txi.log.Error("failed to parse bounds:", err)
   588  			} else {
   589  				if withinBounds {
   590  					txi.setTmpHashes(tmpHashes, it)
   591  				}
   592  			}
   593  
   594  			// XXX: passing time in a ABCI Events is not yet implemented
   595  			// case time.Time:
   596  			// 	v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64)
   597  			// 	if v == r.upperBound {
   598  			// 		break
   599  			// 	}
   600  		}
   601  
   602  		// Potentially exit early.
   603  		select {
   604  		case <-ctx.Done():
   605  			break LOOP
   606  		default:
   607  		}
   608  	}
   609  	if err := it.Error(); err != nil {
   610  		panic(err)
   611  	}
   612  
   613  	if len(tmpHashes) == 0 || firstRun {
   614  		// Either:
   615  		//
   616  		// 1. Regardless if a previous match was attempted, which may have had
   617  		// results, but no match was found for the current condition, then we
   618  		// return no matches (assuming AND operand).
   619  		//
   620  		// 2. A previous match was not attempted, so we return all results.
   621  		return tmpHashes
   622  	}
   623  
   624  	// Remove/reduce matches in filteredHashes that were not found in this
   625  	// match (tmpHashes).
   626  REMOVE_LOOP:
   627  	for k, v := range filteredHashes {
   628  		tmpHash := tmpHashes[k]
   629  		if tmpHash == nil || !bytes.Equal(tmpHashes[k], v) {
   630  			delete(filteredHashes, k)
   631  
   632  			// Potentially exit early.
   633  			select {
   634  			case <-ctx.Done():
   635  				break REMOVE_LOOP
   636  			default:
   637  			}
   638  		}
   639  	}
   640  
   641  	return filteredHashes
   642  }
   643  
   644  // Keys
   645  
   646  func isTagKey(key []byte) bool {
   647  	// Normally, if the event was indexed with an event sequence, the number of
   648  	// tags should 4. Alternatively it should be 3 if the event was not indexed
   649  	// with the corresponding event sequence. However, some attribute values in
   650  	// production can contain the tag separator. Therefore, the condition is >= 3.
   651  	numTags := strings.Count(string(key), tagKeySeparator)
   652  	return numTags >= 3
   653  }
   654  
   655  func extractHeightFromKey(key []byte) (int64, error) {
   656  	parts := strings.SplitN(string(key), tagKeySeparator, -1)
   657  
   658  	return strconv.ParseInt(parts[len(parts)-2], 10, 64)
   659  }
   660  func extractValueFromKey(key []byte) string {
   661  	keyString := string(key)
   662  	parts := strings.SplitN(keyString, tagKeySeparator, -1)
   663  	partsLen := len(parts)
   664  	value := strings.TrimPrefix(keyString, parts[0]+tagKeySeparator)
   665  
   666  	suffix := ""
   667  	suffixLen := 2
   668  
   669  	for i := 1; i <= suffixLen; i++ {
   670  		suffix = tagKeySeparator + parts[partsLen-i] + suffix
   671  	}
   672  	return strings.TrimSuffix(value, suffix)
   673  
   674  }
   675  
   676  func extractEventSeqFromKey(key []byte) string {
   677  	parts := strings.SplitN(string(key), tagKeySeparator, -1)
   678  
   679  	lastEl := parts[len(parts)-1]
   680  
   681  	if strings.Contains(lastEl, eventSeqSeparator) {
   682  		return strings.SplitN(lastEl, eventSeqSeparator, 2)[1]
   683  	}
   684  	return "0"
   685  }
   686  func keyForEvent(key string, value string, result *abci.TxResult, eventSeq int64) []byte {
   687  	return []byte(fmt.Sprintf("%s/%s/%d/%d%s",
   688  		key,
   689  		value,
   690  		result.Height,
   691  		result.Index,
   692  		eventSeqSeparator+strconv.FormatInt(eventSeq, 10),
   693  	))
   694  }
   695  
   696  func keyForHeight(result *abci.TxResult) []byte {
   697  	return []byte(fmt.Sprintf("%s/%d/%d/%d%s",
   698  		types.TxHeightKey,
   699  		result.Height,
   700  		result.Height,
   701  		result.Index,
   702  		// Added to facilitate having the eventSeq in event keys
   703  		// Otherwise queries break expecting 5 entries
   704  		eventSeqSeparator+"0",
   705  	))
   706  }
   707  
   708  func startKeyForCondition(c syntax.Condition, height int64) []byte {
   709  	if height > 0 {
   710  		return startKey(c.Tag, c.Arg.Value(), height)
   711  	}
   712  	return startKey(c.Tag, c.Arg.Value())
   713  }
   714  
   715  func startKey(fields ...interface{}) []byte {
   716  	var b bytes.Buffer
   717  	for _, f := range fields {
   718  		b.Write([]byte(fmt.Sprintf("%v", f) + tagKeySeparator))
   719  	}
   720  	return b.Bytes()
   721  }