diff --git a/action/protocol/staking/contractstaking/bucket.go b/action/protocol/staking/contractstaking/bucket.go index ba7866d0da..f83edb4d4f 100644 --- a/action/protocol/staking/contractstaking/bucket.go +++ b/action/protocol/staking/contractstaking/bucket.go @@ -24,3 +24,22 @@ type Bucket struct { AutoStake bool ContractAddress string // contract address for the bucket } + +func assembleBucket(token uint64, bi *bucketInfo, bt *BucketType, contractAddr string) (*Bucket, error) { + vb := Bucket{ + Index: token, + StakedAmount: bt.Amount, + StakedDurationBlockNumber: bt.Duration, + CreateBlockHeight: bi.CreatedAt, + StakeStartBlockHeight: bi.CreatedAt, + UnstakeStartBlockHeight: bi.UnstakedAt, + AutoStake: bi.UnlockedAt == maxBlockNumber, + Candidate: bi.Delegate, + Owner: bi.Owner, + ContractAddress: contractAddr, + } + if bi.UnlockedAt != maxBlockNumber { + vb.StakeStartBlockHeight = bi.UnlockedAt + } + return &vb, nil +} diff --git a/action/protocol/staking/contractstaking/cache.go b/action/protocol/staking/contractstaking/cache.go new file mode 100644 index 0000000000..43ed459b56 --- /dev/null +++ b/action/protocol/staking/contractstaking/cache.go @@ -0,0 +1,188 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package contractstaking + +import ( + "math/big" + + "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" +) + +type ( + contractStakingCache struct { + bucketInfoMap map[uint64]*bucketInfo // map[token]bucketInfo + candidateBucketMap map[string]map[uint64]bool // map[candidate]bucket + bucketTypeMap map[uint64]*BucketType // map[bucketTypeId]BucketType + propertyBucketTypeMap map[int64]map[uint64]uint64 // map[amount][duration]index + height uint64 + totalBucketCount uint64 // total number of buckets including burned buckets + contractAddress string // contract address for the bucket + } +) + +var ( + // ErrBucketNotExist is the error when bucket does not exist + ErrBucketNotExist = errors.New("bucket does not exist") +) + +func newContractStakingCache(contractAddr string) *contractStakingCache { + cache := &contractStakingCache{ + bucketInfoMap: make(map[uint64]*bucketInfo), + bucketTypeMap: make(map[uint64]*BucketType), + propertyBucketTypeMap: make(map[int64]map[uint64]uint64), + candidateBucketMap: make(map[string]map[uint64]bool), + contractAddress: contractAddr, + } + return cache +} + +func (s *contractStakingCache) Height() uint64 { + return s.height +} + +func (s *contractStakingCache) CandidateVotes(candidate address.Address) *big.Int { + votes := big.NewInt(0) + m, ok := s.candidateBucketMap[candidate.String()] + if !ok { + return votes + } + for id, existed := range m { + if !existed { + continue + } + bi := s.mustGetBucketInfo(id) + // only count the bucket that is not unstaked + if bi.UnstakedAt != maxBlockNumber { + continue + } + bt := s.mustGetBucketType(bi.TypeIndex) + votes.Add(votes, bt.Amount) + } + return votes +} + +func (s *contractStakingCache) Buckets() ([]*Bucket, error) { + vbs := []*Bucket{} + for id, bi := range s.getAllBucketInfo() { + bt := s.mustGetBucketType(bi.TypeIndex) + vb, err := assembleBucket(id, bi, bt, s.contractAddress) + if err != nil { + return nil, err + } + vbs = append(vbs, vb) + } + return vbs, nil +} + +func (s *contractStakingCache) Bucket(id uint64) (*Bucket, error) { + return s.getBucket(id) +} + +func (s *contractStakingCache) BucketsByCandidate(candidate address.Address) ([]*Bucket, error) { + bucketMap := s.getBucketInfoByCandidate(candidate) + vbs := make([]*Bucket, 0, len(bucketMap)) + for id := range bucketMap { + vb, err := s.getBucket(id) + if err != nil { + return nil, err + } + vbs = append(vbs, vb) + } + return vbs, nil +} + +func (s *contractStakingCache) BucketsByIndices(indices []uint64) ([]*Bucket, error) { + vbs := make([]*Bucket, 0, len(indices)) + for _, id := range indices { + vb, err := s.getBucket(id) + if err != nil { + return nil, err + } + vbs = append(vbs, vb) + } + return vbs, nil +} + +func (s *contractStakingCache) TotalBucketCount() uint64 { + return s.totalBucketCount +} + +func (s *contractStakingCache) ActiveBucketTypes() map[uint64]*BucketType { + m := make(map[uint64]*BucketType) + for k, v := range s.bucketTypeMap { + if v.ActivatedAt != maxBlockNumber { + m[k] = v + } + } + return m +} + +func (s *contractStakingCache) getBucketTypeIndex(amount *big.Int, duration uint64) (uint64, bool) { + m, ok := s.propertyBucketTypeMap[amount.Int64()] + if !ok { + return 0, false + } + id, ok := m[duration] + return id, ok +} + +func (s *contractStakingCache) getBucketType(id uint64) (*BucketType, bool) { + bt, ok := s.bucketTypeMap[id] + return bt, ok +} + +func (s *contractStakingCache) mustGetBucketType(id uint64) *BucketType { + bt, ok := s.bucketTypeMap[id] + if !ok { + panic("bucket type not found") + } + return bt +} + +func (s *contractStakingCache) getBucketInfo(id uint64) (*bucketInfo, bool) { + bi, ok := s.bucketInfoMap[id] + return bi, ok +} + +func (s *contractStakingCache) mustGetBucketInfo(id uint64) *bucketInfo { + bt, ok := s.bucketInfoMap[id] + if !ok { + panic("bucket info not found") + } + return bt +} + +func (s *contractStakingCache) getBucket(id uint64) (*Bucket, error) { + bi, ok := s.getBucketInfo(id) + if !ok { + return nil, errors.Wrapf(ErrBucketNotExist, "id %d", id) + } + bt := s.mustGetBucketType(bi.TypeIndex) + return assembleBucket(id, bi, bt, s.contractAddress) +} + +func (s *contractStakingCache) getTotalBucketTypeCount() uint64 { + return uint64(len(s.bucketTypeMap)) +} + +func (s *contractStakingCache) getAllBucketInfo() map[uint64]*bucketInfo { + m := make(map[uint64]*bucketInfo) + for k, v := range s.bucketInfoMap { + m[k] = v + } + return m +} + +func (s *contractStakingCache) getBucketInfoByCandidate(candidate address.Address) map[uint64]*bucketInfo { + m := make(map[uint64]*bucketInfo) + for k, v := range s.candidateBucketMap[candidate.String()] { + if v { + m[k] = s.bucketInfoMap[k] + } + } + return m +} diff --git a/action/protocol/staking/contractstaking/indexer.go b/action/protocol/staking/contractstaking/indexer.go new file mode 100644 index 0000000000..876f99b178 --- /dev/null +++ b/action/protocol/staking/contractstaking/indexer.go @@ -0,0 +1,84 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package contractstaking + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common/math" + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/db" +) + +const ( + maxBlockNumber uint64 = math.MaxUint64 +) + +type ( + // Indexer is the contract staking indexer + // Main functions: + // 1. handle contract staking contract events when new block comes to generate index data + // 2. provide query interface for contract staking index data + Indexer struct { + kvstore db.KVStore // persistent storage, used to initialize index cache at startup + cache *contractStakingCache // in-memory index for clean data, used to query index data + contractAddress string // stake contract address + } +) + +// NewContractStakingIndexer creates a new contract staking indexer +func NewContractStakingIndexer(kvStore db.KVStore, contractAddr string) *Indexer { + return &Indexer{ + kvstore: kvStore, + cache: newContractStakingCache(contractAddr), + } +} + +// Height returns the tip block height +func (s *Indexer) Height() (uint64, error) { + return s.cache.Height(), nil +} + +// CandidateVotes returns the candidate votes +func (s *Indexer) CandidateVotes(candidate address.Address) *big.Int { + return s.cache.CandidateVotes(candidate) +} + +// Buckets returns the buckets +func (s *Indexer) Buckets() ([]*Bucket, error) { + return s.cache.Buckets() +} + +// Bucket returns the bucket +func (s *Indexer) Bucket(id uint64) (*Bucket, error) { + return s.cache.Bucket(id) +} + +// BucketsByIndices returns the buckets by indices +func (s *Indexer) BucketsByIndices(indices []uint64) ([]*Bucket, error) { + return s.cache.BucketsByIndices(indices) +} + +// BucketsByCandidate returns the buckets by candidate +func (s *Indexer) BucketsByCandidate(candidate address.Address) ([]*Bucket, error) { + return s.cache.BucketsByCandidate(candidate) +} + +// TotalBucketCount returns the total bucket count including active and burnt buckets +func (s *Indexer) TotalBucketCount() uint64 { + return s.cache.TotalBucketCount() +} + +// BucketTypes returns the active bucket types +func (s *Indexer) BucketTypes() ([]*BucketType, error) { + btMap := s.cache.ActiveBucketTypes() + bts := make([]*BucketType, 0, len(btMap)) + for _, bt := range btMap { + bts = append(bts, bt) + } + return bts, nil +}