github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/state/indexer/block/kv/kv.go (about)

     1  package kv
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"math/big"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/google/orderedcode"
    14  
    15  	dbm "github.com/badrootd/nibiru-db"
    16  
    17  	abci "github.com/badrootd/nibiru-cometbft/abci/types"
    18  	"github.com/badrootd/nibiru-cometbft/libs/pubsub/query"
    19  	"github.com/badrootd/nibiru-cometbft/state/indexer"
    20  	"github.com/badrootd/nibiru-cometbft/types"
    21  )
    22  
    23  var _ indexer.BlockIndexer = (*BlockerIndexer)(nil)
    24  
    25  // BlockerIndexer implements a block indexer, indexing BeginBlock and EndBlock
    26  // events with an underlying KV store. Block events are indexed by their height,
    27  // such that matching search criteria returns the respective block height(s).
    28  type BlockerIndexer struct {
    29  	store dbm.DB
    30  
    31  	// Add unique event identifier to use when querying
    32  	// Matching will be done both on height AND eventSeq
    33  	eventSeq int64
    34  }
    35  
    36  func New(store dbm.DB) *BlockerIndexer {
    37  	return &BlockerIndexer{
    38  		store: store,
    39  	}
    40  }
    41  
    42  // Has returns true if the given height has been indexed. An error is returned
    43  // upon database query failure.
    44  func (idx *BlockerIndexer) Has(height int64) (bool, error) {
    45  	key, err := heightKey(height)
    46  	if err != nil {
    47  		return false, fmt.Errorf("failed to create block height index key: %w", err)
    48  	}
    49  
    50  	return idx.store.Has(key)
    51  }
    52  
    53  // Index indexes BeginBlock and EndBlock events for a given block by its height.
    54  // The following is indexed:
    55  //
    56  // primary key: encode(block.height | height) => encode(height)
    57  // BeginBlock events: encode(eventType.eventAttr|eventValue|height|begin_block) => encode(height)
    58  // EndBlock events: encode(eventType.eventAttr|eventValue|height|end_block) => encode(height)
    59  func (idx *BlockerIndexer) Index(bh types.EventDataNewBlockHeader) error {
    60  	batch := idx.store.NewBatch()
    61  	defer batch.Close()
    62  
    63  	height := bh.Header.Height
    64  
    65  	// 1. index by height
    66  	key, err := heightKey(height)
    67  	if err != nil {
    68  		return fmt.Errorf("failed to create block height index key: %w", err)
    69  	}
    70  	if err := batch.Set(key, int64ToBytes(height)); err != nil {
    71  		return err
    72  	}
    73  
    74  	// 2. index BeginBlock events
    75  	if err := idx.indexEvents(batch, bh.ResultBeginBlock.Events, "begin_block", height); err != nil {
    76  		return fmt.Errorf("failed to index BeginBlock events: %w", err)
    77  	}
    78  
    79  	// 3. index EndBlock events
    80  	if err := idx.indexEvents(batch, bh.ResultEndBlock.Events, "end_block", height); err != nil {
    81  		return fmt.Errorf("failed to index EndBlock events: %w", err)
    82  	}
    83  
    84  	return batch.WriteSync()
    85  }
    86  
    87  // Search performs a query for block heights that match a given BeginBlock
    88  // and Endblock event search criteria. The given query can match against zero,
    89  // one or more block heights. In the case of height queries, i.e. block.height=H,
    90  // if the height is indexed, that height alone will be returned. An error and
    91  // nil slice is returned. Otherwise, a non-nil slice and nil error is returned.
    92  func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64, error) {
    93  	results := make([]int64, 0)
    94  	select {
    95  	case <-ctx.Done():
    96  		return results, nil
    97  
    98  	default:
    99  	}
   100  
   101  	conditions, err := q.Conditions()
   102  	if err != nil {
   103  		return nil, fmt.Errorf("failed to parse query conditions: %w", err)
   104  	}
   105  	// conditions to skip because they're handled before "everything else"
   106  	skipIndexes := make([]int, 0)
   107  
   108  	var ok bool
   109  
   110  	var heightInfo HeightInfo
   111  	// If we are not matching events and block.height occurs more than once, the later value will
   112  	// overwrite the first one.
   113  	conditions, heightInfo, ok = dedupHeight(conditions)
   114  
   115  	// Extract ranges. If both upper and lower bounds exist, it's better to get
   116  	// them in order as to not iterate over kvs that are not within range.
   117  	ranges, rangeIndexes, heightRange := indexer.LookForRangesWithHeight(conditions)
   118  	heightInfo.heightRange = heightRange
   119  
   120  	// If we have additional constraints and want to query per event
   121  	// attributes, we cannot simply return all blocks for a height.
   122  	// But we remember the height we want to find and forward it to
   123  	// match(). If we only have the height constraint
   124  	// in the query (the second part of the ||), we don't need to query
   125  	// per event conditions and return all events within the height range.
   126  	if ok && heightInfo.onlyHeightEq {
   127  		ok, err := idx.Has(heightInfo.height)
   128  		if err != nil {
   129  			return nil, err
   130  		}
   131  
   132  		if ok {
   133  			return []int64{heightInfo.height}, nil
   134  		}
   135  
   136  		return results, nil
   137  	}
   138  
   139  	var heightsInitialized bool
   140  	filteredHeights := make(map[string][]byte)
   141  	if heightInfo.heightEqIdx != -1 {
   142  		skipIndexes = append(skipIndexes, heightInfo.heightEqIdx)
   143  	}
   144  
   145  	if len(ranges) > 0 {
   146  		skipIndexes = append(skipIndexes, rangeIndexes...)
   147  
   148  		for _, qr := range ranges {
   149  			// If we have a query range over height and want to still look for
   150  			// specific event values we do not want to simply return all
   151  			// blocks in this height range. We remember the height range info
   152  			// and pass it on to match() to take into account when processing events.
   153  			if qr.Key == types.BlockHeightKey && !heightInfo.onlyHeightRange {
   154  				// If the query contains ranges other than the height then we need to treat the height
   155  				// range when querying the conditions of the other range.
   156  				// Otherwise we can just return all the blocks within the height range (as there is no
   157  				// additional constraint on events)
   158  
   159  				continue
   160  
   161  			}
   162  			prefix, err := orderedcode.Append(nil, qr.Key)
   163  			if err != nil {
   164  				return nil, fmt.Errorf("failed to create prefix key: %w", err)
   165  			}
   166  
   167  			if !heightsInitialized {
   168  				filteredHeights, err = idx.matchRange(ctx, qr, prefix, filteredHeights, true, heightInfo)
   169  				if err != nil {
   170  					return nil, err
   171  				}
   172  
   173  				heightsInitialized = true
   174  
   175  				// Ignore any remaining conditions if the first condition resulted in no
   176  				// matches (assuming implicit AND operand).
   177  				if len(filteredHeights) == 0 {
   178  					break
   179  				}
   180  			} else {
   181  				filteredHeights, err = idx.matchRange(ctx, qr, prefix, filteredHeights, false, heightInfo)
   182  				if err != nil {
   183  					return nil, err
   184  				}
   185  			}
   186  		}
   187  	}
   188  
   189  	// for all other conditions
   190  	for i, c := range conditions {
   191  		if intInSlice(i, skipIndexes) {
   192  			continue
   193  		}
   194  
   195  		startKey, err := orderedcode.Append(nil, c.CompositeKey, fmt.Sprintf("%v", c.Operand))
   196  
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  
   201  		if !heightsInitialized {
   202  			filteredHeights, err = idx.match(ctx, c, startKey, filteredHeights, true, heightInfo)
   203  			if err != nil {
   204  				return nil, err
   205  			}
   206  
   207  			heightsInitialized = true
   208  
   209  			// Ignore any remaining conditions if the first condition resulted in no
   210  			// matches (assuming implicit AND operand).
   211  			if len(filteredHeights) == 0 {
   212  				break
   213  			}
   214  		} else {
   215  			filteredHeights, err = idx.match(ctx, c, startKey, filteredHeights, false, heightInfo)
   216  			if err != nil {
   217  				return nil, err
   218  			}
   219  		}
   220  	}
   221  
   222  	// fetch matching heights
   223  	results = make([]int64, 0, len(filteredHeights))
   224  	resultMap := make(map[int64]struct{})
   225  	for _, hBz := range filteredHeights {
   226  		h := int64FromBytes(hBz)
   227  
   228  		ok, err := idx.Has(h)
   229  		if err != nil {
   230  			return nil, err
   231  		}
   232  		if ok {
   233  			if _, ok := resultMap[h]; !ok {
   234  				resultMap[h] = struct{}{}
   235  				results = append(results, h)
   236  			}
   237  		}
   238  
   239  		select {
   240  		case <-ctx.Done():
   241  			break
   242  
   243  		default:
   244  		}
   245  	}
   246  
   247  	sort.Slice(results, func(i, j int) bool { return results[i] < results[j] })
   248  
   249  	return results, nil
   250  }
   251  
   252  // matchRange returns all matching block heights that match a given QueryRange
   253  // and start key. An already filtered result (filteredHeights) is provided such
   254  // that any non-intersecting matches are removed.
   255  //
   256  // NOTE: The provided filteredHeights may be empty if no previous condition has
   257  // matched.
   258  func (idx *BlockerIndexer) matchRange(
   259  	ctx context.Context,
   260  	qr indexer.QueryRange,
   261  	startKey []byte,
   262  	filteredHeights map[string][]byte,
   263  	firstRun bool,
   264  	heightInfo HeightInfo,
   265  ) (map[string][]byte, error) {
   266  	// A previous match was attempted but resulted in no matches, so we return
   267  	// no matches (assuming AND operand).
   268  	if !firstRun && len(filteredHeights) == 0 {
   269  		return filteredHeights, nil
   270  	}
   271  
   272  	tmpHeights := make(map[string][]byte)
   273  
   274  	it, err := dbm.IteratePrefix(idx.store, startKey)
   275  	if err != nil {
   276  		return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
   277  	}
   278  	defer it.Close()
   279  
   280  LOOP:
   281  	for ; it.Valid(); it.Next() {
   282  		var (
   283  			eventValue string
   284  			err        error
   285  		)
   286  
   287  		if qr.Key == types.BlockHeightKey {
   288  			eventValue, err = parseValueFromPrimaryKey(it.Key())
   289  		} else {
   290  			eventValue, err = parseValueFromEventKey(it.Key())
   291  		}
   292  
   293  		if err != nil {
   294  			continue
   295  		}
   296  
   297  		if _, ok := qr.AnyBound().(*big.Int); ok {
   298  			v := new(big.Int)
   299  			v, ok := v.SetString(eventValue, 10)
   300  			if !ok { // If the number was not int it might be a float but this behavior is kept the same as before the patch
   301  				continue LOOP
   302  			}
   303  
   304  			if qr.Key != types.BlockHeightKey {
   305  				keyHeight, err := parseHeightFromEventKey(it.Key())
   306  				if err != nil || !checkHeightConditions(heightInfo, keyHeight) {
   307  					continue LOOP
   308  				}
   309  			}
   310  			if checkBounds(qr, v) {
   311  				idx.setTmpHeights(tmpHeights, it)
   312  			}
   313  		}
   314  
   315  		select {
   316  		case <-ctx.Done():
   317  			break
   318  
   319  		default:
   320  		}
   321  	}
   322  
   323  	if err := it.Error(); err != nil {
   324  		return nil, err
   325  	}
   326  
   327  	if len(tmpHeights) == 0 || firstRun {
   328  		// Either:
   329  		//
   330  		// 1. Regardless if a previous match was attempted, which may have had
   331  		// results, but no match was found for the current condition, then we
   332  		// return no matches (assuming AND operand).
   333  		//
   334  		// 2. A previous match was not attempted, so we return all results.
   335  		return tmpHeights, nil
   336  	}
   337  
   338  	// Remove/reduce matches in filteredHashes that were not found in this
   339  	// match (tmpHashes).
   340  	for k, v := range filteredHeights {
   341  		tmpHeight := tmpHeights[k]
   342  
   343  		// Check whether in this iteration we have not found an overlapping height (tmpHeight == nil)
   344  		// or whether the events in which the attributed occurred do not match (first part of the condition)
   345  		if tmpHeight == nil || !bytes.Equal(tmpHeight, v) {
   346  			delete(filteredHeights, k)
   347  
   348  			select {
   349  			case <-ctx.Done():
   350  				break
   351  
   352  			default:
   353  			}
   354  		}
   355  	}
   356  
   357  	return filteredHeights, nil
   358  }
   359  
   360  func (idx *BlockerIndexer) setTmpHeights(tmpHeights map[string][]byte, it dbm.Iterator) {
   361  	// If we return attributes that occur within the same events, then store the event sequence in the
   362  	// result map as well
   363  	eventSeq, _ := parseEventSeqFromEventKey(it.Key())
   364  	retVal := it.Value()
   365  	tmpHeights[string(retVal)+strconv.FormatInt(eventSeq, 10)] = it.Value()
   366  
   367  }
   368  
   369  func checkBounds(ranges indexer.QueryRange, v *big.Int) bool {
   370  	include := true
   371  	lowerBound := ranges.LowerBoundValue()
   372  	upperBound := ranges.UpperBoundValue()
   373  
   374  	if lowerBound != nil && v.Cmp(lowerBound.(*big.Int)) == -1 {
   375  		include = false
   376  	}
   377  
   378  	if upperBound != nil && v.Cmp(upperBound.(*big.Int)) == 1 {
   379  		include = false
   380  	}
   381  
   382  	return include
   383  }
   384  
   385  // match returns all matching heights that meet a given query condition and start
   386  // key. An already filtered result (filteredHeights) is provided such that any
   387  // non-intersecting matches are removed.
   388  //
   389  // NOTE: The provided filteredHeights may be empty if no previous condition has
   390  // matched.
   391  func (idx *BlockerIndexer) match(
   392  	ctx context.Context,
   393  	c query.Condition,
   394  	startKeyBz []byte,
   395  	filteredHeights map[string][]byte,
   396  	firstRun bool,
   397  	heightInfo HeightInfo,
   398  ) (map[string][]byte, error) {
   399  	// A previous match was attempted but resulted in no matches, so we return
   400  	// no matches (assuming AND operand).
   401  	if !firstRun && len(filteredHeights) == 0 {
   402  		return filteredHeights, nil
   403  	}
   404  
   405  	tmpHeights := make(map[string][]byte)
   406  
   407  	switch {
   408  	case c.Op == query.OpEqual:
   409  		it, err := dbm.IteratePrefix(idx.store, startKeyBz)
   410  		if err != nil {
   411  			return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
   412  		}
   413  		defer it.Close()
   414  
   415  		for ; it.Valid(); it.Next() {
   416  
   417  			keyHeight, err := parseHeightFromEventKey(it.Key())
   418  			if err != nil || !checkHeightConditions(heightInfo, keyHeight) {
   419  				continue
   420  			}
   421  
   422  			idx.setTmpHeights(tmpHeights, it)
   423  
   424  			if err := ctx.Err(); err != nil {
   425  				break
   426  			}
   427  		}
   428  
   429  		if err := it.Error(); err != nil {
   430  			return nil, err
   431  		}
   432  
   433  	case c.Op == query.OpExists:
   434  		prefix, err := orderedcode.Append(nil, c.CompositeKey)
   435  		if err != nil {
   436  			return nil, err
   437  		}
   438  
   439  		it, err := dbm.IteratePrefix(idx.store, prefix)
   440  		if err != nil {
   441  			return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
   442  		}
   443  		defer it.Close()
   444  
   445  		for ; it.Valid(); it.Next() {
   446  			keyHeight, err := parseHeightFromEventKey(it.Key())
   447  			if err != nil || !checkHeightConditions(heightInfo, keyHeight) {
   448  				continue
   449  			}
   450  			idx.setTmpHeights(tmpHeights, it)
   451  
   452  			select {
   453  			case <-ctx.Done():
   454  				break
   455  
   456  			default:
   457  			}
   458  		}
   459  
   460  		if err := it.Error(); err != nil {
   461  			return nil, err
   462  		}
   463  
   464  	case c.Op == query.OpContains:
   465  		prefix, err := orderedcode.Append(nil, c.CompositeKey)
   466  		if err != nil {
   467  			return nil, err
   468  		}
   469  
   470  		it, err := dbm.IteratePrefix(idx.store, prefix)
   471  		if err != nil {
   472  			return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
   473  		}
   474  		defer it.Close()
   475  
   476  		for ; it.Valid(); it.Next() {
   477  			eventValue, err := parseValueFromEventKey(it.Key())
   478  			if err != nil {
   479  				continue
   480  			}
   481  
   482  			if strings.Contains(eventValue, c.Operand.(string)) {
   483  				keyHeight, err := parseHeightFromEventKey(it.Key())
   484  				if err != nil || !checkHeightConditions(heightInfo, keyHeight) {
   485  					continue
   486  				}
   487  				idx.setTmpHeights(tmpHeights, it)
   488  			}
   489  
   490  			select {
   491  			case <-ctx.Done():
   492  				break
   493  
   494  			default:
   495  			}
   496  		}
   497  		if err := it.Error(); err != nil {
   498  			return nil, err
   499  		}
   500  
   501  	default:
   502  		return nil, errors.New("other operators should be handled already")
   503  	}
   504  
   505  	if len(tmpHeights) == 0 || firstRun {
   506  		// Either:
   507  		//
   508  		// 1. Regardless if a previous match was attempted, which may have had
   509  		// results, but no match was found for the current condition, then we
   510  		// return no matches (assuming AND operand).
   511  		//
   512  		// 2. A previous match was not attempted, so we return all results.
   513  		return tmpHeights, nil
   514  	}
   515  
   516  	// Remove/reduce matches in filteredHeights that were not found in this
   517  	// match (tmpHeights).
   518  	for k, v := range filteredHeights {
   519  		tmpHeight := tmpHeights[k]
   520  		if tmpHeight == nil || !bytes.Equal(tmpHeight, v) {
   521  			delete(filteredHeights, k)
   522  
   523  			select {
   524  			case <-ctx.Done():
   525  				break
   526  
   527  			default:
   528  			}
   529  		}
   530  	}
   531  
   532  	return filteredHeights, nil
   533  }
   534  
   535  func (idx *BlockerIndexer) indexEvents(batch dbm.Batch, events []abci.Event, typ string, height int64) error {
   536  	heightBz := int64ToBytes(height)
   537  
   538  	for _, event := range events {
   539  		idx.eventSeq = idx.eventSeq + 1
   540  		// only index events with a non-empty type
   541  		if len(event.Type) == 0 {
   542  			continue
   543  		}
   544  
   545  		for _, attr := range event.Attributes {
   546  			if len(attr.Key) == 0 {
   547  				continue
   548  			}
   549  
   550  			// index iff the event specified index:true and it's not a reserved event
   551  			compositeKey := fmt.Sprintf("%s.%s", event.Type, attr.Key)
   552  			if compositeKey == types.BlockHeightKey {
   553  				return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeKey)
   554  			}
   555  
   556  			if attr.GetIndex() {
   557  				key, err := eventKey(compositeKey, typ, attr.Value, height, idx.eventSeq)
   558  				if err != nil {
   559  					return fmt.Errorf("failed to create block index key: %w", err)
   560  				}
   561  
   562  				if err := batch.Set(key, heightBz); err != nil {
   563  					return err
   564  				}
   565  			}
   566  		}
   567  	}
   568  
   569  	return nil
   570  }