github.com/iotexproject/iotex-core@v1.14.1-rc1/action/protocol/staking/candidate_buckets_indexer.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 staking
     7  
     8  import (
     9  	"context"
    10  
    11  	"github.com/pkg/errors"
    12  	"google.golang.org/protobuf/proto"
    13  
    14  	"github.com/iotexproject/go-pkgs/hash"
    15  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    16  
    17  	"github.com/iotexproject/iotex-core/db"
    18  	"github.com/iotexproject/iotex-core/db/batch"
    19  	"github.com/iotexproject/iotex-core/pkg/util/byteutil"
    20  )
    21  
    22  const (
    23  	// StakingCandidatesNamespace is a namespace to store candidates with epoch start height
    24  	StakingCandidatesNamespace = "stakingCandidates"
    25  	// StakingBucketsNamespace is a namespace to store vote buckets with epoch start height
    26  	StakingBucketsNamespace = "stakingBuckets"
    27  	// StakingMetaNamespace is a namespace to store metadata
    28  	StakingMetaNamespace = "stakingMeta"
    29  )
    30  
    31  var (
    32  	_candHeightKey        = []byte("cht")
    33  	_bucketHeightKey      = []byte("bht")
    34  	_latestCandidatesHash = []byte("lch")
    35  	_latestBucketsHash    = []byte("lbh")
    36  )
    37  
    38  // CandidatesBucketsIndexer is an indexer to store candidates by given height
    39  type CandidatesBucketsIndexer struct {
    40  	latestCandidatesHeight uint64
    41  	latestBucketsHeight    uint64
    42  	latestCandidatesHash   hash.Hash160
    43  	latestBucketsHash      hash.Hash160
    44  	kvStore                db.KVStoreForRangeIndex
    45  }
    46  
    47  // NewStakingCandidatesBucketsIndexer creates a new StakingCandidatesIndexer
    48  func NewStakingCandidatesBucketsIndexer(kv db.KVStoreForRangeIndex) (*CandidatesBucketsIndexer, error) {
    49  	if kv == nil {
    50  		return nil, ErrMissingField
    51  	}
    52  	return &CandidatesBucketsIndexer{
    53  		kvStore: kv,
    54  	}, nil
    55  }
    56  
    57  // Start starts the indexer
    58  func (cbi *CandidatesBucketsIndexer) Start(ctx context.Context) error {
    59  	if err := cbi.kvStore.Start(ctx); err != nil {
    60  		return err
    61  	}
    62  	ret, err := cbi.kvStore.Get(StakingMetaNamespace, _candHeightKey)
    63  	switch errors.Cause(err) {
    64  	case nil:
    65  		cbi.latestCandidatesHeight = byteutil.BytesToUint64BigEndian(ret)
    66  	case db.ErrNotExist:
    67  		cbi.latestCandidatesHeight = 0
    68  	default:
    69  		return err
    70  	}
    71  
    72  	ret, err = cbi.kvStore.Get(StakingMetaNamespace, _latestCandidatesHash)
    73  	switch errors.Cause(err) {
    74  	case nil:
    75  		cbi.latestCandidatesHash = hash.BytesToHash160(ret)
    76  	case db.ErrNotExist:
    77  		cbi.latestCandidatesHash = hash.ZeroHash160
    78  	default:
    79  		return err
    80  	}
    81  
    82  	ret, err = cbi.kvStore.Get(StakingMetaNamespace, _bucketHeightKey)
    83  	switch errors.Cause(err) {
    84  	case nil:
    85  		cbi.latestBucketsHeight = byteutil.BytesToUint64BigEndian(ret)
    86  	case db.ErrNotExist:
    87  		cbi.latestBucketsHeight = 0
    88  	default:
    89  		return err
    90  	}
    91  
    92  	ret, err = cbi.kvStore.Get(StakingMetaNamespace, _latestBucketsHash)
    93  	switch errors.Cause(err) {
    94  	case nil:
    95  		cbi.latestBucketsHash = hash.BytesToHash160(ret)
    96  	case db.ErrNotExist:
    97  		cbi.latestBucketsHash = hash.ZeroHash160
    98  	default:
    99  		return err
   100  	}
   101  	return nil
   102  }
   103  
   104  // Stop stops the indexer
   105  func (cbi *CandidatesBucketsIndexer) Stop(ctx context.Context) error {
   106  	return cbi.kvStore.Stop(ctx)
   107  }
   108  
   109  // PutCandidates puts candidates into indexer
   110  func (cbi *CandidatesBucketsIndexer) PutCandidates(height uint64, candidates *iotextypes.CandidateListV2) error {
   111  	candidatesBytes, err := proto.Marshal(candidates)
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	if err := cbi.putToIndexer(StakingCandidatesNamespace, height, candidatesBytes); err != nil {
   117  		return err
   118  	}
   119  	cbi.latestCandidatesHeight = height
   120  	return nil
   121  }
   122  
   123  // GetCandidates gets candidates from indexer given epoch start height
   124  func (cbi *CandidatesBucketsIndexer) GetCandidates(height uint64, offset, limit uint32) (*iotextypes.CandidateListV2, uint64, error) {
   125  	if height > cbi.latestCandidatesHeight {
   126  		height = cbi.latestCandidatesHeight
   127  	}
   128  	candidateList := &iotextypes.CandidateListV2{}
   129  	ret, err := getFromIndexer(cbi.kvStore, StakingCandidatesNamespace, height)
   130  	cause := errors.Cause(err)
   131  	if cause == db.ErrNotExist || cause == db.ErrBucketNotExist {
   132  		return candidateList, height, nil
   133  	}
   134  	if err != nil {
   135  		return nil, height, err
   136  	}
   137  	if err := proto.Unmarshal(ret, candidateList); err != nil {
   138  		return nil, height, err
   139  	}
   140  	length := uint32(len(candidateList.Candidates))
   141  	if offset >= length {
   142  		return &iotextypes.CandidateListV2{}, height, nil
   143  	}
   144  	end := offset + limit
   145  	if end > uint32(len(candidateList.Candidates)) {
   146  		end = uint32(len(candidateList.Candidates))
   147  	}
   148  	candidateList.Candidates = candidateList.Candidates[offset:end]
   149  	return candidateList, height, nil
   150  }
   151  
   152  // PutBuckets puts vote buckets into indexer
   153  func (cbi *CandidatesBucketsIndexer) PutBuckets(height uint64, buckets *iotextypes.VoteBucketList) error {
   154  	bucketsBytes, err := proto.Marshal(buckets)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	if err := cbi.putToIndexer(StakingBucketsNamespace, height, bucketsBytes); err != nil {
   160  		return err
   161  	}
   162  	cbi.latestBucketsHeight = height
   163  	return nil
   164  }
   165  
   166  // GetBuckets gets vote buckets from indexer given epoch start height
   167  func (cbi *CandidatesBucketsIndexer) GetBuckets(height uint64, offset, limit uint32) (*iotextypes.VoteBucketList, uint64, error) {
   168  	if height > cbi.latestBucketsHeight {
   169  		height = cbi.latestBucketsHeight
   170  	}
   171  	buckets := &iotextypes.VoteBucketList{}
   172  	ret, err := getFromIndexer(cbi.kvStore, StakingBucketsNamespace, height)
   173  	cause := errors.Cause(err)
   174  	if cause == db.ErrNotExist || cause == db.ErrBucketNotExist {
   175  		return buckets, height, nil
   176  	}
   177  	if err != nil {
   178  		return nil, height, err
   179  	}
   180  	if err := proto.Unmarshal(ret, buckets); err != nil {
   181  		return nil, height, err
   182  	}
   183  	length := uint32(len(buckets.Buckets))
   184  	if offset >= length {
   185  		return &iotextypes.VoteBucketList{}, height, nil
   186  	}
   187  	end := offset + limit
   188  	if end > uint32(len(buckets.Buckets)) {
   189  		end = uint32(len(buckets.Buckets))
   190  	}
   191  	buckets.Buckets = buckets.Buckets[offset:end]
   192  	return buckets, height, nil
   193  }
   194  
   195  func (cbi *CandidatesBucketsIndexer) putToIndexer(ns string, height uint64, data []byte) error {
   196  	var (
   197  		h          = hash.Hash160b(data)
   198  		dataExist  bool
   199  		heightKey  []byte
   200  		latestHash []byte
   201  	)
   202  	switch ns {
   203  	case StakingCandidatesNamespace:
   204  		dataExist = (h == cbi.latestCandidatesHash)
   205  		heightKey = _candHeightKey
   206  		latestHash = _latestCandidatesHash
   207  	case StakingBucketsNamespace:
   208  		dataExist = (h == cbi.latestBucketsHash)
   209  		heightKey = _bucketHeightKey
   210  		latestHash = _latestBucketsHash
   211  	default:
   212  		return ErrTypeAssertion
   213  	}
   214  
   215  	heightBytes := byteutil.Uint64ToBytesBigEndian(height)
   216  	if dataExist {
   217  		// same bytes already exist, do nothing
   218  		return cbi.kvStore.Put(StakingMetaNamespace, heightKey, heightBytes)
   219  	}
   220  
   221  	// update latest height
   222  	b := batch.NewBatch()
   223  	b.Put(ns, heightBytes, data, "failed to write data bytes")
   224  	b.Put(StakingMetaNamespace, heightKey, heightBytes, "failed to update indexer height")
   225  	b.Put(StakingMetaNamespace, latestHash, h[:], "failed to update latest hash")
   226  	if err := cbi.kvStore.WriteBatch(b); err != nil {
   227  		return err
   228  	}
   229  	// update latest hash
   230  	if ns == StakingCandidatesNamespace {
   231  		cbi.latestCandidatesHash = h
   232  	} else {
   233  		cbi.latestBucketsHash = h
   234  	}
   235  	return nil
   236  }
   237  
   238  func getFromIndexer(kv db.KVStoreForRangeIndex, ns string, height uint64) ([]byte, error) {
   239  	b, err := kv.Get(ns, byteutil.Uint64ToBytesBigEndian(height))
   240  	switch errors.Cause(err) {
   241  	case nil:
   242  		return b, nil
   243  	case db.ErrNotExist:
   244  		// height does not exist, fallback to previous height
   245  		return kv.SeekPrev([]byte(ns), height)
   246  	default:
   247  		return nil, err
   248  	}
   249  }