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

     1  // Copyright (c) 2023 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 contractstaking
     7  
     8  import (
     9  	"context"
    10  	"math/big"
    11  	"time"
    12  
    13  	"github.com/ethereum/go-ethereum/common/math"
    14  	"github.com/iotexproject/iotex-address/address"
    15  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    16  	"github.com/pkg/errors"
    17  
    18  	"github.com/iotexproject/iotex-core/blockchain/block"
    19  	"github.com/iotexproject/iotex-core/db"
    20  	"github.com/iotexproject/iotex-core/pkg/util/byteutil"
    21  )
    22  
    23  const (
    24  	maxBlockNumber uint64 = math.MaxUint64
    25  )
    26  
    27  type (
    28  	// Indexer is the contract staking indexer
    29  	// Main functions:
    30  	// 		1. handle contract staking contract events when new block comes to generate index data
    31  	// 		2. provide query interface for contract staking index data
    32  	Indexer struct {
    33  		kvstore db.KVStore            // persistent storage, used to initialize index cache at startup
    34  		cache   *contractStakingCache // in-memory index for clean data, used to query index data
    35  		config  Config                // indexer config
    36  	}
    37  
    38  	// Config is the config for contract staking indexer
    39  	Config struct {
    40  		ContractAddress      string // stake contract ContractAddress
    41  		ContractDeployHeight uint64 // height of the contract deployment
    42  		// TODO: move calculateVoteWeightFunc out of config
    43  		CalculateVoteWeight calculateVoteWeightFunc // calculate vote weight function
    44  		BlockInterval       time.Duration           // block produce interval
    45  	}
    46  
    47  	calculateVoteWeightFunc func(v *Bucket) *big.Int
    48  )
    49  
    50  // NewContractStakingIndexer creates a new contract staking indexer
    51  func NewContractStakingIndexer(kvStore db.KVStore, config Config) (*Indexer, error) {
    52  	if kvStore == nil {
    53  		return nil, errors.New("kv store is nil")
    54  	}
    55  	if _, err := address.FromString(config.ContractAddress); err != nil {
    56  		return nil, errors.Wrapf(err, "invalid contract address %s", config.ContractAddress)
    57  	}
    58  	if config.CalculateVoteWeight == nil {
    59  		return nil, errors.New("calculate vote weight function is nil")
    60  	}
    61  	return &Indexer{
    62  		kvstore: kvStore,
    63  		cache:   newContractStakingCache(config),
    64  		config:  config,
    65  	}, nil
    66  }
    67  
    68  // Start starts the indexer
    69  func (s *Indexer) Start(ctx context.Context) error {
    70  	if err := s.kvstore.Start(ctx); err != nil {
    71  		return err
    72  	}
    73  	return s.loadFromDB()
    74  }
    75  
    76  // Stop stops the indexer
    77  func (s *Indexer) Stop(ctx context.Context) error {
    78  	if err := s.kvstore.Stop(ctx); err != nil {
    79  		return err
    80  	}
    81  	s.cache = newContractStakingCache(s.config)
    82  	return nil
    83  }
    84  
    85  // Height returns the tip block height
    86  func (s *Indexer) Height() (uint64, error) {
    87  	return s.cache.Height(), nil
    88  }
    89  
    90  // StartHeight returns the start height of the indexer
    91  func (s *Indexer) StartHeight() uint64 {
    92  	return s.config.ContractDeployHeight
    93  }
    94  
    95  // CandidateVotes returns the candidate votes
    96  func (s *Indexer) CandidateVotes(ctx context.Context, candidate address.Address, height uint64) (*big.Int, error) {
    97  	if s.isIgnored(height) {
    98  		return big.NewInt(0), nil
    99  	}
   100  	return s.cache.CandidateVotes(ctx, candidate, height)
   101  }
   102  
   103  // Buckets returns the buckets
   104  func (s *Indexer) Buckets(height uint64) ([]*Bucket, error) {
   105  	if s.isIgnored(height) {
   106  		return []*Bucket{}, nil
   107  	}
   108  	return s.cache.Buckets(height)
   109  }
   110  
   111  // Bucket returns the bucket
   112  func (s *Indexer) Bucket(id uint64, height uint64) (*Bucket, bool, error) {
   113  	if s.isIgnored(height) {
   114  		return nil, false, nil
   115  	}
   116  	return s.cache.Bucket(id, height)
   117  }
   118  
   119  // BucketsByIndices returns the buckets by indices
   120  func (s *Indexer) BucketsByIndices(indices []uint64, height uint64) ([]*Bucket, error) {
   121  	if s.isIgnored(height) {
   122  		return []*Bucket{}, nil
   123  	}
   124  	return s.cache.BucketsByIndices(indices, height)
   125  }
   126  
   127  // BucketsByCandidate returns the buckets by candidate
   128  func (s *Indexer) BucketsByCandidate(candidate address.Address, height uint64) ([]*Bucket, error) {
   129  	if s.isIgnored(height) {
   130  		return []*Bucket{}, nil
   131  	}
   132  	return s.cache.BucketsByCandidate(candidate, height)
   133  }
   134  
   135  // TotalBucketCount returns the total bucket count including active and burnt buckets
   136  func (s *Indexer) TotalBucketCount(height uint64) (uint64, error) {
   137  	if s.isIgnored(height) {
   138  		return 0, nil
   139  	}
   140  	return s.cache.TotalBucketCount(height)
   141  }
   142  
   143  // BucketTypes returns the active bucket types
   144  func (s *Indexer) BucketTypes(height uint64) ([]*BucketType, error) {
   145  	if s.isIgnored(height) {
   146  		return []*BucketType{}, nil
   147  	}
   148  	btMap, err := s.cache.ActiveBucketTypes(height)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	bts := make([]*BucketType, 0, len(btMap))
   153  	for _, bt := range btMap {
   154  		bts = append(bts, bt)
   155  	}
   156  	return bts, nil
   157  }
   158  
   159  // PutBlock puts a block into indexer
   160  func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error {
   161  	expectHeight := s.cache.Height() + 1
   162  	if expectHeight < s.config.ContractDeployHeight {
   163  		expectHeight = s.config.ContractDeployHeight
   164  	}
   165  	if blk.Height() < expectHeight {
   166  		return nil
   167  	}
   168  	if blk.Height() > expectHeight {
   169  		return errors.Errorf("invalid block height %d, expect %d", blk.Height(), expectHeight)
   170  	}
   171  	// new event handler for this block
   172  	handler := newContractStakingEventHandler(s.cache)
   173  
   174  	// handle events of block
   175  	for _, receipt := range blk.Receipts {
   176  		if receipt.Status != uint64(iotextypes.ReceiptStatus_Success) {
   177  			continue
   178  		}
   179  		for _, log := range receipt.Logs() {
   180  			if log.Address != s.config.ContractAddress {
   181  				continue
   182  			}
   183  			if err := handler.HandleEvent(ctx, blk, log); err != nil {
   184  				return err
   185  			}
   186  		}
   187  	}
   188  
   189  	// commit the result
   190  	return s.commit(handler, blk.Height())
   191  }
   192  
   193  // DeleteTipBlock deletes the tip block from indexer
   194  func (s *Indexer) DeleteTipBlock(context.Context, *block.Block) error {
   195  	return errors.New("not implemented")
   196  }
   197  
   198  func (s *Indexer) commit(handler *contractStakingEventHandler, height uint64) error {
   199  	batch, delta := handler.Result()
   200  	// update cache
   201  	if err := s.cache.Merge(delta, height); err != nil {
   202  		s.reloadCache()
   203  		return err
   204  	}
   205  	// update db
   206  	batch.Put(_StakingNS, _stakingHeightKey, byteutil.Uint64ToBytesBigEndian(height), "failed to put height")
   207  	if err := s.kvstore.WriteBatch(batch); err != nil {
   208  		s.reloadCache()
   209  		return err
   210  	}
   211  	return nil
   212  }
   213  
   214  func (s *Indexer) reloadCache() error {
   215  	s.cache = newContractStakingCache(s.config)
   216  	return s.loadFromDB()
   217  }
   218  
   219  func (s *Indexer) loadFromDB() error {
   220  	return s.cache.LoadFromDB(s.kvstore)
   221  }
   222  
   223  // isIgnored returns true if before cotractDeployHeight.
   224  // it aims to be compatible with blocks between feature hard-fork and contract deployed
   225  // read interface should return empty result instead of invalid height error if it returns true
   226  func (s *Indexer) isIgnored(height uint64) bool {
   227  	return height < s.config.ContractDeployHeight
   228  }