diff --git a/action/protocol/staking/contractstake_bucket_type.go b/action/protocol/staking/contractstake_bucket_type.go index 06bec09be4..70a886e124 100644 --- a/action/protocol/staking/contractstake_bucket_type.go +++ b/action/protocol/staking/contractstake_bucket_type.go @@ -38,6 +38,15 @@ func (bt *ContractStakingBucketType) Deserialize(b []byte) error { return bt.loadProto(&m) } +// Clone clones the bucket type +func (bt *ContractStakingBucketType) Clone() *ContractStakingBucketType { + return &ContractStakingBucketType{ + Amount: big.NewInt(0).Set(bt.Amount), + Duration: bt.Duration, + ActivatedAt: bt.ActivatedAt, + } +} + func (bt *ContractStakingBucketType) toProto() *stakingpb.BucketType { return &stakingpb.BucketType{ Amount: bt.Amount.String(), diff --git a/blockindex/contractstaking/bucket_info.go b/blockindex/contractstaking/bucket_info.go index bb3dc138cb..ee22717bfa 100644 --- a/blockindex/contractstaking/bucket_info.go +++ b/blockindex/contractstaking/bucket_info.go @@ -39,6 +39,26 @@ func (bi *bucketInfo) Deserialize(b []byte) error { return bi.loadProto(&m) } +// clone clones the bucket info +func (bi *bucketInfo) clone() *bucketInfo { + delegate := bi.Delegate + if delegate != nil { + delegate, _ = address.FromBytes(delegate.Bytes()) + } + owner := bi.Owner + if owner != nil { + owner, _ = address.FromBytes(owner.Bytes()) + } + return &bucketInfo{ + TypeIndex: bi.TypeIndex, + CreatedAt: bi.CreatedAt, + UnlockedAt: bi.UnlockedAt, + UnstakedAt: bi.UnstakedAt, + Delegate: delegate, + Owner: owner, + } +} + func (bi *bucketInfo) toProto() *contractstakingpb.BucketInfo { pb := &contractstakingpb.BucketInfo{ TypeIndex: bi.TypeIndex, diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index 6c328a432e..c32ff685ef 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -22,10 +22,9 @@ type ( 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 - mutex sync.RWMutex // a RW mutex for the cache to protect concurrent access + totalBucketCount uint64 // total number of buckets including burned buckets + contractAddress string // contract address for the bucket + mutex sync.RWMutex // a RW mutex for the cache to protect concurrent access } ) @@ -44,12 +43,6 @@ func newContractStakingCache(contractAddr string) *contractStakingCache { } } -func (s *contractStakingCache) Height() uint64 { - s.mutex.RLock() - defer s.mutex.RUnlock() - return s.height -} - func (s *contractStakingCache) CandidateVotes(candidate address.Address) *big.Int { s.mutex.RLock() defer s.mutex.RUnlock() @@ -79,9 +72,9 @@ func (s *contractStakingCache) Buckets() []*Bucket { defer s.mutex.RUnlock() vbs := []*Bucket{} - for id, bi := range s.getAllBucketInfo() { + for id, bi := range s.bucketInfoMap { bt := s.mustGetBucketType(bi.TypeIndex) - vb := assembleBucket(id, bi, bt, s.contractAddress) + vb := assembleBucket(id, bi.clone(), bt, s.contractAddress) vbs = append(vbs, vb) } return vbs @@ -126,7 +119,7 @@ func (s *contractStakingCache) BucketsByCandidate(candidate address.Address) []* s.mutex.RLock() defer s.mutex.RUnlock() - bucketMap := s.getBucketInfoByCandidate(candidate) + bucketMap := s.candidateBucketMap[candidate.String()] vbs := make([]*Bucket, 0, len(bucketMap)) for id := range bucketMap { vb := s.mustGetBucket(id) @@ -153,7 +146,7 @@ func (s *contractStakingCache) TotalBucketCount() uint64 { s.mutex.RLock() defer s.mutex.RUnlock() - return s.getTotalBucketCount() + return s.totalBucketCount } func (s *contractStakingCache) ActiveBucketTypes() map[uint64]*BucketType { @@ -163,7 +156,7 @@ 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 + m[k] = v.Clone() } } return m @@ -190,18 +183,15 @@ func (s *contractStakingCache) DeleteBucketInfo(id uint64) { s.deleteBucketInfo(id) } -func (s *contractStakingCache) PutHeight(h uint64) { - s.mutex.Lock() - defer s.mutex.Unlock() - - s.putHeight(h) -} - func (s *contractStakingCache) Merge(delta *contractStakingDelta) error { s.mutex.Lock() defer s.mutex.Unlock() - return s.merge(delta) + if err := s.merge(delta); err != nil { + return err + } + s.putTotalBucketCount(s.totalBucketCount + delta.AddedBucketCnt()) + return nil } func (s *contractStakingCache) PutTotalBucketCount(count uint64) { @@ -233,21 +223,6 @@ func (s *contractStakingCache) LoadFromDB(kvstore db.KVStore) error { s.mutex.Lock() defer s.mutex.Unlock() - delta := newContractStakingDelta() - // load height - var height uint64 - h, err := kvstore.Get(_StakingNS, _stakingHeightKey) - if err != nil { - if !errors.Is(err, db.ErrNotExist) { - return err - } - height = 0 - } else { - height = byteutil.BytesToUint64BigEndian(h) - - } - delta.PutHeight(height) - // load total bucket count var totalBucketCount uint64 tbc, err := kvstore.Get(_StakingNS, _stakingTotalBucketCountKey) @@ -258,7 +233,7 @@ func (s *contractStakingCache) LoadFromDB(kvstore db.KVStore) error { } else { totalBucketCount = byteutil.BytesToUint64BigEndian(tbc) } - delta.PutTotalBucketCount(totalBucketCount) + s.putTotalBucketCount(totalBucketCount) // load bucket info ks, vs, err := kvstore.Filter(_StakingBucketInfoNS, func(k, v []byte) bool { return true }, nil, nil) @@ -270,7 +245,7 @@ func (s *contractStakingCache) LoadFromDB(kvstore db.KVStore) error { if err := b.Deserialize(vs[i]); err != nil { return err } - delta.AddBucketInfo(byteutil.BytesToUint64BigEndian(ks[i]), &b) + s.putBucketInfo(byteutil.BytesToUint64BigEndian(ks[i]), &b) } // load bucket type @@ -283,9 +258,9 @@ func (s *contractStakingCache) LoadFromDB(kvstore db.KVStore) error { if err := b.Deserialize(vs[i]); err != nil { return err } - delta.AddBucketType(byteutil.BytesToUint64BigEndian(ks[i]), &b) + s.putBucketType(byteutil.BytesToUint64BigEndian(ks[i]), &b) } - return s.merge(delta) + return nil } func (s *contractStakingCache) getBucketTypeIndex(amount *big.Int, duration uint64) (uint64, bool) { @@ -299,7 +274,10 @@ func (s *contractStakingCache) getBucketTypeIndex(amount *big.Int, duration uint func (s *contractStakingCache) getBucketType(id uint64) (*BucketType, bool) { bt, ok := s.bucketTypeMap[id] - return bt, ok + if !ok { + return nil, false + } + return bt.Clone(), ok } func (s *contractStakingCache) mustGetBucketType(id uint64) *BucketType { @@ -312,7 +290,10 @@ func (s *contractStakingCache) mustGetBucketType(id uint64) *BucketType { func (s *contractStakingCache) getBucketInfo(id uint64) (*bucketInfo, bool) { bi, ok := s.bucketInfoMap[id] - return bi, ok + if !ok { + return nil, false + } + return bi.clone(), ok } func (s *contractStakingCache) mustGetBucketInfo(id uint64) *bucketInfo { @@ -338,28 +319,6 @@ func (s *contractStakingCache) getBucket(id uint64) (*Bucket, bool) { return assembleBucket(id, bi, bt, s.contractAddress), true } -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 -} - -func (s *contractStakingCache) getTotalBucketCount() uint64 { - return s.totalBucketCount -} - func (s *contractStakingCache) putBucketType(id uint64, bt *BucketType) { amount := bt.Amount.Int64() s.bucketTypeMap[id] = bt @@ -372,11 +331,21 @@ func (s *contractStakingCache) putBucketType(id uint64, bt *BucketType) { } func (s *contractStakingCache) putBucketInfo(id uint64, bi *bucketInfo) { + oldBi := s.bucketInfoMap[id] s.bucketInfoMap[id] = bi - if _, ok := s.candidateBucketMap[bi.Delegate.String()]; !ok { - s.candidateBucketMap[bi.Delegate.String()] = make(map[uint64]bool) + // update candidate bucket map + newDelegate := bi.Delegate.String() + if _, ok := s.candidateBucketMap[newDelegate]; !ok { + s.candidateBucketMap[newDelegate] = make(map[uint64]bool) + } + s.candidateBucketMap[newDelegate][id] = true + // delete old candidate bucket map + if oldBi != nil { + oldDelegate := oldBi.Delegate.String() + if oldDelegate != newDelegate { + delete(s.candidateBucketMap[oldDelegate], id) + } } - s.candidateBucketMap[bi.Delegate.String()][id] = true } func (s *contractStakingCache) deleteBucketInfo(id uint64) { @@ -391,10 +360,6 @@ func (s *contractStakingCache) deleteBucketInfo(id uint64) { delete(s.candidateBucketMap[bi.Delegate.String()], id) } -func (s *contractStakingCache) putHeight(h uint64) { - s.height = h -} - func (s *contractStakingCache) putTotalBucketCount(count uint64) { s.totalBucketCount = count } @@ -418,7 +383,5 @@ func (s *contractStakingCache) merge(delta *contractStakingDelta) error { } } } - s.putHeight(delta.GetHeight()) - s.putTotalBucketCount(s.getTotalBucketCount() + delta.AddedBucketCnt()) return nil } diff --git a/blockindex/contractstaking/cache_test.go b/blockindex/contractstaking/cache_test.go new file mode 100644 index 0000000000..f8f7b3c1ea --- /dev/null +++ b/blockindex/contractstaking/cache_test.go @@ -0,0 +1,482 @@ +package contractstaking + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/action/protocol/staking" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" + "github.com/iotexproject/iotex-core/test/identityset" + "github.com/iotexproject/iotex-core/testutil" +) + +func TestContractStakingCache_CandidateVotes(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + // no bucket + require.EqualValues(0, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // one bucket + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + require.EqualValues(100, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // two buckets + cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + require.EqualValues(200, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // add one bucket with different delegate + cache.PutBucketInfo(3, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)}) + require.EqualValues(200, cache.CandidateVotes(identityset.Address(1)).Int64()) + require.EqualValues(100, cache.CandidateVotes(identityset.Address(3)).Int64()) + + // add one bucket with different owner + cache.PutBucketInfo(4, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(4)}) + require.EqualValues(300, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // add one bucket with different amount + cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1}) + cache.PutBucketInfo(5, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + require.EqualValues(500, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // add one bucket with different duration + cache.PutBucketType(3, &BucketType{Amount: big.NewInt(300), Duration: 200, ActivatedAt: 1}) + cache.PutBucketInfo(6, &bucketInfo{TypeIndex: 3, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + require.EqualValues(800, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // add one bucket that is unstaked + cache.PutBucketInfo(7, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: 1, UnstakedAt: 1, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + require.EqualValues(800, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // add one bucket that is unlocked and staked + cache.PutBucketInfo(8, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: 100, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + require.EqualValues(900, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // change delegate of bucket 1 + cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)}) + require.EqualValues(800, cache.CandidateVotes(identityset.Address(1)).Int64()) + require.EqualValues(200, cache.CandidateVotes(identityset.Address(3)).Int64()) + + // change owner of bucket 1 + cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)}) + require.EqualValues(800, cache.CandidateVotes(identityset.Address(1)).Int64()) + require.EqualValues(200, cache.CandidateVotes(identityset.Address(3)).Int64()) + + // change amount of bucket 1 + cache.putBucketInfo(1, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)}) + require.EqualValues(800, cache.CandidateVotes(identityset.Address(1)).Int64()) + require.EqualValues(300, cache.CandidateVotes(identityset.Address(3)).Int64()) +} + +func TestContractStakingCache_Buckets(t *testing.T) { + require := require.New(t) + contractAddr := identityset.Address(27).String() + cache := newContractStakingCache(contractAddr) + + // no bucket + require.Empty(cache.Buckets()) + + // add one bucket + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + buckets := cache.Buckets() + require.Len(buckets, 1) + checkVoteBucket(require, buckets[0], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + bucket, ok := cache.Bucket(1) + require.True(ok) + checkVoteBucket(require, bucket, 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // add one bucket with different index + cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 1}) + cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)}) + bucketMaps := bucketsToMap(cache.Buckets()) + require.Len(bucketMaps, 2) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(3).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr) + bucket, ok = cache.Bucket(1) + require.True(ok) + checkVoteBucket(require, bucket, 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + bucket, ok = cache.Bucket(2) + require.True(ok) + checkVoteBucket(require, bucket, 2, identityset.Address(3).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr) + + // update delegate of bucket 2 + cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(5), Owner: identityset.Address(4)}) + bucketMaps = bucketsToMap(cache.Buckets()) + require.Len(bucketMaps, 2) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr) + bucket, ok = cache.Bucket(1) + require.True(ok) + checkVoteBucket(require, bucket, 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + bucket, ok = cache.Bucket(2) + require.True(ok) + checkVoteBucket(require, bucket, 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr) + + // delete bucket 1 + cache.DeleteBucketInfo(1) + bucketMaps = bucketsToMap(cache.Buckets()) + require.Len(bucketMaps, 1) + checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr) + _, ok = cache.Bucket(1) + require.False(ok) + bucket, ok = cache.Bucket(2) + require.True(ok) + checkVoteBucket(require, bucket, 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr) +} + +func TestContractStakingCache_BucketsByCandidate(t *testing.T) { + require := require.New(t) + contractAddr := identityset.Address(27).String() + cache := newContractStakingCache(contractAddr) + + // no bucket + buckets := cache.BucketsByCandidate(identityset.Address(1)) + require.Len(buckets, 0) + + // one bucket + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + bucketMaps := bucketsToMap(cache.BucketsByCandidate(identityset.Address(1))) + require.Len(bucketMaps, 1) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // two buckets + cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + bucketMaps = bucketsToMap(cache.BucketsByCandidate(identityset.Address(1))) + require.Len(bucketMaps, 2) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // add one bucket with different delegate + cache.PutBucketInfo(3, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)}) + bucketMaps = bucketsToMap(cache.BucketsByCandidate(identityset.Address(1))) + require.Len(bucketMaps, 2) + require.Nil(bucketMaps[3]) + bucketMaps = bucketsToMap(cache.BucketsByCandidate(identityset.Address(3))) + require.Len(bucketMaps, 1) + checkVoteBucket(require, bucketMaps[3], 3, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // change delegate of bucket 1 + cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)}) + bucketMaps = bucketsToMap(cache.BucketsByCandidate(identityset.Address(1))) + require.Len(bucketMaps, 1) + require.Nil(bucketMaps[1]) + checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + bucketMaps = bucketsToMap(cache.BucketsByCandidate(identityset.Address(3))) + require.Len(bucketMaps, 2) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + checkVoteBucket(require, bucketMaps[3], 3, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // delete bucket 2 + cache.DeleteBucketInfo(2) + bucketMaps = bucketsToMap(cache.BucketsByCandidate(identityset.Address(1))) + require.Len(bucketMaps, 0) + bucketMaps = bucketsToMap(cache.BucketsByCandidate(identityset.Address(3))) + require.Len(bucketMaps, 2) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + checkVoteBucket(require, bucketMaps[3], 3, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + +} + +func TestContractStakingCache_BucketsByIndices(t *testing.T) { + require := require.New(t) + contractAddr := identityset.Address(27).String() + cache := newContractStakingCache(contractAddr) + + // no bucket + buckets, err := cache.BucketsByIndices([]uint64{1}) + require.NoError(err) + require.Len(buckets, 0) + + // one bucket + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + buckets, err = cache.BucketsByIndices([]uint64{1}) + require.NoError(err) + require.Len(buckets, 1) + bucketMaps := bucketsToMap(buckets) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // two buckets + cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + buckets, err = cache.BucketsByIndices([]uint64{1, 2}) + require.NoError(err) + require.Len(buckets, 2) + bucketMaps = bucketsToMap(buckets) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // one bucket not found + buckets, err = cache.BucketsByIndices([]uint64{3}) + require.NoError(err) + require.Len(buckets, 0) + + // one bucket found, one not found + buckets, err = cache.BucketsByIndices([]uint64{1, 3}) + require.NoError(err) + require.Len(buckets, 1) + bucketMaps = bucketsToMap(buckets) + checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr) + + // delete bucket 1 + cache.DeleteBucketInfo(1) + buckets, err = cache.BucketsByIndices([]uint64{1}) + require.NoError(err) + require.Len(buckets, 0) +} + +func TestContractStakingCache_TotalBucketCount(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + // no bucket + require.EqualValues(0, cache.TotalBucketCount()) + + // one bucket + cache.PutTotalBucketCount(1) + require.EqualValues(1, cache.TotalBucketCount()) + + // two buckets + cache.PutTotalBucketCount(2) + require.EqualValues(2, cache.TotalBucketCount()) + + // delete bucket 1 + cache.DeleteBucketInfo(1) + require.EqualValues(2, cache.TotalBucketCount()) +} + +func TestContractStakingCache_ActiveBucketTypes(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + // no bucket type + require.Empty(cache.ActiveBucketTypes()) + + // one bucket type + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + activeBucketTypes := cache.ActiveBucketTypes() + require.Len(activeBucketTypes, 1) + require.EqualValues(100, activeBucketTypes[1].Amount.Int64()) + require.EqualValues(100, activeBucketTypes[1].Duration) + require.EqualValues(1, activeBucketTypes[1].ActivatedAt) + + // two bucket types + cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) + activeBucketTypes = cache.ActiveBucketTypes() + require.Len(activeBucketTypes, 2) + require.EqualValues(100, activeBucketTypes[1].Amount.Int64()) + require.EqualValues(100, activeBucketTypes[1].Duration) + require.EqualValues(1, activeBucketTypes[1].ActivatedAt) + require.EqualValues(200, activeBucketTypes[2].Amount.Int64()) + require.EqualValues(200, activeBucketTypes[2].Duration) + require.EqualValues(2, activeBucketTypes[2].ActivatedAt) + + // add one inactive bucket type + cache.PutBucketType(3, &BucketType{Amount: big.NewInt(300), Duration: 300, ActivatedAt: maxBlockNumber}) + activeBucketTypes = cache.ActiveBucketTypes() + require.Len(activeBucketTypes, 2) + require.EqualValues(100, activeBucketTypes[1].Amount.Int64()) + require.EqualValues(100, activeBucketTypes[1].Duration) + require.EqualValues(1, activeBucketTypes[1].ActivatedAt) + require.EqualValues(200, activeBucketTypes[2].Amount.Int64()) + require.EqualValues(200, activeBucketTypes[2].Duration) + require.EqualValues(2, activeBucketTypes[2].ActivatedAt) + + // deactivate bucket type 1 + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: maxBlockNumber}) + activeBucketTypes = cache.ActiveBucketTypes() + require.Len(activeBucketTypes, 1) + require.EqualValues(200, activeBucketTypes[2].Amount.Int64()) + require.EqualValues(200, activeBucketTypes[2].Duration) + require.EqualValues(2, activeBucketTypes[2].ActivatedAt) + + // reactivate bucket type 1 + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + activeBucketTypes = cache.ActiveBucketTypes() + require.Len(activeBucketTypes, 2) + require.EqualValues(100, activeBucketTypes[1].Amount.Int64()) + require.EqualValues(100, activeBucketTypes[1].Duration) + require.EqualValues(1, activeBucketTypes[1].ActivatedAt) + require.EqualValues(200, activeBucketTypes[2].Amount.Int64()) + require.EqualValues(200, activeBucketTypes[2].Duration) + require.EqualValues(2, activeBucketTypes[2].ActivatedAt) +} + +func TestContractStakingCache_Merge(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + // create delta with one bucket type + delta := newContractStakingDelta() + delta.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + // merge delta into cache + err := cache.Merge(delta) + require.NoError(err) + // check that bucket type was added to cache + activeBucketTypes := cache.ActiveBucketTypes() + require.Len(activeBucketTypes, 1) + require.EqualValues(100, activeBucketTypes[1].Amount.Int64()) + require.EqualValues(100, activeBucketTypes[1].Duration) + require.EqualValues(1, activeBucketTypes[1].ActivatedAt) + + // create delta with one bucket + delta = newContractStakingDelta() + delta.AddBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + // merge delta into cache + err = cache.Merge(delta) + require.NoError(err) + // check that bucket was added to cache and vote count is correct + require.EqualValues(100, cache.CandidateVotes(identityset.Address(1)).Int64()) + + // create delta with updated bucket delegate + delta = newContractStakingDelta() + delta.UpdateBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)}) + // merge delta into cache + err = cache.Merge(delta) + require.NoError(err) + // check that bucket delegate was updated and vote count is correct + require.EqualValues(0, cache.CandidateVotes(identityset.Address(1)).Int64()) + require.EqualValues(100, cache.CandidateVotes(identityset.Address(3)).Int64()) + + // create delta with deleted bucket + delta = newContractStakingDelta() + delta.DeleteBucketInfo(1) + // merge delta into cache + err = cache.Merge(delta) + require.NoError(err) + // check that bucket was deleted from cache and vote count is 0 + require.EqualValues(0, cache.CandidateVotes(identityset.Address(3)).Int64()) +} + +func TestContractStakingCache_MatchBucketType(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + // no bucket types + _, bucketType, ok := cache.MatchBucketType(big.NewInt(100), 100) + require.False(ok) + require.Nil(bucketType) + + // one bucket type + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + // match exact bucket type + id, bucketType, ok := cache.MatchBucketType(big.NewInt(100), 100) + require.True(ok) + require.EqualValues(1, id) + require.EqualValues(100, bucketType.Amount.Int64()) + require.EqualValues(100, bucketType.Duration) + require.EqualValues(1, bucketType.ActivatedAt) + + // match bucket type with different amount + _, bucketType, ok = cache.MatchBucketType(big.NewInt(200), 100) + require.False(ok) + require.Nil(bucketType) + + // match bucket type with different duration + _, bucketType, ok = cache.MatchBucketType(big.NewInt(100), 200) + require.False(ok) + require.Nil(bucketType) + + // no match + _, bucketType, ok = cache.MatchBucketType(big.NewInt(200), 200) + require.False(ok) + require.Nil(bucketType) +} + +func TestContractStakingCache_BucketTypeCount(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + // no bucket type + require.EqualValues(0, cache.BucketTypeCount()) + + // one bucket type + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + require.EqualValues(1, cache.BucketTypeCount()) + + // two bucket types + cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) + require.EqualValues(2, cache.BucketTypeCount()) + + // deactivate bucket type 1 + cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: maxBlockNumber}) + require.EqualValues(2, cache.BucketTypeCount()) +} + +func TestContractStakingCache_LoadFromDB(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + // load from empty db + path, err := testutil.PathOfTempFile("staking.db") + require.NoError(err) + defer testutil.CleanupPath(path) + cfg := config.Default.DB + cfg.DbPath = path + kvstore := db.NewBoltDB(cfg) + require.NoError(kvstore.Start(context.Background())) + defer kvstore.Stop(context.Background()) + + err = cache.LoadFromDB(kvstore) + require.NoError(err) + require.Equal(uint64(0), cache.TotalBucketCount()) + require.Equal(0, len(cache.Buckets())) + require.EqualValues(0, cache.BucketTypeCount()) + + // load from db with height and total bucket count + kvstore.Put(_StakingNS, _stakingHeightKey, byteutil.Uint64ToBytesBigEndian(12345)) + kvstore.Put(_StakingNS, _stakingTotalBucketCountKey, byteutil.Uint64ToBytesBigEndian(10)) + err = cache.LoadFromDB(kvstore) + require.NoError(err) + require.Equal(uint64(10), cache.TotalBucketCount()) + require.Equal(0, len(cache.Buckets())) + require.EqualValues(0, cache.BucketTypeCount()) + + // load from db with bucket + bucketInfo := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)} + kvstore.Put(_StakingBucketInfoNS, byteutil.Uint64ToBytesBigEndian(1), bucketInfo.Serialize()) + bucketType := &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1} + kvstore.Put(_StakingBucketTypeNS, byteutil.Uint64ToBytesBigEndian(1), bucketType.Serialize()) + err = cache.LoadFromDB(kvstore) + require.NoError(err) + require.Equal(uint64(10), cache.TotalBucketCount()) + bi, ok := cache.BucketInfo(1) + require.True(ok) + require.Equal(1, len(cache.Buckets())) + require.Equal(bucketInfo, bi) + require.EqualValues(1, cache.BucketTypeCount()) + id, bt, ok := cache.MatchBucketType(big.NewInt(100), 100) + require.True(ok) + require.EqualValues(1, id) + require.EqualValues(100, bt.Amount.Int64()) + require.EqualValues(100, bt.Duration) + require.EqualValues(1, bt.ActivatedAt) +} + +func bucketsToMap(buckets []*staking.VoteBucket) map[uint64]*staking.VoteBucket { + m := make(map[uint64]*staking.VoteBucket) + for _, bucket := range buckets { + m[bucket.Index] = bucket + } + return m +} + +func checkVoteBucket(r *require.Assertions, bucket *staking.VoteBucket, index uint64, candidate, owner string, amount, duration, createHeight, startHeight, unstakeHeight uint64, autoStake bool, contractAddr string) { + r.EqualValues(index, bucket.Index) + r.EqualValues(candidate, bucket.Candidate.String()) + r.EqualValues(owner, bucket.Owner.String()) + r.EqualValues(amount, bucket.StakedAmount.Int64()) + r.EqualValues(duration, bucket.StakedDurationBlockNumber) + r.EqualValues(createHeight, bucket.CreateBlockHeight) + r.EqualValues(startHeight, bucket.StakeStartBlockHeight) + r.EqualValues(unstakeHeight, bucket.UnstakeStartBlockHeight) + r.EqualValues(autoStake, bucket.AutoStake) + r.EqualValues(contractAddr, bucket.ContractAddress) +} diff --git a/blockindex/contractstaking/delta_cache.go b/blockindex/contractstaking/delta_cache.go index bd28d438d0..eb4a7aca68 100644 --- a/blockindex/contractstaking/delta_cache.go +++ b/blockindex/contractstaking/delta_cache.go @@ -24,18 +24,6 @@ func newContractStakingDelta() *contractStakingDelta { } } -func (s *contractStakingDelta) PutHeight(height uint64) { - s.cache.PutHeight(height) -} - -func (s *contractStakingDelta) GetHeight() uint64 { - return s.cache.Height() -} - -func (s *contractStakingDelta) PutTotalBucketCount(count uint64) { - s.cache.PutTotalBucketCount(count) -} - func (s *contractStakingDelta) BucketInfoDelta() map[deltaState]map[uint64]*bucketInfo { delta := map[deltaState]map[uint64]*bucketInfo{ deltaStateAdded: make(map[uint64]*bucketInfo), @@ -80,27 +68,23 @@ func (s *contractStakingDelta) MatchBucketType(amount *big.Int, duration uint64) } func (s *contractStakingDelta) GetBucketInfo(id uint64) (*bucketInfo, deltaState) { - if state, ok := s.bucketInfoDeltaState[id]; ok { - switch state { - case deltaStateAdded, deltaStateModified: - return s.cache.MustGetBucketInfo(id), state - default: - return nil, state - } + state := s.bucketInfoDeltaState[id] + switch state { + case deltaStateAdded, deltaStateModified: + return s.cache.MustGetBucketInfo(id), state + default: // deltaStateRemoved, deltaStateUnchanged + return nil, state } - return nil, deltaStateReverted } func (s *contractStakingDelta) GetBucketType(id uint64) (*BucketType, deltaState) { - if state, ok := s.bucketTypeDeltaState[id]; ok { - switch state { - case deltaStateAdded, deltaStateModified: - return s.cache.MustGetBucketType(id), state - default: - return nil, state - } + state := s.bucketTypeDeltaState[id] + switch state { + case deltaStateAdded, deltaStateModified: + return s.cache.MustGetBucketType(id), state + default: // deltaStateUnchanged + return nil, state } - return nil, deltaStateReverted } func (s *contractStakingDelta) AddBucketInfo(id uint64, bi *bucketInfo) error { @@ -108,56 +92,41 @@ func (s *contractStakingDelta) AddBucketInfo(id uint64, bi *bucketInfo) error { } func (s *contractStakingDelta) AddBucketType(id uint64, bt *BucketType) error { - if _, ok := s.bucketTypeDeltaState[id]; !ok { - s.bucketTypeDeltaState[id] = deltaStateAdded - } else { - var err error - s.bucketTypeDeltaState[id], err = s.bucketTypeDeltaState[id].Transfer(deltaActionAdd) - if err != nil { - return err - } + var err error + s.bucketTypeDeltaState[id], err = s.bucketTypeDeltaState[id].Transfer(deltaActionAdd) + if err != nil { + return err } + s.cache.PutBucketType(id, bt) return nil } func (s *contractStakingDelta) UpdateBucketType(id uint64, bt *BucketType) error { - if _, ok := s.bucketTypeDeltaState[id]; !ok { - s.bucketTypeDeltaState[id] = deltaStateModified - } else { - var err error - s.bucketTypeDeltaState[id], err = s.bucketTypeDeltaState[id].Transfer(deltaActionModify) - if err != nil { - return err - } + var err error + s.bucketTypeDeltaState[id], err = s.bucketTypeDeltaState[id].Transfer(deltaActionModify) + if err != nil { + return err } s.cache.PutBucketType(id, bt) return nil } func (s *contractStakingDelta) UpdateBucketInfo(id uint64, bi *bucketInfo) error { - if _, ok := s.bucketInfoDeltaState[id]; !ok { - s.bucketInfoDeltaState[id] = deltaStateModified - } else { - var err error - s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].Transfer(deltaActionModify) - if err != nil { - return err - } + var err error + s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].Transfer(deltaActionModify) + if err != nil { + return err } s.cache.PutBucketInfo(id, bi) return nil } func (s *contractStakingDelta) DeleteBucketInfo(id uint64) error { - if _, ok := s.bucketInfoDeltaState[id]; !ok { - s.bucketInfoDeltaState[id] = deltaStateRemoved - } else { - var err error - s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].Transfer(deltaActionRemove) - if err != nil { - return err - } + var err error + s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].Transfer(deltaActionRemove) + if err != nil { + return err } s.cache.DeleteBucketInfo(id) return nil @@ -184,21 +153,14 @@ func (s *contractStakingDelta) AddedBucketTypeCnt() uint64 { } func (s *contractStakingDelta) isBucketDeleted(id uint64) bool { - if _, ok := s.bucketInfoDeltaState[id]; ok { - return s.bucketInfoDeltaState[id] == deltaStateRemoved - } - return false + return s.bucketInfoDeltaState[id] == deltaStateRemoved } func (s *contractStakingDelta) addBucketInfo(id uint64, bi *bucketInfo) error { var err error - if _, ok := s.bucketInfoDeltaState[id]; !ok { - s.bucketInfoDeltaState[id] = deltaStateAdded - } else { - s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].Transfer(deltaActionAdd) - if err != nil { - return err - } + s.bucketInfoDeltaState[id], err = s.bucketInfoDeltaState[id].Transfer(deltaActionAdd) + if err != nil { + return err } s.cache.PutBucketInfo(id, bi) return nil diff --git a/blockindex/contractstaking/delta_cache_test.go b/blockindex/contractstaking/delta_cache_test.go new file mode 100644 index 0000000000..9242a95277 --- /dev/null +++ b/blockindex/contractstaking/delta_cache_test.go @@ -0,0 +1,269 @@ +package contractstaking + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/test/identityset" +) + +func TestContractStakingDelta_BucketInfoDelta(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // add bucket info + bi := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)} + require.NoError(cache.AddBucketInfo(1, bi)) + + // modify bucket info + bi = &bucketInfo{TypeIndex: 2, CreatedAt: 2, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)} + require.NoError(cache.UpdateBucketInfo(2, bi)) + + // remove bucket info + require.NoError(cache.DeleteBucketInfo(3)) + + // get bucket info delta + delta := cache.BucketInfoDelta() + + // check added bucket info + require.Len(delta[deltaStateAdded], 1) + added, ok := delta[deltaStateAdded][1] + require.True(ok) + require.NotNil(added) + require.EqualValues(1, added.TypeIndex) + require.EqualValues(1, added.CreatedAt) + require.EqualValues(maxBlockNumber, added.UnlockedAt) + require.EqualValues(maxBlockNumber, added.UnstakedAt) + require.EqualValues(identityset.Address(1), added.Delegate) + require.EqualValues(identityset.Address(2), added.Owner) + + // check modified bucket info + require.Len(delta[deltaStateModified], 1) + modified, ok := delta[deltaStateModified][2] + require.True(ok) + require.NotNil(modified) + require.EqualValues(2, modified.TypeIndex) + require.EqualValues(2, modified.CreatedAt) + require.EqualValues(maxBlockNumber, modified.UnlockedAt) + require.EqualValues(maxBlockNumber, modified.UnstakedAt) + require.EqualValues(identityset.Address(3), modified.Delegate) + require.EqualValues(identityset.Address(4), modified.Owner) + + // check removed bucket info + require.Len(delta[deltaStateRemoved], 1) + removed, ok := delta[deltaStateRemoved][3] + require.True(ok) + require.Nil(removed) + +} + +func TestContractStakingDelta_BucketTypeDelta(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // add bucket type + require.NoError(cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})) + require.NoError(cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1})) + + // modify bucket type 1 & 3 + require.NoError(cache.UpdateBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 3})) + require.NoError(cache.UpdateBucketType(3, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 4})) + + delta := cache.BucketTypeDelta() + // check added bucket type + require.Len(delta[deltaStateAdded], 2) + added, ok := delta[deltaStateAdded][1] + require.True(ok) + require.NotNil(added) + require.EqualValues(100, added.Amount.Int64()) + require.EqualValues(100, added.Duration) + require.EqualValues(3, added.ActivatedAt) + // check modified bucket type + modified, ok := delta[deltaStateModified][3] + require.True(ok) + require.NotNil(modified) + require.EqualValues(100, modified.Amount.Int64()) + require.EqualValues(100, modified.Duration) + require.EqualValues(4, modified.ActivatedAt) + +} + +func TestContractStakingDelta_MatchBucketType(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // test with empty bucket type + index, bucketType, ok := cache.MatchBucketType(big.NewInt(100), 100) + require.False(ok) + require.EqualValues(0, index) + require.Nil(bucketType) + + // add bucket types + require.NoError(cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})) + require.NoError(cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1})) + + // test with amount and duration that match bucket type 1 + amount := big.NewInt(100) + duration := uint64(100) + index, bucketType, ok = cache.MatchBucketType(amount, duration) + require.True(ok) + require.EqualValues(1, index) + require.NotNil(bucketType) + require.EqualValues(big.NewInt(100), bucketType.Amount) + require.EqualValues(uint64(100), bucketType.Duration) + require.EqualValues(uint64(1), bucketType.ActivatedAt) + + // test with amount and duration that match bucket type 2 + amount = big.NewInt(200) + duration = uint64(100) + index, bucketType, ok = cache.MatchBucketType(amount, duration) + require.True(ok) + require.EqualValues(2, index) + require.NotNil(bucketType) + require.EqualValues(big.NewInt(200), bucketType.Amount) + require.EqualValues(uint64(100), bucketType.Duration) + require.EqualValues(uint64(1), bucketType.ActivatedAt) + + // test with amount and duration that do not match any bucket type + amount = big.NewInt(300) + duration = uint64(100) + index, bucketType, ok = cache.MatchBucketType(amount, duration) + require.False(ok) + require.EqualValues(0, index) + require.Nil(bucketType) +} + +func TestContractStakingDelta_GetBucketInfo(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // add bucket info + bi := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)} + require.NoError(cache.AddBucketInfo(1, bi)) + + // get added bucket info + info, state := cache.GetBucketInfo(1) + require.NotNil(info) + require.EqualValues(1, info.TypeIndex) + require.EqualValues(1, info.CreatedAt) + require.EqualValues(maxBlockNumber, info.UnlockedAt) + require.EqualValues(maxBlockNumber, info.UnstakedAt) + require.EqualValues(identityset.Address(1), info.Delegate) + require.EqualValues(identityset.Address(2), info.Owner) + require.EqualValues(deltaStateAdded, state) + + // modify bucket info 2 + bi = &bucketInfo{TypeIndex: 2, CreatedAt: 2, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)} + require.NoError(cache.UpdateBucketInfo(2, bi)) + // get modified bucket info + info, state = cache.GetBucketInfo(2) + require.NotNil(info) + require.EqualValues(2, info.TypeIndex) + require.EqualValues(2, info.CreatedAt) + require.EqualValues(maxBlockNumber, info.UnlockedAt) + require.EqualValues(maxBlockNumber, info.UnstakedAt) + require.EqualValues(identityset.Address(3), info.Delegate) + require.EqualValues(identityset.Address(4), info.Owner) + require.EqualValues(deltaStateModified, state) + + // remove bucket info 2 + require.NoError(cache.DeleteBucketInfo(2)) + // get removed bucket info + info, state = cache.GetBucketInfo(2) + require.Nil(info) + require.EqualValues(deltaStateRemoved, state) +} + +func TestContractStakingDelta_GetBucketType(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // add bucket type + bt := &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1} + require.NoError(cache.AddBucketType(1, bt)) + + // get added bucket type + bucketType, state := cache.GetBucketType(1) + require.NotNil(bucketType) + require.EqualValues(big.NewInt(100), bucketType.Amount) + require.EqualValues(100, bucketType.Duration) + require.EqualValues(1, bucketType.ActivatedAt) + require.EqualValues(deltaStateAdded, state) + + // modify bucket type + bt = &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2} + require.NoError(cache.UpdateBucketType(2, bt)) + // get modified bucket type + bucketType, state = cache.GetBucketType(2) + require.NotNil(bucketType) + require.EqualValues(big.NewInt(200), bucketType.Amount) + require.EqualValues(200, bucketType.Duration) + require.EqualValues(2, bucketType.ActivatedAt) + require.EqualValues(deltaStateModified, state) + +} + +func TestContractStakingDelta_AddedBucketCnt(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // test with no added bucket info + addedBucketCnt := cache.AddedBucketCnt() + require.EqualValues(0, addedBucketCnt) + + // add bucket types + require.NoError(cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})) + require.NoError(cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1})) + + // add bucket info + bi := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)} + require.NoError(cache.AddBucketInfo(1, bi)) + // add bucket info + bi = &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)} + require.NoError(cache.AddBucketInfo(2, bi)) + + // test with added bucket info + addedBucketCnt = cache.AddedBucketCnt() + require.EqualValues(2, addedBucketCnt) + + // remove bucket info + require.NoError(cache.DeleteBucketInfo(3)) + + // test with removed bucket info + addedBucketCnt = cache.AddedBucketCnt() + require.EqualValues(2, addedBucketCnt) +} + +func TestContractStakingDelta_AddedBucketTypeCnt(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // test with no added bucket types + addedBucketTypeCnt := cache.AddedBucketTypeCnt() + require.EqualValues(0, addedBucketTypeCnt) + + // add bucket types + require.NoError(cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})) + require.NoError(cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1})) + require.NoError(cache.AddBucketType(3, &BucketType{Amount: big.NewInt(300), Duration: 100, ActivatedAt: 1})) + + // test with added bucket type + addedBucketTypeCnt = cache.AddedBucketTypeCnt() + require.EqualValues(3, addedBucketTypeCnt) +} diff --git a/blockindex/contractstaking/delta_state.go b/blockindex/contractstaking/delta_state.go index 053a6faf19..4375f8d248 100644 --- a/blockindex/contractstaking/delta_state.go +++ b/blockindex/contractstaking/delta_state.go @@ -8,30 +8,30 @@ package contractstaking import "github.com/pkg/errors" const ( - deltaStateAdded deltaState = iota + // deltaState constants + // deltaStateUnchanged is the zero-value of the type deltaState + deltaStateUnchanged deltaState = iota + deltaStateAdded deltaStateRemoved deltaStateModified - deltaStateReverted ) type deltaState int var ( deltaStateTransferMap = map[deltaState]map[deltaAction]deltaState{ + deltaStateUnchanged: { + deltaActionAdd: deltaStateAdded, + deltaActionRemove: deltaStateRemoved, + deltaActionModify: deltaStateModified, + }, deltaStateAdded: { - deltaActionRemove: deltaStateReverted, deltaActionModify: deltaStateAdded, }, - deltaStateRemoved: { - deltaActionAdd: deltaStateModified, - }, deltaStateModified: { deltaActionModify: deltaStateModified, deltaActionRemove: deltaStateRemoved, }, - deltaStateReverted: { - deltaActionAdd: deltaStateAdded, - }, } ) diff --git a/blockindex/contractstaking/delta_state_test.go b/blockindex/contractstaking/delta_state_test.go new file mode 100644 index 0000000000..73c5895c8e --- /dev/null +++ b/blockindex/contractstaking/delta_state_test.go @@ -0,0 +1,54 @@ +package contractstaking + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDeltaState_Transfer(t *testing.T) { + require := require.New(t) + + cases := []struct { + name string + state deltaState + action deltaAction + expected deltaState + err string + }{ + {"unchanged->add", deltaStateUnchanged, deltaActionAdd, deltaStateAdded, ""}, + {"unchanged->remove", deltaStateUnchanged, deltaActionRemove, deltaStateRemoved, ""}, + {"unchanged->modify", deltaStateUnchanged, deltaActionModify, deltaStateModified, ""}, + {"added->add", deltaStateAdded, deltaActionAdd, deltaStateUnchanged, "invalid delta action 0 on state 1"}, + {"added->remove", deltaStateAdded, deltaActionRemove, deltaStateUnchanged, "invalid delta action 1 on state 1"}, + {"added->modify", deltaStateAdded, deltaActionModify, deltaStateAdded, ""}, + {"removed->add", deltaStateRemoved, deltaActionAdd, deltaStateUnchanged, "invalid delta state 2"}, + {"removed->remove", deltaStateRemoved, deltaActionRemove, deltaStateUnchanged, "invalid delta state 2"}, + {"removed->modify", deltaStateRemoved, deltaActionModify, deltaStateUnchanged, "invalid delta state 2"}, + {"modified->add", deltaStateModified, deltaActionAdd, deltaStateUnchanged, "invalid delta action 0 on state 3"}, + {"modified->remove", deltaStateModified, deltaActionRemove, deltaStateRemoved, ""}, + {"modified->modify", deltaStateModified, deltaActionModify, deltaStateModified, ""}, + {"invalid state", deltaState(100), deltaActionAdd, deltaState(100), "invalid delta state 100"}, + {"invalid action", deltaStateUnchanged, deltaAction(100), deltaStateUnchanged, "invalid delta action 100 on state 0"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + s, err := c.state.Transfer(c.action) + if len(c.err) > 0 { + require.Error(err) + require.Contains(err.Error(), c.err) + } else { + require.NoError(err) + require.Equal(c.expected, s) + } + }) + } +} + +func TestDeltaState_ZeroValue(t *testing.T) { + require := require.New(t) + + var state deltaState + require.Equal(deltaStateUnchanged, state) +} diff --git a/blockindex/contractstaking/dirty_cache.go b/blockindex/contractstaking/dirty_cache.go index 22b8bc02d8..222b097d67 100644 --- a/blockindex/contractstaking/dirty_cache.go +++ b/blockindex/contractstaking/dirty_cache.go @@ -51,11 +51,6 @@ func newContractStakingDirty(clean *contractStakingCache) *contractStakingDirty } } -func (dirty *contractStakingDirty) putHeight(h uint64) { - dirty.batch.Put(_StakingNS, _stakingHeightKey, byteutil.Uint64ToBytesBigEndian(h), "failed to put height") - dirty.delta.PutHeight(h) -} - func (dirty *contractStakingDirty) addBucketInfo(id uint64, bi *bucketInfo) error { dirty.batch.Put(_StakingBucketInfoNS, byteutil.Uint64ToBytesBigEndian(id), bi.Serialize(), "failed to put bucket info") return dirty.delta.AddBucketInfo(id, bi) diff --git a/blockindex/contractstaking/dirty_cache_test.go b/blockindex/contractstaking/dirty_cache_test.go new file mode 100644 index 0000000000..0b501bb597 --- /dev/null +++ b/blockindex/contractstaking/dirty_cache_test.go @@ -0,0 +1,266 @@ +package contractstaking + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/db/batch" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" + "github.com/iotexproject/iotex-core/test/identityset" +) + +func TestContractStakingDirty_getBucketType(t *testing.T) { + require := require.New(t) + clean := newContractStakingCache("") + dirty := newContractStakingDirty(clean) + + // no bucket type + bt, ok := dirty.getBucketType(1) + require.False(ok) + require.Nil(bt) + + // bucket type in clean cache + clean.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + bt, ok = dirty.getBucketType(1) + require.True(ok) + require.EqualValues(100, bt.Amount.Int64()) + require.EqualValues(100, bt.Duration) + require.EqualValues(1, bt.ActivatedAt) + + // added bucket type + dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) + bt, ok = dirty.getBucketType(2) + require.True(ok) + require.EqualValues(200, bt.Amount.Int64()) + require.EqualValues(200, bt.Duration) + require.EqualValues(2, bt.ActivatedAt) +} + +func TestContractStakingDirty_getBucketInfo(t *testing.T) { + require := require.New(t) + clean := newContractStakingCache("") + dirty := newContractStakingDirty(clean) + + // no bucket info + bi, ok := dirty.getBucketInfo(1) + require.False(ok) + require.Nil(bi) + + // bucket info in clean cache + clean.putBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + clean.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + bi, ok = dirty.getBucketInfo(1) + require.True(ok) + require.EqualValues(1, bi.TypeIndex) + require.EqualValues(1, bi.CreatedAt) + require.EqualValues(maxBlockNumber, bi.UnlockedAt) + require.EqualValues(maxBlockNumber, bi.UnstakedAt) + require.Equal(identityset.Address(1), bi.Delegate) + require.Equal(identityset.Address(2), bi.Owner) + + // added bucket info + require.NoError(dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2})) + require.NoError(dirty.addBucketInfo(2, &bucketInfo{TypeIndex: 2, CreatedAt: 2, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(2), Owner: identityset.Address(3)})) + bi, ok = dirty.getBucketInfo(2) + require.True(ok) + require.EqualValues(2, bi.TypeIndex) + require.EqualValues(2, bi.CreatedAt) + require.EqualValues(maxBlockNumber, bi.UnlockedAt) + require.EqualValues(maxBlockNumber, bi.UnstakedAt) + require.Equal(identityset.Address(2), bi.Delegate) + require.Equal(identityset.Address(3), bi.Owner) + + // modified bucket info + require.NoError(dirty.updateBucketInfo(1, &bucketInfo{TypeIndex: 2, CreatedAt: 3, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)})) + bi, ok = dirty.getBucketInfo(1) + require.True(ok) + require.EqualValues(2, bi.TypeIndex) + require.EqualValues(3, bi.CreatedAt) + require.EqualValues(maxBlockNumber, bi.UnlockedAt) + require.EqualValues(maxBlockNumber, bi.UnstakedAt) + require.Equal(identityset.Address(3), bi.Delegate) + require.Equal(identityset.Address(4), bi.Owner) + + // removed bucket info + require.NoError(dirty.deleteBucketInfo(1)) + bi, ok = dirty.getBucketInfo(1) + require.False(ok) + require.Nil(bi) +} + +func TestContractStakingDirty_matchBucketType(t *testing.T) { + require := require.New(t) + clean := newContractStakingCache("") + dirty := newContractStakingDirty(clean) + + // no bucket type + id, bt, ok := dirty.matchBucketType(big.NewInt(100), 100) + require.False(ok) + require.Nil(bt) + require.EqualValues(0, id) + + // bucket type in clean cache + clean.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + id, bt, ok = dirty.matchBucketType(big.NewInt(100), 100) + require.True(ok) + require.EqualValues(100, bt.Amount.Int64()) + require.EqualValues(100, bt.Duration) + require.EqualValues(1, bt.ActivatedAt) + require.EqualValues(1, id) + + // added bucket type + require.NoError(dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2})) + id, bt, ok = dirty.matchBucketType(big.NewInt(200), 200) + require.True(ok) + require.EqualValues(200, bt.Amount.Int64()) + require.EqualValues(200, bt.Duration) + require.EqualValues(2, bt.ActivatedAt) + require.EqualValues(2, id) +} + +func TestContractStakingDirty_getBucketTypeCount(t *testing.T) { + require := require.New(t) + clean := newContractStakingCache("") + dirty := newContractStakingDirty(clean) + + // no bucket type + count := dirty.getBucketTypeCount() + require.EqualValues(0, count) + + // bucket type in clean cache + clean.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + count = dirty.getBucketTypeCount() + require.EqualValues(1, count) + + // added bucket type + require.NoError(dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2})) + count = dirty.getBucketTypeCount() + require.EqualValues(2, count) +} + +func TestContractStakingDirty_finalize(t *testing.T) { + require := require.New(t) + clean := newContractStakingCache("") + dirty := newContractStakingDirty(clean) + + // no dirty data + batcher, delta := dirty.finalize() + require.EqualValues(1, batcher.Size()) + info, err := batcher.Entry(0) + require.NoError(err) + require.EqualValues(_StakingNS, info.Namespace()) + require.EqualValues(batch.Put, info.WriteType()) + require.EqualValues(_stakingTotalBucketCountKey, info.Key()) + require.EqualValues(byteutil.Uint64ToBytesBigEndian(0), info.Value()) + for _, d := range delta.BucketTypeDelta() { + require.Len(d, 0) + } + for _, d := range delta.BucketTypeDelta() { + require.Len(d, 0) + } + + // added bucket type + bt := &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1} + require.NoError(dirty.addBucketType(1, bt)) + batcher, delta = dirty.finalize() + require.EqualValues(2, batcher.Size()) + info, err = batcher.Entry(1) + require.NoError(err) + require.EqualValues(_StakingBucketTypeNS, info.Namespace()) + require.EqualValues(batch.Put, info.WriteType()) + require.EqualValues(byteutil.Uint64ToBytesBigEndian(1), info.Key()) + require.EqualValues(bt.Serialize(), info.Value()) + btDelta := delta.BucketTypeDelta() + require.NotNil(btDelta[deltaStateAdded]) + require.Len(btDelta[deltaStateAdded], 1) + require.EqualValues(100, btDelta[deltaStateAdded][1].Amount.Int64()) + require.EqualValues(100, btDelta[deltaStateAdded][1].Duration) + require.EqualValues(1, btDelta[deltaStateAdded][1].ActivatedAt) + + // add bucket info + bi := &bucketInfo{TypeIndex: 1, CreatedAt: 2, UnlockedAt: 3, UnstakedAt: 4, Delegate: identityset.Address(1), Owner: identityset.Address(2)} + require.NoError(dirty.addBucketInfo(1, bi)) + batcher, delta = dirty.finalize() + require.EqualValues(3, batcher.Size()) + info, err = batcher.Entry(2) + require.NoError(err) + require.EqualValues(_StakingBucketInfoNS, info.Namespace()) + require.EqualValues(batch.Put, info.WriteType()) + require.EqualValues(byteutil.Uint64ToBytesBigEndian(1), info.Key()) + require.EqualValues(bi.Serialize(), info.Value()) + biDelta := delta.BucketInfoDelta() + require.NotNil(biDelta[deltaStateAdded]) + require.Len(biDelta[deltaStateAdded], 1) + require.EqualValues(1, biDelta[deltaStateAdded][1].TypeIndex) + require.EqualValues(2, biDelta[deltaStateAdded][1].CreatedAt) + require.EqualValues(3, biDelta[deltaStateAdded][1].UnlockedAt) + require.EqualValues(4, biDelta[deltaStateAdded][1].UnstakedAt) + require.EqualValues(identityset.Address(1).String(), biDelta[deltaStateAdded][1].Delegate.String()) + require.EqualValues(identityset.Address(2).String(), biDelta[deltaStateAdded][1].Owner.String()) + +} + +func TestContractStakingDirty_noSideEffectOnClean(t *testing.T) { + require := require.New(t) + clean := newContractStakingCache("") + dirty := newContractStakingDirty(clean) + + // add bucket type to dirty cache + require.NoError(dirty.addBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})) + // check that clean cache is not affected + bt, ok := clean.getBucketType(1) + require.False(ok) + require.Nil(bt) + + // add bucket info to dirty cache + require.NoError(dirty.addBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})) + // check that clean cache is not affected + bi, ok := clean.getBucketInfo(1) + require.False(ok) + require.Nil(bi) + + // update bucket type in dirty cache + require.NoError(dirty.updateBucketType(1, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2})) + // check that clean cache is not affected + bt, ok = clean.getBucketType(1) + require.False(ok) + require.Nil(bt) + + // update bucket info in dirty cache + require.NoError(dirty.updateBucketInfo(1, &bucketInfo{TypeIndex: 2, CreatedAt: 3, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)})) + // check that clean cache is not affected + bi, ok = clean.getBucketInfo(1) + require.False(ok) + require.Nil(bi) + + // update bucket info existed in clean cache + clean.PutBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + // update bucket info in dirty cache + require.NoError(dirty.updateBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 3, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)})) + // check that clean cache is not affected + bi, ok = clean.getBucketInfo(2) + require.True(ok) + require.EqualValues(1, bi.TypeIndex) + require.EqualValues(1, bi.CreatedAt) + require.EqualValues(maxBlockNumber, bi.UnlockedAt) + require.EqualValues(maxBlockNumber, bi.UnstakedAt) + require.EqualValues(identityset.Address(1).String(), bi.Delegate.String()) + require.EqualValues(identityset.Address(2).String(), bi.Owner.String()) + + // remove bucket info existed in clean cache + clean.PutBucketInfo(3, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + // remove bucket info from dirty cache + require.NoError(dirty.deleteBucketInfo(3)) + // check that clean cache is not affected + bi, ok = clean.getBucketInfo(3) + require.True(ok) + require.EqualValues(1, bi.TypeIndex) + require.EqualValues(1, bi.CreatedAt) + require.EqualValues(maxBlockNumber, bi.UnlockedAt) + require.EqualValues(maxBlockNumber, bi.UnstakedAt) + require.EqualValues(identityset.Address(1).String(), bi.Delegate.String()) + require.EqualValues(identityset.Address(2).String(), bi.Owner.String()) + +} diff --git a/blockindex/contractstaking/event_handler.go b/blockindex/contractstaking/event_handler.go index 5d4fe76493..cf6c17c357 100644 --- a/blockindex/contractstaking/event_handler.go +++ b/blockindex/contractstaking/event_handler.go @@ -366,9 +366,8 @@ func init() { } } -func newContractStakingEventHandler(cache *contractStakingCache, height uint64) *contractStakingEventHandler { +func newContractStakingEventHandler(cache *contractStakingCache) *contractStakingEventHandler { dirty := newContractStakingDirty(cache) - dirty.putHeight(height) return &contractStakingEventHandler{ dirty: dirty, tokenOwner: make(map[uint64]address.Address), diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index f4e063d188..09099800c1 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -8,6 +8,7 @@ package contractstaking import ( "context" "math/big" + "sync/atomic" "github.com/ethereum/go-ethereum/common/math" "github.com/iotexproject/iotex-address/address" @@ -17,6 +18,7 @@ import ( "github.com/iotexproject/iotex-core/blockchain/block" "github.com/iotexproject/iotex-core/blockchain/blockdao" "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" ) const ( @@ -52,6 +54,7 @@ type ( cache *contractStakingCache // in-memory index for clean data, used to query index data contractAddress string // stake contract address contractDeployHeight uint64 // height of the contract deployment + height atomic.Value // uint64, current block height } ) @@ -76,7 +79,7 @@ func (s *Indexer) Start(ctx context.Context) error { if err := s.kvstore.Start(ctx); err != nil { return err } - return s.cache.LoadFromDB(s.kvstore) + return s.loadFromDB() } // Stop stops the indexer @@ -90,7 +93,7 @@ func (s *Indexer) Stop(ctx context.Context) error { // Height returns the tip block height func (s *Indexer) Height() (uint64, error) { - return s.cache.Height(), nil + return s.height.Load().(uint64), nil } // StartHeight returns the start height of the indexer @@ -140,11 +143,11 @@ func (s *Indexer) BucketTypes() ([]*BucketType, error) { // PutBlock puts a block into indexer func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error { - if blk.Height() < s.contractDeployHeight { + if blk.Height() < s.contractDeployHeight || blk.Height() <= s.height.Load().(uint64) { return nil } // new event handler for this block - handler := newContractStakingEventHandler(s.cache, blk.Height()) + handler := newContractStakingEventHandler(s.cache) // handle events of block for _, receipt := range blk.Receipts { @@ -162,7 +165,7 @@ func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error { } // commit the result - return s.commit(handler) + return s.commit(handler, blk.Height()) } // DeleteTipBlock deletes the tip block from indexer @@ -170,20 +173,43 @@ func (s *Indexer) DeleteTipBlock(context.Context, *block.Block) error { return errors.New("not implemented") } -func (s *Indexer) commit(handler *contractStakingEventHandler) error { +func (s *Indexer) commit(handler *contractStakingEventHandler, height uint64) error { batch, delta := handler.Result() + // update cache if err := s.cache.Merge(delta); err != nil { s.reloadCache() return err } + // update db + batch.Put(_StakingNS, _stakingHeightKey, byteutil.Uint64ToBytesBigEndian(height), "failed to put height") if err := s.kvstore.WriteBatch(batch); err != nil { s.reloadCache() return err } + // update indexer height cache + s.height.Store(height) return nil } func (s *Indexer) reloadCache() error { s.cache = newContractStakingCache(s.contractAddress) + return s.loadFromDB() +} + +func (s *Indexer) loadFromDB() error { + // load height + var height uint64 + h, err := s.kvstore.Get(_StakingNS, _stakingHeightKey) + if err != nil { + if !errors.Is(err, db.ErrNotExist) { + return err + } + height = 0 + } else { + height = byteutil.BytesToUint64BigEndian(h) + + } + s.height.Store(height) + // load cache return s.cache.LoadFromDB(s.kvstore) } diff --git a/blockindex/contractstaking/indexer_test.go b/blockindex/contractstaking/indexer_test.go index d5d2e03838..f9e3d576a7 100644 --- a/blockindex/contractstaking/indexer_test.go +++ b/blockindex/contractstaking/indexer_test.go @@ -10,13 +10,16 @@ import ( "math/big" "sync" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/iotexproject/iotex-address/address" "github.com/stretchr/testify/require" "golang.org/x/exp/slices" + "github.com/iotexproject/iotex-core/action/protocol/staking" "github.com/iotexproject/iotex-core/blockchain/block" + "github.com/iotexproject/iotex-core/config" "github.com/iotexproject/iotex-core/db" "github.com/iotexproject/iotex-core/test/identityset" "github.com/iotexproject/iotex-core/testutil" @@ -66,15 +69,21 @@ func TestContractStakingIndexerLoadCache(t *testing.T) { // create a stake height := uint64(1) startHeight := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) activateBucketType(r, handler, 10, 100, height) owner := identityset.Address(0) delegate := identityset.Address(1) stake(r, handler, owner, delegate, 1, 10, 100, height) - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) buckets, err := indexer.Buckets() r.NoError(err) + r.EqualValues(1, len(buckets)) + r.EqualValues(1, indexer.TotalBucketCount()) + h, err := indexer.Height() + r.NoError(err) + r.EqualValues(height, h) + r.NoError(indexer.Stop(context.Background())) // load cache from db @@ -111,12 +120,12 @@ func TestContractStakingIndexerDirty(t *testing.T) { // before commit dirty, the cache should be empty height := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) gotHeight, err := indexer.Height() r.NoError(err) r.EqualValues(0, gotHeight) // after commit dirty, the cache should be updated - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) gotHeight, err = indexer.Height() r.NoError(err) @@ -163,14 +172,14 @@ func TestContractStakingIndexerThreadSafe(t *testing.T) { go func() { defer wait.Done() // activate bucket type - handler := newContractStakingEventHandler(indexer.cache, 1) + handler := newContractStakingEventHandler(indexer.cache) activateBucketType(r, handler, 10, 100, 1) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, 1)) for i := 2; i < 1000; i++ { height := uint64(i) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) stake(r, handler, owner, delegate, int64(i), 10, 100, height) - err := indexer.commit(handler) + err := indexer.commit(handler, height) r.NoError(err) } }() @@ -209,11 +218,11 @@ func TestContractStakingIndexerBucketType(t *testing.T) { } height := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) for _, data := range bucketTypeData { activateBucketType(r, handler, data[0], data[1], height) } - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) bucketTypes, err := indexer.BucketTypes() r.NoError(err) @@ -224,12 +233,12 @@ func TestContractStakingIndexerBucketType(t *testing.T) { } // deactivate height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) for i := 0; i < 2; i++ { data := bucketTypeData[i] deactivateBucketType(r, handler, data[0], data[1], height) } - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) bucketTypes, err = indexer.BucketTypes() r.NoError(err) @@ -240,12 +249,12 @@ func TestContractStakingIndexerBucketType(t *testing.T) { } // reactivate height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) for i := 0; i < 2; i++ { data := bucketTypeData[i] activateBucketType(r, handler, data[0], data[1], height) } - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) bucketTypes, err = indexer.BucketTypes() r.NoError(err) @@ -282,11 +291,11 @@ func TestContractStakingIndexerBucketInfo(t *testing.T) { {20, 100}, } height := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) for _, data := range bucketTypeData { activateBucketType(r, handler, data[0], data[1], height) } - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) // stake @@ -294,10 +303,10 @@ func TestContractStakingIndexerBucketInfo(t *testing.T) { delegate := identityset.Address(1) height++ createHeight := height - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) stake(r, handler, owner, delegate, 1, 10, 100, height) r.NoError(err) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok := indexer.Bucket(1) r.True(ok) r.EqualValues(1, bucket.Index) @@ -316,18 +325,18 @@ func TestContractStakingIndexerBucketInfo(t *testing.T) { // transfer newOwner := identityset.Address(2) height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) transfer(r, handler, newOwner, int64(bucket.Index)) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok = indexer.Bucket(bucket.Index) r.True(ok) r.EqualValues(newOwner, bucket.Owner) // unlock height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) unlock(r, handler, int64(bucket.Index), height) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok = indexer.Bucket(bucket.Index) r.True(ok) r.EqualValues(1, bucket.Index) @@ -345,9 +354,9 @@ func TestContractStakingIndexerBucketInfo(t *testing.T) { // lock again height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) lock(r, handler, int64(bucket.Index), int64(10)) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok = indexer.Bucket(bucket.Index) r.True(ok) r.EqualValues(1, bucket.Index) @@ -365,10 +374,10 @@ func TestContractStakingIndexerBucketInfo(t *testing.T) { // unstake height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) unlock(r, handler, int64(bucket.Index), height) unstake(r, handler, int64(bucket.Index), height) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok = indexer.Bucket(bucket.Index) r.True(ok) r.EqualValues(1, bucket.Index) @@ -386,9 +395,9 @@ func TestContractStakingIndexerBucketInfo(t *testing.T) { // withdraw height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) withdraw(r, handler, int64(bucket.Index)) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok = indexer.Bucket(bucket.Index) r.False(ok) r.EqualValues(0, indexer.CandidateVotes(delegate).Uint64()) @@ -415,26 +424,26 @@ func TestContractStakingIndexerChangeBucketType(t *testing.T) { {20, 100}, } height := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) for _, data := range bucketTypeData { activateBucketType(r, handler, data[0], data[1], height) } - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) t.Run("expand bucket type", func(t *testing.T) { owner := identityset.Address(0) delegate := identityset.Address(1) height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) stake(r, handler, owner, delegate, 1, 10, 100, height) r.NoError(err) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok := indexer.Bucket(1) r.True(ok) expandBucketType(r, handler, int64(bucket.Index), 20, 100) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bucket, ok = indexer.Bucket(bucket.Index) r.True(ok) r.EqualValues(20, bucket.StakedAmount.Int64()) @@ -462,11 +471,11 @@ func TestContractStakingIndexerReadBuckets(t *testing.T) { {20, 100}, } height := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) for _, data := range bucketTypeData { activateBucketType(r, handler, data[0], data[1], height) } - err = indexer.commit(handler) + err = indexer.commit(handler, height) r.NoError(err) // stake @@ -482,12 +491,12 @@ func TestContractStakingIndexerReadBuckets(t *testing.T) { {1, 3, 20, 100}, } height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) for i, data := range stakeData { stake(r, handler, identityset.Address(data.owner), identityset.Address(data.delegate), int64(i), int64(data.amount), int64(data.duration), height) } r.NoError(err) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) t.Run("Buckets", func(t *testing.T) { buckets, err := indexer.Buckets() @@ -537,6 +546,322 @@ func TestContractStakingIndexerReadBuckets(t *testing.T) { }) } +func TestContractStakingIndexerCacheClean(t *testing.T) { + r := require.New(t) + testDBPath, err := testutil.PathOfTempFile("staking.db") + r.NoError(err) + defer testutil.CleanupPath(testDBPath) + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + indexer, err := NewContractStakingIndexer(kvStore, _testStakingContractAddress, 0) + r.NoError(err) + r.NoError(indexer.Start(context.Background())) + + // init bucket type + height := uint64(1) + handler := newContractStakingEventHandler(indexer.cache) + activateBucketType(r, handler, 10, 10, height) + activateBucketType(r, handler, 20, 20, height) + // create bucket + owner := identityset.Address(10) + delegate1 := identityset.Address(1) + delegate2 := identityset.Address(2) + stake(r, handler, owner, delegate1, 1, 10, 10, height) + stake(r, handler, owner, delegate1, 2, 20, 20, height) + stake(r, handler, owner, delegate2, 3, 20, 20, height) + stake(r, handler, owner, delegate2, 4, 20, 20, height) + r.Len(indexer.cache.ActiveBucketTypes(), 0) + r.Len(indexer.cache.Buckets(), 0) + r.NoError(indexer.commit(handler, height)) + r.Len(indexer.cache.ActiveBucketTypes(), 2) + r.Len(indexer.cache.Buckets(), 4) + + height++ + handler = newContractStakingEventHandler(indexer.cache) + changeDelegate(r, handler, delegate1, 3) + transfer(r, handler, delegate1, 1) + bt, ok := indexer.Bucket(3) + r.True(ok) + r.Equal(delegate2.String(), bt.Candidate.String()) + bt, ok = indexer.Bucket(1) + r.True(ok) + r.Equal(owner.String(), bt.Owner.String()) + r.NoError(indexer.commit(handler, height)) + bt, ok = indexer.Bucket(3) + r.True(ok) + r.Equal(delegate1.String(), bt.Candidate.String()) + bt, ok = indexer.Bucket(1) + r.True(ok) + r.Equal(delegate1.String(), bt.Owner.String()) +} + +func TestContractStakingIndexerVotes(t *testing.T) { + r := require.New(t) + testDBPath, err := testutil.PathOfTempFile("staking.db") + r.NoError(err) + defer testutil.CleanupPath(testDBPath) + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + indexer, err := NewContractStakingIndexer(kvStore, _testStakingContractAddress, 0) + r.NoError(err) + r.NoError(indexer.Start(context.Background())) + + // init bucket type + height := uint64(1) + handler := newContractStakingEventHandler(indexer.cache) + activateBucketType(r, handler, 10, 10, height) + activateBucketType(r, handler, 20, 20, height) + activateBucketType(r, handler, 30, 20, height) + activateBucketType(r, handler, 60, 20, height) + // create bucket + owner := identityset.Address(10) + delegate1 := identityset.Address(1) + delegate2 := identityset.Address(2) + stake(r, handler, owner, delegate1, 1, 10, 10, height) + stake(r, handler, owner, delegate1, 2, 20, 20, height) + stake(r, handler, owner, delegate2, 3, 20, 20, height) + stake(r, handler, owner, delegate2, 4, 20, 20, height) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(30, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(40, indexer.CandidateVotes(delegate2).Uint64()) + r.EqualValues(0, indexer.CandidateVotes(owner).Uint64()) + + // change delegate bucket 3 to delegate1 + height++ + handler = newContractStakingEventHandler(indexer.cache) + changeDelegate(r, handler, delegate1, 3) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // unlock bucket 1 & 4 + height++ + handler = newContractStakingEventHandler(indexer.cache) + unlock(r, handler, 1, height) + unlock(r, handler, 4, height) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // unstake bucket 1 & lock 4 + height++ + handler = newContractStakingEventHandler(indexer.cache) + unstake(r, handler, 1, height) + lock(r, handler, 4, 20) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(40, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // expand bucket 2 + height++ + handler = newContractStakingEventHandler(indexer.cache) + expandBucketType(r, handler, 2, 30, 20) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // transfer bucket 4 + height++ + handler = newContractStakingEventHandler(indexer.cache) + transfer(r, handler, delegate2, 4) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // create bucket 5, 6, 7 + height++ + handler = newContractStakingEventHandler(indexer.cache) + stake(r, handler, owner, delegate2, 5, 20, 20, height) + stake(r, handler, owner, delegate2, 6, 20, 20, height) + stake(r, handler, owner, delegate2, 7, 20, 20, height) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(80, indexer.CandidateVotes(delegate2).Uint64()) + + // merge bucket 5, 6, 7 + height++ + handler = newContractStakingEventHandler(indexer.cache) + mergeBuckets(r, handler, []int64{5, 6, 7}, 60, 20) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(80, indexer.CandidateVotes(delegate2).Uint64()) + + // unlock & unstake 5 + height++ + handler = newContractStakingEventHandler(indexer.cache) + unlock(r, handler, 5, height) + unstake(r, handler, 5, height) + r.NoError(indexer.commit(handler, height)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + t.Run("Height", func(t *testing.T) { + h, err := indexer.Height() + r.NoError(err) + r.EqualValues(height, h) + }) + + t.Run("BucketTypes", func(t *testing.T) { + bts, err := indexer.BucketTypes() + r.NoError(err) + r.Len(bts, 4) + slices.SortFunc(bts, func(i, j *staking.ContractStakingBucketType) bool { + return i.Amount.Int64() < j.Amount.Int64() + }) + r.EqualValues(10, bts[0].Duration) + r.EqualValues(20, bts[1].Duration) + r.EqualValues(20, bts[2].Duration) + r.EqualValues(20, bts[3].Duration) + r.EqualValues(10, bts[0].Amount.Int64()) + r.EqualValues(20, bts[1].Amount.Int64()) + r.EqualValues(30, bts[2].Amount.Int64()) + r.EqualValues(60, bts[3].Amount.Int64()) + }) + + t.Run("Buckets", func(t *testing.T) { + bts, err := indexer.Buckets() + r.NoError(err) + r.Len(bts, 5) + slices.SortFunc(bts, func(i, j *staking.VoteBucket) bool { + return i.Index < j.Index + }) + r.EqualValues(1, bts[0].Index) + r.EqualValues(2, bts[1].Index) + r.EqualValues(3, bts[2].Index) + r.EqualValues(4, bts[3].Index) + r.EqualValues(5, bts[4].Index) + r.EqualValues(10, bts[0].StakedDurationBlockNumber) + r.EqualValues(20, bts[1].StakedDurationBlockNumber) + r.EqualValues(20, bts[2].StakedDurationBlockNumber) + r.EqualValues(20, bts[3].StakedDurationBlockNumber) + r.EqualValues(20, bts[4].StakedDurationBlockNumber) + r.EqualValues(10, bts[0].StakedAmount.Int64()) + r.EqualValues(30, bts[1].StakedAmount.Int64()) + r.EqualValues(20, bts[2].StakedAmount.Int64()) + r.EqualValues(20, bts[3].StakedAmount.Int64()) + r.EqualValues(60, bts[4].StakedAmount.Int64()) + r.EqualValues(delegate1.String(), bts[0].Candidate.String()) + r.EqualValues(delegate1.String(), bts[1].Candidate.String()) + r.EqualValues(delegate1.String(), bts[2].Candidate.String()) + r.EqualValues(delegate2.String(), bts[3].Candidate.String()) + r.EqualValues(delegate2.String(), bts[4].Candidate.String()) + r.EqualValues(owner.String(), bts[0].Owner.String()) + r.EqualValues(owner.String(), bts[1].Owner.String()) + r.EqualValues(owner.String(), bts[2].Owner.String()) + r.EqualValues(delegate2.String(), bts[3].Owner.String()) + r.EqualValues(owner.String(), bts[4].Owner.String()) + r.False(bts[0].AutoStake) + r.True(bts[1].AutoStake) + r.True(bts[2].AutoStake) + r.True(bts[3].AutoStake) + r.False(bts[4].AutoStake) + r.EqualValues(1, bts[0].CreateBlockHeight) + r.EqualValues(1, bts[1].CreateBlockHeight) + r.EqualValues(1, bts[2].CreateBlockHeight) + r.EqualValues(1, bts[3].CreateBlockHeight) + r.EqualValues(7, bts[4].CreateBlockHeight) + r.EqualValues(3, bts[0].StakeStartBlockHeight) + r.EqualValues(1, bts[1].StakeStartBlockHeight) + r.EqualValues(1, bts[2].StakeStartBlockHeight) + r.EqualValues(1, bts[3].StakeStartBlockHeight) + r.EqualValues(9, bts[4].StakeStartBlockHeight) + r.EqualValues(4, bts[0].UnstakeStartBlockHeight) + r.EqualValues(maxBlockNumber, bts[1].UnstakeStartBlockHeight) + r.EqualValues(maxBlockNumber, bts[2].UnstakeStartBlockHeight) + r.EqualValues(maxBlockNumber, bts[3].UnstakeStartBlockHeight) + r.EqualValues(9, bts[4].UnstakeStartBlockHeight) + for _, b := range bts { + r.EqualValues(0, b.StakedDuration) + r.EqualValues(time.Time{}, b.CreateTime) + r.EqualValues(time.Time{}, b.StakeStartTime) + r.EqualValues(time.Time{}, b.UnstakeStartTime) + r.EqualValues(_testStakingContractAddress, b.ContractAddress) + } + }) + + t.Run("BucketsByCandidate", func(t *testing.T) { + d1Bts, err := indexer.BucketsByCandidate(delegate1) + r.NoError(err) + r.Len(d1Bts, 3) + slices.SortFunc(d1Bts, func(i, j *staking.VoteBucket) bool { + return i.Index < j.Index + }) + r.EqualValues(1, d1Bts[0].Index) + r.EqualValues(2, d1Bts[1].Index) + r.EqualValues(3, d1Bts[2].Index) + d2Bts, err := indexer.BucketsByCandidate(delegate2) + r.NoError(err) + r.Len(d2Bts, 2) + slices.SortFunc(d2Bts, func(i, j *staking.VoteBucket) bool { + return i.Index < j.Index + }) + r.EqualValues(4, d2Bts[0].Index) + r.EqualValues(5, d2Bts[1].Index) + }) + + t.Run("BucketsByIndices", func(t *testing.T) { + bts, err := indexer.BucketsByIndices([]uint64{1, 2, 3, 4, 5}) + r.NoError(err) + r.Len(bts, 5) + }) +} + +func TestIndexer_PutBlock(t *testing.T) { + r := require.New(t) + + cases := []struct { + name string + height uint64 + startHeight uint64 + blockHeight uint64 + expectedHeight uint64 + }{ + {"block < height < start", 10, 20, 9, 10}, + {"block = height < start", 10, 20, 10, 10}, + {"height < block < start", 10, 20, 11, 10}, + {"height < block = start", 10, 20, 20, 20}, + {"height < start < block", 10, 20, 21, 21}, + {"block < start < height", 20, 10, 9, 20}, + {"block = start < height", 20, 10, 10, 20}, + {"start < block < height", 20, 10, 11, 20}, + {"start < block = height", 20, 10, 20, 20}, + {"start < height < block", 20, 10, 21, 21}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // Create a new Indexer + height := c.height + startHeight := c.startHeight + cfg := config.Default.DB + dbPath, err := testutil.PathOfTempFile("db") + r.NoError(err) + cfg.DbPath = dbPath + indexer, err := NewContractStakingIndexer(db.NewBoltDB(cfg), identityset.Address(1).String(), startHeight) + r.NoError(err) + r.NoError(indexer.Start(context.Background())) + defer func() { + r.NoError(indexer.Stop(context.Background())) + testutil.CleanupPath(dbPath) + }() + indexer.height.Store(height) + // Create a mock block + builder := block.NewBuilder(block.NewRunnableActionsBuilder().Build()) + builder.SetHeight(c.blockHeight) + blk, err := builder.SignAndBuild(identityset.PrivateKey(1)) + r.NoError(err) + // Put the block + err = indexer.PutBlock(context.Background(), &blk) + r.NoError(err) + // Check the block height + r.EqualValues(c.expectedHeight, indexer.height.Load().(uint64)) + }) + } + +} + func BenchmarkIndexer_PutBlockBeforeContractHeight(b *testing.B) { // Create a new Indexer with a contract height of 100 indexer := &Indexer{contractDeployHeight: 100} @@ -630,3 +955,24 @@ func transfer(r *require.Assertions, handler *contractStakingEventHandler, owner }) r.NoError(err) } + +func changeDelegate(r *require.Assertions, handler *contractStakingEventHandler, delegate address.Address, token int64) { + err := handler.handleDelegateChangedEvent(eventParam{ + "newDelegate": common.BytesToAddress(delegate.Bytes()), + "tokenId": big.NewInt(token), + }) + r.NoError(err) +} + +func mergeBuckets(r *require.Assertions, handler *contractStakingEventHandler, tokenIds []int64, amount, duration int64) { + tokens := make([]*big.Int, len(tokenIds)) + for i, token := range tokenIds { + tokens[i] = big.NewInt(token) + } + err := handler.handleMergedEvent(eventParam{ + "amount": big.NewInt(amount), + "duration": big.NewInt(duration), + "tokenIds": tokens, + }) + r.NoError(err) +}