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 }