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