github.com/iotexproject/iotex-core@v1.14.1-rc1/blockindex/bloomfilterindexer.go (about)

     1  // Copyright (c) 2020 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package blockindex
     7  
     8  import (
     9  	"context"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/iotexproject/go-pkgs/bloom"
    14  	"github.com/pkg/errors"
    15  	"golang.org/x/sync/errgroup"
    16  
    17  	"github.com/iotexproject/iotex-core/action"
    18  	filter "github.com/iotexproject/iotex-core/api/logfilter"
    19  	"github.com/iotexproject/iotex-core/blockchain/block"
    20  	"github.com/iotexproject/iotex-core/blockchain/blockdao"
    21  	"github.com/iotexproject/iotex-core/db"
    22  	"github.com/iotexproject/iotex-core/db/batch"
    23  	"github.com/iotexproject/iotex-core/pkg/util/byteutil"
    24  )
    25  
    26  const (
    27  	// BlockBloomFilterNamespace indicated the kvstore namespace to store block BloomFilters
    28  	BlockBloomFilterNamespace = "BlockBloomFilters"
    29  	// RangeBloomFilterNamespace indicates the kvstore namespace to store range BloomFilters
    30  	RangeBloomFilterNamespace = "RangeBloomFilters"
    31  	// CurrentHeightKey indicates the key of current bf indexer height in underlying DB
    32  	CurrentHeightKey = "CurrentHeight"
    33  )
    34  
    35  const (
    36  	_maxBlockRange = 1e6
    37  	_workerNumbers = 5
    38  )
    39  
    40  var (
    41  	// TotalBloomFilterNamespace indicates the kvstore namespace to store total ranges
    42  	TotalBloomFilterNamespace = []byte("TotalBloomFilters")
    43  
    44  	errRangeTooLarge = errors.New("block range is too large")
    45  
    46  	_queryTimeout = 20 * time.Second
    47  )
    48  
    49  type (
    50  	// BloomFilterIndexer is the interface for bloomfilter indexer
    51  	BloomFilterIndexer interface {
    52  		blockdao.BlockIndexer
    53  		// RangeBloomFilterNumElements returns the number of elements that each rangeBloomfilter indexes
    54  		RangeBloomFilterNumElements() uint64
    55  		// BlockFilterByHeight returns the block-level bloomfilter which includes not only topic but also address of logs info by given block height
    56  		BlockFilterByHeight(uint64) (bloom.BloomFilter, error)
    57  		// FilterBlocksInRange returns the block numbers by given logFilter in range from start to end
    58  		FilterBlocksInRange(*filter.LogFilter, uint64, uint64, uint64) ([]uint64, error)
    59  	}
    60  
    61  	// bloomfilterIndexer is a struct for bloomfilter indexer
    62  	bloomfilterIndexer struct {
    63  		mutex               sync.RWMutex // mutex for curRangeBloomfilter
    64  		kvStore             db.KVStore
    65  		rangeSize           uint64
    66  		bfSize              uint64
    67  		bfNumHash           uint64
    68  		currRangeBfKey      []byte
    69  		curRangeBloomfilter *bloomRange
    70  		totalRange          db.RangeIndex
    71  	}
    72  
    73  	jobDesc struct {
    74  		idx uint64
    75  		key []byte
    76  	}
    77  )
    78  
    79  // NewBloomfilterIndexer creates a new bloomfilterindexer struct by given kvstore and rangebloomfilter size
    80  func NewBloomfilterIndexer(kv db.KVStore, cfg Config) (BloomFilterIndexer, error) {
    81  	if kv == nil {
    82  		return nil, errors.New("empty kvStore")
    83  	}
    84  
    85  	return &bloomfilterIndexer{
    86  		kvStore:   kv,
    87  		rangeSize: cfg.RangeBloomFilterNumElements,
    88  		bfSize:    cfg.RangeBloomFilterSize,
    89  		bfNumHash: cfg.RangeBloomFilterNumHash,
    90  	}, nil
    91  }
    92  
    93  // Start starts the bloomfilter indexer
    94  func (bfx *bloomfilterIndexer) Start(ctx context.Context) error {
    95  	if err := bfx.kvStore.Start(ctx); err != nil {
    96  		return err
    97  	}
    98  
    99  	bfx.mutex.Lock()
   100  	defer bfx.mutex.Unlock()
   101  	tipHeightData, err := bfx.kvStore.Get(RangeBloomFilterNamespace, []byte(CurrentHeightKey))
   102  	switch errors.Cause(err) {
   103  	case nil:
   104  		tipHeight := byteutil.BytesToUint64BigEndian(tipHeightData)
   105  		return bfx.initRangeBloomFilter(tipHeight)
   106  	case db.ErrNotExist:
   107  		if err = bfx.kvStore.Put(RangeBloomFilterNamespace, []byte(CurrentHeightKey), byteutil.Uint64ToBytes(0)); err != nil {
   108  			return err
   109  		}
   110  		return bfx.initRangeBloomFilter(0)
   111  	default:
   112  		return err
   113  	}
   114  }
   115  
   116  func (bfx *bloomfilterIndexer) initRangeBloomFilter(height uint64) error {
   117  	var (
   118  		err        error
   119  		zero8Bytes = make([]byte, 8)
   120  	)
   121  	bfx.totalRange, err = db.NewRangeIndex(bfx.kvStore, TotalBloomFilterNamespace, zero8Bytes)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	if bfx.curRangeBloomfilter, err = newBloomRange(bfx.bfSize, bfx.bfNumHash); err != nil {
   126  		return err
   127  	}
   128  	if height > 0 {
   129  		bfx.currRangeBfKey, err = bfx.totalRange.Get(height)
   130  		if err != nil {
   131  			return err
   132  		}
   133  		if err := bfx.loadBloomRangeFromDB(bfx.curRangeBloomfilter, bfx.currRangeBfKey); err != nil {
   134  			return err
   135  		}
   136  	} else {
   137  		bfx.curRangeBloomfilter.SetStart(1)
   138  		bfx.currRangeBfKey = zero8Bytes
   139  	}
   140  	return nil
   141  }
   142  
   143  // Stop stops the bloomfilter indexer
   144  func (bfx *bloomfilterIndexer) Stop(ctx context.Context) error {
   145  	bfx.totalRange.Close()
   146  	return bfx.kvStore.Stop(ctx)
   147  }
   148  
   149  // Height returns the tipHeight from underlying DB
   150  func (bfx *bloomfilterIndexer) Height() (uint64, error) {
   151  	h, err := bfx.kvStore.Get(RangeBloomFilterNamespace, []byte(CurrentHeightKey))
   152  	if err != nil {
   153  		return 0, err
   154  	}
   155  	return byteutil.BytesToUint64BigEndian(h), nil
   156  }
   157  
   158  // PutBlock processes new block by adding logs into rangebloomfilter, and if necessary, updating underlying DB
   159  func (bfx *bloomfilterIndexer) PutBlock(ctx context.Context, blk *block.Block) (err error) {
   160  	bfx.mutex.Lock()
   161  	defer bfx.mutex.Unlock()
   162  	bfx.addLogsToRangeBloomFilter(ctx, blk.Height(), blk.Receipts)
   163  	// commit into DB and update tipHeight
   164  	if err := bfx.commit(blk.Height(), bfx.calculateBlockBloomFilter(ctx, blk.Receipts)); err != nil {
   165  		return err
   166  	}
   167  	if bfx.curRangeBloomfilter.NumElements() >= bfx.rangeSize {
   168  		nextIndex := byteutil.BytesToUint64BigEndian(bfx.currRangeBfKey) + 1
   169  		bfx.currRangeBfKey = byteutil.Uint64ToBytesBigEndian(nextIndex)
   170  		if err := bfx.totalRange.Insert(blk.Height()+1, bfx.currRangeBfKey); err != nil {
   171  			return errors.Wrapf(err, "failed to write next bloomfilter index")
   172  		}
   173  		if bfx.curRangeBloomfilter, err = newBloomRange(bfx.bfSize, bfx.bfNumHash); err != nil {
   174  			return err
   175  		}
   176  		bfx.curRangeBloomfilter.SetStart(blk.Height() + 1)
   177  	}
   178  	return nil
   179  }
   180  
   181  // DeleteTipBlock deletes tip height from underlying DB if necessary
   182  func (bfx *bloomfilterIndexer) DeleteTipBlock(_ context.Context, blk *block.Block) (err error) {
   183  	bfx.mutex.Lock()
   184  	defer bfx.mutex.Unlock()
   185  	height := blk.Height()
   186  	if err := bfx.kvStore.Delete(BlockBloomFilterNamespace, byteutil.Uint64ToBytesBigEndian(height)); err != nil {
   187  		return err
   188  	}
   189  	bfx.curRangeBloomfilter = nil
   190  	return nil
   191  }
   192  
   193  // RangeBloomFilterNumElements returns the number of elements that each rangeBloomfilter indexes
   194  func (bfx *bloomfilterIndexer) RangeBloomFilterNumElements() uint64 {
   195  	bfx.mutex.RLock()
   196  	defer bfx.mutex.RUnlock()
   197  	return bfx.rangeSize
   198  }
   199  
   200  // BlockFilterByHeight returns the block-level bloomfilter which includes not only topic but also address of logs info by given block height
   201  func (bfx *bloomfilterIndexer) BlockFilterByHeight(height uint64) (bloom.BloomFilter, error) {
   202  	bfBytes, err := bfx.kvStore.Get(BlockBloomFilterNamespace, byteutil.Uint64ToBytesBigEndian(height))
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  	bf, err := bloom.NewBloomFilter(bfx.bfSize, bfx.bfNumHash)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	if err := bf.FromBytes(bfBytes); err != nil {
   211  		return nil, err
   212  	}
   213  	return bf, nil
   214  }
   215  
   216  // FilterBlocksInRange returns the block numbers by given logFilter in range [start, end].
   217  // Result blocks are limited when pagination is larger than 0
   218  func (bfx *bloomfilterIndexer) FilterBlocksInRange(l *filter.LogFilter, start, end uint64, pagination uint64) ([]uint64, error) {
   219  	if start == 0 || end == 0 || end < start {
   220  		return nil, errors.New("start/end height should be bigger than zero")
   221  	}
   222  	if end-start > _maxBlockRange {
   223  		return nil, errRangeTooLarge
   224  	}
   225  	var (
   226  		startIndex, endIndex uint64
   227  		err                  error
   228  	)
   229  	if startIndex, err = bfx.getIndexByHeight(start); err != nil {
   230  		return nil, err
   231  	}
   232  	if endIndex, err = bfx.getIndexByHeight(end); err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	var (
   237  		ctx, cancel = context.WithTimeout(context.Background(), _queryTimeout)
   238  		blkNums     = make([][]uint64, endIndex-startIndex+1)
   239  		jobs        = make(chan jobDesc, endIndex-startIndex+1)
   240  		eg          *errgroup.Group
   241  		bufPool     sync.Pool
   242  	)
   243  	defer cancel()
   244  	eg, ctx = errgroup.WithContext(ctx)
   245  
   246  	// create pool for BloomRange object reusing
   247  	if _, err := newBloomRange(bfx.bfSize, bfx.bfNumHash); err != nil {
   248  		return nil, err
   249  	}
   250  	bufPool = sync.Pool{
   251  		New: func() interface{} {
   252  			br, _ := newBloomRange(bfx.bfSize, bfx.bfNumHash)
   253  			return br
   254  		},
   255  	}
   256  
   257  	for w := 0; w < _workerNumbers; w++ {
   258  		eg.Go(func() error {
   259  			for {
   260  				select {
   261  				case <-ctx.Done():
   262  					return ctx.Err()
   263  				case job, ok := <-jobs:
   264  					if !ok {
   265  						return nil
   266  					}
   267  					br := bufPool.Get().(*bloomRange)
   268  					if err := bfx.loadBloomRangeFromDB(br, job.key); err != nil {
   269  						bufPool.Put(br)
   270  						return err
   271  					}
   272  					if l.ExistInBloomFilterv2(br.BloomFilter) {
   273  						searchStart := br.Start()
   274  						if start > searchStart {
   275  							searchStart = start
   276  						}
   277  						searchEnd := br.End()
   278  						if end < searchEnd {
   279  							searchEnd = end
   280  						}
   281  						blkNums[job.idx] = l.SelectBlocksFromRangeBloomFilter(br.BloomFilter, searchStart, searchEnd)
   282  					}
   283  					bufPool.Put(br)
   284  				}
   285  			}
   286  		})
   287  	}
   288  
   289  	// send job to job chan
   290  	for idx := startIndex; idx <= endIndex; idx++ {
   291  		jobs <- jobDesc{idx - startIndex, byteutil.Uint64ToBytesBigEndian(idx)}
   292  	}
   293  	close(jobs)
   294  
   295  	if err := eg.Wait(); err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	// collect results from goroutines
   300  	ret := []uint64{}
   301  	for i := range blkNums {
   302  		if len(blkNums[i]) > 0 {
   303  			ret = append(ret, blkNums[i]...)
   304  			if pagination > 0 && uint64(len(ret)) > pagination {
   305  				return ret, nil
   306  			}
   307  		}
   308  	}
   309  	return ret, nil
   310  }
   311  
   312  func (bfx *bloomfilterIndexer) commit(blockNumber uint64, blkBloomfilter bloom.BloomFilter) error {
   313  	bfx.curRangeBloomfilter.SetEnd(blockNumber)
   314  	bfBytes, err := bfx.curRangeBloomfilter.Bytes()
   315  	if err != nil {
   316  		return err
   317  	}
   318  	b := batch.NewBatch()
   319  	b.Put(RangeBloomFilterNamespace, bfx.currRangeBfKey, bfBytes, "failed to put range bloom filter")
   320  	b.Put(BlockBloomFilterNamespace, byteutil.Uint64ToBytesBigEndian(blockNumber), blkBloomfilter.Bytes(), "failed to put block bloom filter")
   321  	b.Put(RangeBloomFilterNamespace, []byte(CurrentHeightKey), byteutil.Uint64ToBytesBigEndian(blockNumber), "failed to put current height")
   322  	b.AddFillPercent(RangeBloomFilterNamespace, 1.0)
   323  	b.AddFillPercent(BlockBloomFilterNamespace, 1.0)
   324  	return bfx.kvStore.WriteBatch(b)
   325  }
   326  
   327  // TODO: improve performance
   328  func (bfx *bloomfilterIndexer) calculateBlockBloomFilter(ctx context.Context, receipts []*action.Receipt) bloom.BloomFilter {
   329  	bloom, _ := bloom.NewBloomFilter(2048, 3)
   330  	for _, receipt := range receipts {
   331  		for _, l := range receipt.Logs() {
   332  			bloom.Add([]byte(l.Address))
   333  			for i, topic := range l.Topics {
   334  				bloom.Add(append(byteutil.Uint64ToBytes(uint64(i)), topic[:]...)) //position-sensitive
   335  			}
   336  		}
   337  	}
   338  	return bloom
   339  }
   340  
   341  // TODO: improve performance
   342  func (bfx *bloomfilterIndexer) addLogsToRangeBloomFilter(ctx context.Context, blockNumber uint64, receipts []*action.Receipt) {
   343  	Heightkey := append([]byte(filter.BlockHeightPrefix), byteutil.Uint64ToBytes(blockNumber)...)
   344  
   345  	for _, receipt := range receipts {
   346  		for _, l := range receipt.Logs() {
   347  			bfx.curRangeBloomfilter.Add([]byte(l.Address))
   348  			bfx.curRangeBloomfilter.Add(append(Heightkey, []byte(l.Address)...)) // concatenate with block number
   349  			for i, topic := range l.Topics {
   350  				bfx.curRangeBloomfilter.Add(append(byteutil.Uint64ToBytes(uint64(i)), topic[:]...)) //position-sensitive
   351  				bfx.curRangeBloomfilter.Add(append(Heightkey, topic[:]...))                         // concatenate with block number
   352  			}
   353  		}
   354  	}
   355  }
   356  
   357  func (bfx *bloomfilterIndexer) loadBloomRangeFromDB(br *bloomRange, bfKey []byte) error {
   358  	if br == nil {
   359  		return errors.New("bloomRange is empty")
   360  	}
   361  	bfBytes, err := bfx.kvStore.Get(RangeBloomFilterNamespace, bfKey)
   362  	if err != nil {
   363  		return err
   364  	}
   365  	return br.FromBytes(bfBytes)
   366  }
   367  
   368  func (bfx *bloomfilterIndexer) getIndexByHeight(height uint64) (uint64, error) {
   369  	val, err := bfx.totalRange.Get(height)
   370  	if err != nil {
   371  		return 0, err
   372  	}
   373  	return byteutil.BytesToUint64BigEndian(val), nil
   374  }