From 4529cd4006e612705c480683add6b7202cd16bd7 Mon Sep 17 00:00:00 2001 From: envestcc Date: Wed, 14 Jun 2023 18:44:56 +0800 Subject: [PATCH 01/17] fix not remove candidate map when change delegate --- blockindex/contractstaking/cache.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index 6c328a432e..9d840e78ca 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -372,11 +372,15 @@ 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) } s.candidateBucketMap[bi.Delegate.String()][id] = true + if oldBi != nil && oldBi.Delegate.String() != bi.Delegate.String() { + delete(s.candidateBucketMap[oldBi.Delegate.String()], id) + } } func (s *contractStakingCache) deleteBucketInfo(id uint64) { From 1702378ec88a08c5bb0ef4c70ea4f4c30a11a9f6 Mon Sep 17 00:00:00 2001 From: envestcc Date: Thu, 15 Jun 2023 14:06:58 +0800 Subject: [PATCH 02/17] return clone data in read-only method of cache --- .../staking/contractstake_bucket_type.go | 9 + blockindex/contractstaking/bucket_info.go | 12 ++ blockindex/contractstaking/cache.go | 20 +- blockindex/contractstaking/indexer_test.go | 173 ++++++++++++++++++ 4 files changed, 207 insertions(+), 7 deletions(-) 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..eaedd73a2c 100644 --- a/blockindex/contractstaking/bucket_info.go +++ b/blockindex/contractstaking/bucket_info.go @@ -39,6 +39,18 @@ func (bi *bucketInfo) Deserialize(b []byte) error { return bi.loadProto(&m) } +// Clone clones the bucket info +func (bi *bucketInfo) Clone() *bucketInfo { + return &bucketInfo{ + TypeIndex: bi.TypeIndex, + CreatedAt: bi.CreatedAt, + UnlockedAt: bi.UnlockedAt, + UnstakedAt: bi.UnstakedAt, + Delegate: bi.Delegate, + Owner: bi.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 9d840e78ca..da7e5e3957 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -163,7 +163,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 @@ -299,7 +299,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 { @@ -307,12 +310,15 @@ func (s *contractStakingCache) mustGetBucketType(id uint64) *BucketType { if !ok { panic("bucket type not found") } - return bt + return bt.Clone() } 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 { @@ -320,7 +326,7 @@ func (s *contractStakingCache) mustGetBucketInfo(id uint64) *bucketInfo { if !ok { panic("bucket info not found") } - return bt + return bt.Clone() } func (s *contractStakingCache) mustGetBucket(id uint64) *Bucket { @@ -341,7 +347,7 @@ func (s *contractStakingCache) getBucket(id uint64) (*Bucket, bool) { func (s *contractStakingCache) getAllBucketInfo() map[uint64]*bucketInfo { m := make(map[uint64]*bucketInfo) for k, v := range s.bucketInfoMap { - m[k] = v + m[k] = v.Clone() } return m } @@ -350,7 +356,7 @@ func (s *contractStakingCache) getBucketInfoByCandidate(candidate address.Addres m := make(map[uint64]*bucketInfo) for k, v := range s.candidateBucketMap[candidate.String()] { if v { - m[k] = s.bucketInfoMap[k] + m[k] = s.bucketInfoMap[k].Clone() } } return m diff --git a/blockindex/contractstaking/indexer_test.go b/blockindex/contractstaking/indexer_test.go index d5d2e03838..93741cfa77 100644 --- a/blockindex/contractstaking/indexer_test.go +++ b/blockindex/contractstaking/indexer_test.go @@ -537,6 +537,158 @@ 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, height) + 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)) + r.Len(indexer.cache.ActiveBucketTypes(), 2) + r.Len(indexer.cache.Buckets(), 4) + + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + 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)) + 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, height) + 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)) + 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, height) + changeDelegate(r, handler, delegate1, 3) + r.NoError(indexer.commit(handler)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // unlock bucket 1 & 4 + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + unlock(r, handler, 1, height) + unlock(r, handler, 4, height) + r.NoError(indexer.commit(handler)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // unstake bucket 1 & lock 4 + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + unstake(r, handler, 1, height) + lock(r, handler, 4, 20) + r.NoError(indexer.commit(handler)) + r.EqualValues(40, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // expand bucket 2 + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + expandBucketType(r, handler, 2, 30, 20) + r.NoError(indexer.commit(handler)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // transfer bucket 4 + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + transfer(r, handler, delegate2, 4) + r.NoError(indexer.commit(handler)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) + + // create bucket 5, 6, 7 + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + 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)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(80, indexer.CandidateVotes(delegate2).Uint64()) + + // merge bucket 5, 6, 7 + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + mergeBuckets(r, handler, []int64{5, 6, 7}, 60, 20) + r.NoError(indexer.commit(handler)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(80, indexer.CandidateVotes(delegate2).Uint64()) + + // unlock & unstake 5 + height++ + handler = newContractStakingEventHandler(indexer.cache, height) + unlock(r, handler, 5, height) + unstake(r, handler, 5, height) + r.NoError(indexer.commit(handler)) + r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) + r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) +} + func BenchmarkIndexer_PutBlockBeforeContractHeight(b *testing.B) { // Create a new Indexer with a contract height of 100 indexer := &Indexer{contractDeployHeight: 100} @@ -630,3 +782,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) +} From ed174f4bf4ab0996871927d8c302a0fbcb79d835 Mon Sep 17 00:00:00 2001 From: envestcc Date: Thu, 15 Jun 2023 10:38:21 +0800 Subject: [PATCH 03/17] add tests --- blockindex/contractstaking/indexer_test.go | 112 +++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/blockindex/contractstaking/indexer_test.go b/blockindex/contractstaking/indexer_test.go index 93741cfa77..ef1d9896ee 100644 --- a/blockindex/contractstaking/indexer_test.go +++ b/blockindex/contractstaking/indexer_test.go @@ -10,12 +10,14 @@ 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/db" "github.com/iotexproject/iotex-core/test/identityset" @@ -687,6 +689,116 @@ func TestContractStakingIndexerVotes(t *testing.T) { r.NoError(indexer.commit(handler)) 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 BenchmarkIndexer_PutBlockBeforeContractHeight(b *testing.B) { From b72f7bf6a414f80846374fe5c727da8bc28facfe Mon Sep 17 00:00:00 2001 From: envestcc Date: Thu, 15 Jun 2023 22:07:21 +0800 Subject: [PATCH 04/17] add unit test --- blockindex/contractstaking/cache_test.go | 496 +++++++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 blockindex/contractstaking/cache_test.go diff --git a/blockindex/contractstaking/cache_test.go b/blockindex/contractstaking/cache_test.go new file mode 100644 index 0000000000..b626e06421 --- /dev/null +++ b/blockindex/contractstaking/cache_test.go @@ -0,0 +1,496 @@ +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_Height(t *testing.T) { + require := require.New(t) + cache := newContractStakingCache("") + + cache.PutHeight(12345) + require.Equal(uint64(12345), cache.Height()) + + cache.PutHeight(54321) + require.Equal(uint64(54321), cache.Height()) +} + +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.Height()) + 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(12345), cache.Height()) + 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(12345), cache.Height()) + 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) +} From 54e5c20a0b178b6fc8a35a8a57850262a54752e2 Mon Sep 17 00:00:00 2001 From: envestcc Date: Thu, 15 Jun 2023 22:16:11 +0800 Subject: [PATCH 05/17] fix totalbucketcount bug when loadfromdb --- blockindex/contractstaking/cache.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index da7e5e3957..8182f97ee1 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -233,7 +233,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) @@ -246,7 +245,7 @@ func (s *contractStakingCache) LoadFromDB(kvstore db.KVStore) error { height = byteutil.BytesToUint64BigEndian(h) } - delta.PutHeight(height) + s.putHeight(height) // load total bucket count var totalBucketCount uint64 @@ -258,7 +257,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 +269,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 +282,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) { From ee6172478058126da1cf46d2428cb0c6cd3fda16 Mon Sep 17 00:00:00 2001 From: envestcc Date: Fri, 16 Jun 2023 00:11:12 +0800 Subject: [PATCH 06/17] add unit test --- .../contractstaking/delta_cache_test.go | 297 ++++++++++++++++++ .../contractstaking/dirty_cache_test.go | 277 ++++++++++++++++ 2 files changed, 574 insertions(+) create mode 100644 blockindex/contractstaking/delta_cache_test.go create mode 100644 blockindex/contractstaking/dirty_cache_test.go diff --git a/blockindex/contractstaking/delta_cache_test.go b/blockindex/contractstaking/delta_cache_test.go new file mode 100644 index 0000000000..0f280caafb --- /dev/null +++ b/blockindex/contractstaking/delta_cache_test.go @@ -0,0 +1,297 @@ +package contractstaking + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/test/identityset" +) + +func TestContractStakingDelta_GetHeight(t *testing.T) { + require := require.New(t) + + // create a new delta cache + cache := newContractStakingDelta() + + // test with height 0 + height := cache.GetHeight() + require.EqualValues(0, height) + + // test with height 12345 + cache.PutHeight(12345) + height = cache.GetHeight() + require.EqualValues(12345, height) +} + +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)} + 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)} + cache.UpdateBucketInfo(2, bi) + + // remove bucket info + cache.DeleteBucketInfo(3) + + // add and remove bucket info + bi = &bucketInfo{TypeIndex: 4, CreatedAt: 3, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(5), Owner: identityset.Address(6)} + cache.AddBucketInfo(4, bi) + cache.DeleteBucketInfo(4) + + // 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 + cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1}) + + // modify bucket type 1 & 3 + cache.UpdateBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 3}) + 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 + cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + 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)} + 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) + + // revert bucket info 1 + cache.DeleteBucketInfo(1) + // get bucket info + info, state = cache.GetBucketInfo(1) + require.Nil(info) + require.EqualValues(deltaStateReverted, state) + + // modify bucket info 2 + bi = &bucketInfo{TypeIndex: 2, CreatedAt: 2, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)} + 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 + 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} + 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} + 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 + cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + 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)} + 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)} + cache.AddBucketInfo(2, bi) + + // test with added bucket info + addedBucketCnt = cache.AddedBucketCnt() + require.EqualValues(2, addedBucketCnt) + + // remove bucket info + cache.DeleteBucketInfo(2) + + // test with removed bucket info + addedBucketCnt = cache.AddedBucketCnt() + require.EqualValues(1, 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 + cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1}) + 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/dirty_cache_test.go b/blockindex/contractstaking/dirty_cache_test.go new file mode 100644 index 0000000000..f091181851 --- /dev/null +++ b/blockindex/contractstaking/dirty_cache_test.go @@ -0,0 +1,277 @@ +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 + dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) + 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 + 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 + 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 + 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 + 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} + 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)} + 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 + 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 + 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 + 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 + 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) + + // remove bucket info from dirty cache + dirty.deleteBucketInfo(1) + // 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 + 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 + 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()) + + // put height in dirty cache + dirty.putHeight(1) + // check that clean cache is not affected + require.EqualValues(0, clean.Height()) +} From c7500e24ada667106ce97cc5c8fa5ac28f8b37d4 Mon Sep 17 00:00:00 2001 From: envestcc Date: Fri, 16 Jun 2023 11:33:54 +0800 Subject: [PATCH 07/17] refactor: move Height and TotalBucketType out of delta --- blockindex/contractstaking/cache.go | 2 -- blockindex/contractstaking/delta_cache.go | 12 ------------ blockindex/contractstaking/delta_cache_test.go | 16 ---------------- blockindex/contractstaking/dirty_cache.go | 1 - blockindex/contractstaking/event_handler.go | 7 +++++-- blockindex/contractstaking/indexer.go | 4 +++- 6 files changed, 8 insertions(+), 34 deletions(-) diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index 8182f97ee1..67e4f0010a 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -427,7 +427,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/delta_cache.go b/blockindex/contractstaking/delta_cache.go index bd28d438d0..b949961caa 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), diff --git a/blockindex/contractstaking/delta_cache_test.go b/blockindex/contractstaking/delta_cache_test.go index 0f280caafb..5185e73475 100644 --- a/blockindex/contractstaking/delta_cache_test.go +++ b/blockindex/contractstaking/delta_cache_test.go @@ -9,22 +9,6 @@ import ( "github.com/iotexproject/iotex-core/test/identityset" ) -func TestContractStakingDelta_GetHeight(t *testing.T) { - require := require.New(t) - - // create a new delta cache - cache := newContractStakingDelta() - - // test with height 0 - height := cache.GetHeight() - require.EqualValues(0, height) - - // test with height 12345 - cache.PutHeight(12345) - height = cache.GetHeight() - require.EqualValues(12345, height) -} - func TestContractStakingDelta_BucketInfoDelta(t *testing.T) { require := require.New(t) diff --git a/blockindex/contractstaking/dirty_cache.go b/blockindex/contractstaking/dirty_cache.go index 22b8bc02d8..20903d3612 100644 --- a/blockindex/contractstaking/dirty_cache.go +++ b/blockindex/contractstaking/dirty_cache.go @@ -53,7 +53,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 { diff --git a/blockindex/contractstaking/event_handler.go b/blockindex/contractstaking/event_handler.go index 5d4fe76493..4a75a84438 100644 --- a/blockindex/contractstaking/event_handler.go +++ b/blockindex/contractstaking/event_handler.go @@ -352,6 +352,7 @@ const ( type contractStakingEventHandler struct { dirty *contractStakingDirty tokenOwner map[uint64]address.Address + height uint64 } var ( @@ -372,6 +373,7 @@ func newContractStakingEventHandler(cache *contractStakingCache, height uint64) return &contractStakingEventHandler{ dirty: dirty, tokenOwner: make(map[uint64]address.Address), + height: height, } } @@ -420,8 +422,9 @@ func (eh *contractStakingEventHandler) HandleEvent(ctx context.Context, blk *blo } } -func (eh *contractStakingEventHandler) Result() (batch.KVStoreBatch, *contractStakingDelta) { - return eh.dirty.finalize() +func (eh *contractStakingEventHandler) Result() (batch.KVStoreBatch, *contractStakingDelta, uint64) { + batch, delta := eh.dirty.finalize() + return batch, delta, eh.height } func (eh *contractStakingEventHandler) handleTransferEvent(event eventParam) error { diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index f4e063d188..258b6ef738 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -171,11 +171,13 @@ func (s *Indexer) DeleteTipBlock(context.Context, *block.Block) error { } func (s *Indexer) commit(handler *contractStakingEventHandler) error { - batch, delta := handler.Result() + batch, delta, height := handler.Result() if err := s.cache.Merge(delta); err != nil { s.reloadCache() return err } + s.cache.PutHeight(height) + s.cache.PutTotalBucketCount(s.cache.TotalBucketCount() + delta.AddedBucketCnt()) if err := s.kvstore.WriteBatch(batch); err != nil { s.reloadCache() return err From 640581c5ecac8f58ad0c87fe192322fde45f1498 Mon Sep 17 00:00:00 2001 From: envestcc Date: Fri, 16 Jun 2023 11:54:42 +0800 Subject: [PATCH 08/17] refactor: use deltaStateUnchanged instead of deltaStateReverted --- blockindex/contractstaking/delta_cache.go | 92 +++++++------------ .../contractstaking/delta_cache_test.go | 2 +- blockindex/contractstaking/delta_state.go | 14 ++- 3 files changed, 40 insertions(+), 68 deletions(-) diff --git a/blockindex/contractstaking/delta_cache.go b/blockindex/contractstaking/delta_cache.go index b949961caa..eb4a7aca68 100644 --- a/blockindex/contractstaking/delta_cache.go +++ b/blockindex/contractstaking/delta_cache.go @@ -68,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 { @@ -96,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 @@ -172,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 index 5185e73475..b9f2294da9 100644 --- a/blockindex/contractstaking/delta_cache_test.go +++ b/blockindex/contractstaking/delta_cache_test.go @@ -172,7 +172,7 @@ func TestContractStakingDelta_GetBucketInfo(t *testing.T) { // get bucket info info, state = cache.GetBucketInfo(1) require.Nil(info) - require.EqualValues(deltaStateReverted, state) + require.EqualValues(deltaStateUnchanged, state) // modify bucket info 2 bi = &bucketInfo{TypeIndex: 2, CreatedAt: 2, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)} diff --git a/blockindex/contractstaking/delta_state.go b/blockindex/contractstaking/delta_state.go index 053a6faf19..832ab3d423 100644 --- a/blockindex/contractstaking/delta_state.go +++ b/blockindex/contractstaking/delta_state.go @@ -8,10 +8,10 @@ package contractstaking import "github.com/pkg/errors" const ( - deltaStateAdded deltaState = iota + deltaStateUnchanged deltaState = iota + deltaStateAdded deltaStateRemoved deltaStateModified - deltaStateReverted ) type deltaState int @@ -19,18 +19,16 @@ type deltaState int var ( deltaStateTransferMap = map[deltaState]map[deltaAction]deltaState{ deltaStateAdded: { - deltaActionRemove: deltaStateReverted, deltaActionModify: deltaStateAdded, }, - deltaStateRemoved: { - deltaActionAdd: deltaStateModified, - }, deltaStateModified: { deltaActionModify: deltaStateModified, deltaActionRemove: deltaStateRemoved, }, - deltaStateReverted: { - deltaActionAdd: deltaStateAdded, + deltaStateUnchanged: { + deltaActionAdd: deltaStateAdded, + deltaActionRemove: deltaStateRemoved, + deltaActionModify: deltaStateModified, }, } ) From 54f26dbd66e640083844fb559b6c74848309100d Mon Sep 17 00:00:00 2001 From: envestcc Date: Fri, 16 Jun 2023 12:13:32 +0800 Subject: [PATCH 09/17] add unit test for delta_state --- .../contractstaking/delta_cache_test.go | 58 ++++++++----------- .../contractstaking/delta_state_test.go | 46 +++++++++++++++ .../contractstaking/dirty_cache_test.go | 35 +++++------ 3 files changed, 83 insertions(+), 56 deletions(-) create mode 100644 blockindex/contractstaking/delta_state_test.go diff --git a/blockindex/contractstaking/delta_cache_test.go b/blockindex/contractstaking/delta_cache_test.go index b9f2294da9..9242a95277 100644 --- a/blockindex/contractstaking/delta_cache_test.go +++ b/blockindex/contractstaking/delta_cache_test.go @@ -17,19 +17,14 @@ func TestContractStakingDelta_BucketInfoDelta(t *testing.T) { // add bucket info bi := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)} - cache.AddBucketInfo(1, bi) + 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)} - cache.UpdateBucketInfo(2, bi) + require.NoError(cache.UpdateBucketInfo(2, bi)) // remove bucket info - cache.DeleteBucketInfo(3) - - // add and remove bucket info - bi = &bucketInfo{TypeIndex: 4, CreatedAt: 3, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(5), Owner: identityset.Address(6)} - cache.AddBucketInfo(4, bi) - cache.DeleteBucketInfo(4) + require.NoError(cache.DeleteBucketInfo(3)) // get bucket info delta delta := cache.BucketInfoDelta() @@ -73,12 +68,12 @@ func TestContractStakingDelta_BucketTypeDelta(t *testing.T) { cache := newContractStakingDelta() // add bucket type - cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) - cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1}) + 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 - cache.UpdateBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 3}) - cache.UpdateBucketType(3, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 4}) + 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 @@ -112,8 +107,8 @@ func TestContractStakingDelta_MatchBucketType(t *testing.T) { require.Nil(bucketType) // add bucket types - cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) - cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1}) + 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) @@ -154,7 +149,7 @@ func TestContractStakingDelta_GetBucketInfo(t *testing.T) { // add bucket info bi := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)} - cache.AddBucketInfo(1, bi) + require.NoError(cache.AddBucketInfo(1, bi)) // get added bucket info info, state := cache.GetBucketInfo(1) @@ -167,16 +162,9 @@ func TestContractStakingDelta_GetBucketInfo(t *testing.T) { require.EqualValues(identityset.Address(2), info.Owner) require.EqualValues(deltaStateAdded, state) - // revert bucket info 1 - cache.DeleteBucketInfo(1) - // get bucket info - info, state = cache.GetBucketInfo(1) - require.Nil(info) - require.EqualValues(deltaStateUnchanged, state) - // modify bucket info 2 bi = &bucketInfo{TypeIndex: 2, CreatedAt: 2, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)} - cache.UpdateBucketInfo(2, bi) + require.NoError(cache.UpdateBucketInfo(2, bi)) // get modified bucket info info, state = cache.GetBucketInfo(2) require.NotNil(info) @@ -189,7 +177,7 @@ func TestContractStakingDelta_GetBucketInfo(t *testing.T) { require.EqualValues(deltaStateModified, state) // remove bucket info 2 - cache.DeleteBucketInfo(2) + require.NoError(cache.DeleteBucketInfo(2)) // get removed bucket info info, state = cache.GetBucketInfo(2) require.Nil(info) @@ -204,7 +192,7 @@ func TestContractStakingDelta_GetBucketType(t *testing.T) { // add bucket type bt := &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1} - cache.AddBucketType(1, bt) + require.NoError(cache.AddBucketType(1, bt)) // get added bucket type bucketType, state := cache.GetBucketType(1) @@ -216,7 +204,7 @@ func TestContractStakingDelta_GetBucketType(t *testing.T) { // modify bucket type bt = &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2} - cache.UpdateBucketType(2, bt) + require.NoError(cache.UpdateBucketType(2, bt)) // get modified bucket type bucketType, state = cache.GetBucketType(2) require.NotNil(bucketType) @@ -238,26 +226,26 @@ func TestContractStakingDelta_AddedBucketCnt(t *testing.T) { require.EqualValues(0, addedBucketCnt) // add bucket types - cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) - cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1}) + 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)} - cache.AddBucketInfo(1, bi) + 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)} - cache.AddBucketInfo(2, bi) + require.NoError(cache.AddBucketInfo(2, bi)) // test with added bucket info addedBucketCnt = cache.AddedBucketCnt() require.EqualValues(2, addedBucketCnt) // remove bucket info - cache.DeleteBucketInfo(2) + require.NoError(cache.DeleteBucketInfo(3)) // test with removed bucket info addedBucketCnt = cache.AddedBucketCnt() - require.EqualValues(1, addedBucketCnt) + require.EqualValues(2, addedBucketCnt) } func TestContractStakingDelta_AddedBucketTypeCnt(t *testing.T) { @@ -271,9 +259,9 @@ func TestContractStakingDelta_AddedBucketTypeCnt(t *testing.T) { require.EqualValues(0, addedBucketTypeCnt) // add bucket types - cache.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) - cache.AddBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1}) - cache.AddBucketType(3, &BucketType{Amount: big.NewInt(300), Duration: 100, ActivatedAt: 1}) + 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() diff --git a/blockindex/contractstaking/delta_state_test.go b/blockindex/contractstaking/delta_state_test.go new file mode 100644 index 0000000000..97ef0f82d9 --- /dev/null +++ b/blockindex/contractstaking/delta_state_test.go @@ -0,0 +1,46 @@ +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 + isError bool + }{ + {"unchanged->add", deltaStateUnchanged, deltaActionAdd, deltaStateAdded, false}, + {"unchanged->remove", deltaStateUnchanged, deltaActionRemove, deltaStateRemoved, false}, + {"unchanged->modify", deltaStateUnchanged, deltaActionModify, deltaStateModified, false}, + {"added->add", deltaStateAdded, deltaActionAdd, deltaStateUnchanged, true}, + {"added->remove", deltaStateAdded, deltaActionRemove, deltaStateUnchanged, true}, + {"added->modify", deltaStateAdded, deltaActionModify, deltaStateAdded, false}, + {"removed->add", deltaStateRemoved, deltaActionAdd, deltaStateUnchanged, true}, + {"removed->remove", deltaStateRemoved, deltaActionRemove, deltaStateUnchanged, true}, + {"removed->modify", deltaStateRemoved, deltaActionModify, deltaStateUnchanged, true}, + {"modified->add", deltaStateModified, deltaActionAdd, deltaStateUnchanged, true}, + {"modified->remove", deltaStateModified, deltaActionRemove, deltaStateRemoved, false}, + {"modified->modify", deltaStateModified, deltaActionModify, deltaStateModified, false}, + {"invalid state", deltaState(100), deltaActionAdd, deltaState(100), true}, + {"invalid action", deltaStateUnchanged, deltaAction(100), deltaStateUnchanged, true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + s, err := c.state.Transfer(c.action) + if c.isError { + require.Error(err) + } else { + require.NoError(err) + require.Equal(c.expected, s) + } + }) + } +} diff --git a/blockindex/contractstaking/dirty_cache_test.go b/blockindex/contractstaking/dirty_cache_test.go index f091181851..8561e38caa 100644 --- a/blockindex/contractstaking/dirty_cache_test.go +++ b/blockindex/contractstaking/dirty_cache_test.go @@ -61,8 +61,8 @@ func TestContractStakingDirty_getBucketInfo(t *testing.T) { require.Equal(identityset.Address(2), bi.Owner) // added bucket info - dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) - dirty.addBucketInfo(2, &bucketInfo{TypeIndex: 2, CreatedAt: 2, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(2), Owner: identityset.Address(3)}) + 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) @@ -73,7 +73,7 @@ func TestContractStakingDirty_getBucketInfo(t *testing.T) { require.Equal(identityset.Address(3), bi.Owner) // modified bucket info - dirty.updateBucketInfo(1, &bucketInfo{TypeIndex: 2, CreatedAt: 3, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)}) + 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) @@ -84,7 +84,7 @@ func TestContractStakingDirty_getBucketInfo(t *testing.T) { require.Equal(identityset.Address(4), bi.Owner) // removed bucket info - dirty.deleteBucketInfo(1) + require.NoError(dirty.deleteBucketInfo(1)) bi, ok = dirty.getBucketInfo(1) require.False(ok) require.Nil(bi) @@ -111,7 +111,7 @@ func TestContractStakingDirty_matchBucketType(t *testing.T) { require.EqualValues(1, id) // added bucket type - dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) + 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()) @@ -135,7 +135,7 @@ func TestContractStakingDirty_getBucketTypeCount(t *testing.T) { require.EqualValues(1, count) // added bucket type - dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) + require.NoError(dirty.addBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2})) count = dirty.getBucketTypeCount() require.EqualValues(2, count) } @@ -163,7 +163,7 @@ func TestContractStakingDirty_finalize(t *testing.T) { // added bucket type bt := &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1} - dirty.addBucketType(1, bt) + require.NoError(dirty.addBucketType(1, bt)) batcher, delta = dirty.finalize() require.EqualValues(2, batcher.Size()) info, err = batcher.Entry(1) @@ -181,7 +181,7 @@ func TestContractStakingDirty_finalize(t *testing.T) { // add bucket info bi := &bucketInfo{TypeIndex: 1, CreatedAt: 2, UnlockedAt: 3, UnstakedAt: 4, Delegate: identityset.Address(1), Owner: identityset.Address(2)} - dirty.addBucketInfo(1, bi) + require.NoError(dirty.addBucketInfo(1, bi)) batcher, delta = dirty.finalize() require.EqualValues(3, batcher.Size()) info, err = batcher.Entry(2) @@ -208,35 +208,28 @@ func TestContractStakingDirty_noSideEffectOnClean(t *testing.T) { dirty := newContractStakingDirty(clean) // add bucket type to dirty cache - dirty.addBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}) + 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 - dirty.addBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}) + 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 - dirty.updateBucketType(1, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2}) + 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 - 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) - - // remove bucket info from dirty cache - dirty.deleteBucketInfo(1) + 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) @@ -245,7 +238,7 @@ func TestContractStakingDirty_noSideEffectOnClean(t *testing.T) { // 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 - dirty.updateBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 3, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)}) + 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) @@ -259,7 +252,7 @@ func TestContractStakingDirty_noSideEffectOnClean(t *testing.T) { // 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 - dirty.deleteBucketInfo(3) + require.NoError(dirty.deleteBucketInfo(3)) // check that clean cache is not affected bi, ok = clean.getBucketInfo(3) require.True(ok) From b98ff2a7a408986f1ac95adccaac82f2378038ca Mon Sep 17 00:00:00 2001 From: envestcc Date: Fri, 16 Jun 2023 17:03:03 +0800 Subject: [PATCH 10/17] address comments --- blockindex/contractstaking/bucket_info.go | 4 ++-- blockindex/contractstaking/cache.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/blockindex/contractstaking/bucket_info.go b/blockindex/contractstaking/bucket_info.go index eaedd73a2c..0596955b6e 100644 --- a/blockindex/contractstaking/bucket_info.go +++ b/blockindex/contractstaking/bucket_info.go @@ -39,8 +39,8 @@ func (bi *bucketInfo) Deserialize(b []byte) error { return bi.loadProto(&m) } -// Clone clones the bucket info -func (bi *bucketInfo) Clone() *bucketInfo { +// clone clones the bucket info +func (bi *bucketInfo) clone() *bucketInfo { return &bucketInfo{ TypeIndex: bi.TypeIndex, CreatedAt: bi.CreatedAt, diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index 67e4f0010a..8ad51055d4 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -309,7 +309,7 @@ func (s *contractStakingCache) mustGetBucketType(id uint64) *BucketType { if !ok { panic("bucket type not found") } - return bt.Clone() + return bt } func (s *contractStakingCache) getBucketInfo(id uint64) (*bucketInfo, bool) { @@ -317,7 +317,7 @@ func (s *contractStakingCache) getBucketInfo(id uint64) (*bucketInfo, bool) { if !ok { return nil, false } - return bi.Clone(), ok + return bi.clone(), ok } func (s *contractStakingCache) mustGetBucketInfo(id uint64) *bucketInfo { @@ -325,7 +325,7 @@ func (s *contractStakingCache) mustGetBucketInfo(id uint64) *bucketInfo { if !ok { panic("bucket info not found") } - return bt.Clone() + return bt } func (s *contractStakingCache) mustGetBucket(id uint64) *Bucket { @@ -346,7 +346,7 @@ func (s *contractStakingCache) getBucket(id uint64) (*Bucket, bool) { func (s *contractStakingCache) getAllBucketInfo() map[uint64]*bucketInfo { m := make(map[uint64]*bucketInfo) for k, v := range s.bucketInfoMap { - m[k] = v.Clone() + m[k] = v.clone() } return m } @@ -355,7 +355,7 @@ func (s *contractStakingCache) getBucketInfoByCandidate(candidate address.Addres m := make(map[uint64]*bucketInfo) for k, v := range s.candidateBucketMap[candidate.String()] { if v { - m[k] = s.bucketInfoMap[k].Clone() + m[k] = s.bucketInfoMap[k].clone() } } return m From da5edb2b9d293515cb095e2053b9db7de65d8b0f Mon Sep 17 00:00:00 2001 From: envestcc Date: Sat, 17 Jun 2023 10:11:24 +0800 Subject: [PATCH 11/17] address comments --- blockindex/contractstaking/bucket_info.go | 12 +++++- blockindex/contractstaking/cache.go | 16 +++++--- blockindex/contractstaking/delta_state.go | 12 +++--- .../contractstaking/delta_state_test.go | 40 +++++++++++-------- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/blockindex/contractstaking/bucket_info.go b/blockindex/contractstaking/bucket_info.go index 0596955b6e..ee22717bfa 100644 --- a/blockindex/contractstaking/bucket_info.go +++ b/blockindex/contractstaking/bucket_info.go @@ -41,13 +41,21 @@ func (bi *bucketInfo) Deserialize(b []byte) error { // 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: bi.Delegate, - Owner: bi.Owner, + Delegate: delegate, + Owner: owner, } } diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index 8ad51055d4..43fa94237f 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -379,12 +379,18 @@ 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[bi.Delegate.String()][id] = true - if oldBi != nil && oldBi.Delegate.String() != bi.Delegate.String() { - delete(s.candidateBucketMap[oldBi.Delegate.String()], id) + 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) + } } } diff --git a/blockindex/contractstaking/delta_state.go b/blockindex/contractstaking/delta_state.go index 832ab3d423..4375f8d248 100644 --- a/blockindex/contractstaking/delta_state.go +++ b/blockindex/contractstaking/delta_state.go @@ -8,6 +8,8 @@ package contractstaking import "github.com/pkg/errors" const ( + // deltaState constants + // deltaStateUnchanged is the zero-value of the type deltaState deltaStateUnchanged deltaState = iota deltaStateAdded deltaStateRemoved @@ -18,6 +20,11 @@ type deltaState int var ( deltaStateTransferMap = map[deltaState]map[deltaAction]deltaState{ + deltaStateUnchanged: { + deltaActionAdd: deltaStateAdded, + deltaActionRemove: deltaStateRemoved, + deltaActionModify: deltaStateModified, + }, deltaStateAdded: { deltaActionModify: deltaStateAdded, }, @@ -25,11 +32,6 @@ var ( deltaActionModify: deltaStateModified, deltaActionRemove: deltaStateRemoved, }, - deltaStateUnchanged: { - deltaActionAdd: deltaStateAdded, - deltaActionRemove: deltaStateRemoved, - deltaActionModify: deltaStateModified, - }, } ) diff --git a/blockindex/contractstaking/delta_state_test.go b/blockindex/contractstaking/delta_state_test.go index 97ef0f82d9..73c5895c8e 100644 --- a/blockindex/contractstaking/delta_state_test.go +++ b/blockindex/contractstaking/delta_state_test.go @@ -14,29 +14,30 @@ func TestDeltaState_Transfer(t *testing.T) { state deltaState action deltaAction expected deltaState - isError bool + err string }{ - {"unchanged->add", deltaStateUnchanged, deltaActionAdd, deltaStateAdded, false}, - {"unchanged->remove", deltaStateUnchanged, deltaActionRemove, deltaStateRemoved, false}, - {"unchanged->modify", deltaStateUnchanged, deltaActionModify, deltaStateModified, false}, - {"added->add", deltaStateAdded, deltaActionAdd, deltaStateUnchanged, true}, - {"added->remove", deltaStateAdded, deltaActionRemove, deltaStateUnchanged, true}, - {"added->modify", deltaStateAdded, deltaActionModify, deltaStateAdded, false}, - {"removed->add", deltaStateRemoved, deltaActionAdd, deltaStateUnchanged, true}, - {"removed->remove", deltaStateRemoved, deltaActionRemove, deltaStateUnchanged, true}, - {"removed->modify", deltaStateRemoved, deltaActionModify, deltaStateUnchanged, true}, - {"modified->add", deltaStateModified, deltaActionAdd, deltaStateUnchanged, true}, - {"modified->remove", deltaStateModified, deltaActionRemove, deltaStateRemoved, false}, - {"modified->modify", deltaStateModified, deltaActionModify, deltaStateModified, false}, - {"invalid state", deltaState(100), deltaActionAdd, deltaState(100), true}, - {"invalid action", deltaStateUnchanged, deltaAction(100), deltaStateUnchanged, true}, + {"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 c.isError { + if len(c.err) > 0 { require.Error(err) + require.Contains(err.Error(), c.err) } else { require.NoError(err) require.Equal(c.expected, s) @@ -44,3 +45,10 @@ func TestDeltaState_Transfer(t *testing.T) { }) } } + +func TestDeltaState_ZeroValue(t *testing.T) { + require := require.New(t) + + var state deltaState + require.Equal(deltaStateUnchanged, state) +} From 2cae048c33c42115e34f464c6d2b813b724944ee Mon Sep 17 00:00:00 2001 From: envestcc Date: Sun, 18 Jun 2023 15:48:53 +0800 Subject: [PATCH 12/17] address comments --- blockindex/contractstaking/cache.go | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index 43fa94237f..f46b725626 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -79,9 +79,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 +126,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) @@ -343,24 +343,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.clone() - } - 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].clone() - } - } - return m -} - func (s *contractStakingCache) getTotalBucketCount() uint64 { return s.totalBucketCount } From 64c7f8e4b569abcc46a8af29c272e2e3cff8f31a Mon Sep 17 00:00:00 2001 From: envestcc Date: Mon, 19 Jun 2023 10:19:00 +0800 Subject: [PATCH 13/17] move height from cache to indexer --- blockindex/contractstaking/cache.go | 38 +----- blockindex/contractstaking/cache_test.go | 14 --- blockindex/contractstaking/dirty_cache.go | 4 - .../contractstaking/dirty_cache_test.go | 4 - blockindex/contractstaking/event_handler.go | 9 +- blockindex/contractstaking/indexer.go | 41 ++++-- blockindex/contractstaking/indexer_test.go | 118 +++++++++--------- 7 files changed, 98 insertions(+), 130 deletions(-) diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index f46b725626..afe3166482 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() @@ -190,13 +183,6 @@ 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() @@ -233,20 +219,6 @@ func (s *contractStakingCache) LoadFromDB(kvstore db.KVStore) error { s.mutex.Lock() defer s.mutex.Unlock() - // 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) - - } - s.putHeight(height) - // load total bucket count var totalBucketCount uint64 tbc, err := kvstore.Get(_StakingNS, _stakingTotalBucketCountKey) @@ -388,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 } diff --git a/blockindex/contractstaking/cache_test.go b/blockindex/contractstaking/cache_test.go index b626e06421..f8f7b3c1ea 100644 --- a/blockindex/contractstaking/cache_test.go +++ b/blockindex/contractstaking/cache_test.go @@ -15,17 +15,6 @@ import ( "github.com/iotexproject/iotex-core/testutil" ) -func TestContractStakingCache_Height(t *testing.T) { - require := require.New(t) - cache := newContractStakingCache("") - - cache.PutHeight(12345) - require.Equal(uint64(12345), cache.Height()) - - cache.PutHeight(54321) - require.Equal(uint64(54321), cache.Height()) -} - func TestContractStakingCache_CandidateVotes(t *testing.T) { require := require.New(t) cache := newContractStakingCache("") @@ -437,7 +426,6 @@ func TestContractStakingCache_LoadFromDB(t *testing.T) { err = cache.LoadFromDB(kvstore) require.NoError(err) - require.Equal(uint64(0), cache.Height()) require.Equal(uint64(0), cache.TotalBucketCount()) require.Equal(0, len(cache.Buckets())) require.EqualValues(0, cache.BucketTypeCount()) @@ -447,7 +435,6 @@ func TestContractStakingCache_LoadFromDB(t *testing.T) { kvstore.Put(_StakingNS, _stakingTotalBucketCountKey, byteutil.Uint64ToBytesBigEndian(10)) err = cache.LoadFromDB(kvstore) require.NoError(err) - require.Equal(uint64(12345), cache.Height()) require.Equal(uint64(10), cache.TotalBucketCount()) require.Equal(0, len(cache.Buckets())) require.EqualValues(0, cache.BucketTypeCount()) @@ -459,7 +446,6 @@ func TestContractStakingCache_LoadFromDB(t *testing.T) { kvstore.Put(_StakingBucketTypeNS, byteutil.Uint64ToBytesBigEndian(1), bucketType.Serialize()) err = cache.LoadFromDB(kvstore) require.NoError(err) - require.Equal(uint64(12345), cache.Height()) require.Equal(uint64(10), cache.TotalBucketCount()) bi, ok := cache.BucketInfo(1) require.True(ok) diff --git a/blockindex/contractstaking/dirty_cache.go b/blockindex/contractstaking/dirty_cache.go index 20903d3612..222b097d67 100644 --- a/blockindex/contractstaking/dirty_cache.go +++ b/blockindex/contractstaking/dirty_cache.go @@ -51,10 +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") -} - 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 index 8561e38caa..0b501bb597 100644 --- a/blockindex/contractstaking/dirty_cache_test.go +++ b/blockindex/contractstaking/dirty_cache_test.go @@ -263,8 +263,4 @@ func TestContractStakingDirty_noSideEffectOnClean(t *testing.T) { require.EqualValues(identityset.Address(1).String(), bi.Delegate.String()) require.EqualValues(identityset.Address(2).String(), bi.Owner.String()) - // put height in dirty cache - dirty.putHeight(1) - // check that clean cache is not affected - require.EqualValues(0, clean.Height()) } diff --git a/blockindex/contractstaking/event_handler.go b/blockindex/contractstaking/event_handler.go index 4a75a84438..f1fcda54d4 100644 --- a/blockindex/contractstaking/event_handler.go +++ b/blockindex/contractstaking/event_handler.go @@ -352,7 +352,6 @@ const ( type contractStakingEventHandler struct { dirty *contractStakingDirty tokenOwner map[uint64]address.Address - height uint64 } var ( @@ -367,13 +366,11 @@ 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), - height: height, } } @@ -422,9 +419,9 @@ func (eh *contractStakingEventHandler) HandleEvent(ctx context.Context, blk *blo } } -func (eh *contractStakingEventHandler) Result() (batch.KVStoreBatch, *contractStakingDelta, uint64) { +func (eh *contractStakingEventHandler) Result() (batch.KVStoreBatch, *contractStakingDelta) { batch, delta := eh.dirty.finalize() - return batch, delta, eh.height + return batch, delta } func (eh *contractStakingEventHandler) handleTransferEvent(event eventParam) error { diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index 258b6ef738..32d2687d85 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(s.kvstore) } // 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 @@ -144,7 +147,7 @@ func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error { 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,14 +173,18 @@ func (s *Indexer) DeleteTipBlock(context.Context, *block.Block) error { return errors.New("not implemented") } -func (s *Indexer) commit(handler *contractStakingEventHandler) error { - batch, delta, height := handler.Result() +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 } - s.cache.PutHeight(height) s.cache.PutTotalBucketCount(s.cache.TotalBucketCount() + delta.AddedBucketCnt()) + // update indexer height cache + s.height.Store(height) + // 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 @@ -187,5 +194,23 @@ func (s *Indexer) commit(handler *contractStakingEventHandler) error { func (s *Indexer) reloadCache() error { s.cache = newContractStakingCache(s.contractAddress) - return s.cache.LoadFromDB(s.kvstore) + return s.loadFromDB(s.kvstore) +} + +func (s *Indexer) loadFromDB(kvstore db.KVStore) error { + // 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) + + } + s.height.Store(height) + // load cache + return s.cache.LoadFromDB(kvstore) } diff --git a/blockindex/contractstaking/indexer_test.go b/blockindex/contractstaking/indexer_test.go index ef1d9896ee..94e282b40d 100644 --- a/blockindex/contractstaking/indexer_test.go +++ b/blockindex/contractstaking/indexer_test.go @@ -68,12 +68,12 @@ 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) @@ -113,12 +113,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) @@ -165,14 +165,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) } }() @@ -211,11 +211,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) @@ -226,12 +226,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) @@ -242,12 +242,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) @@ -284,11 +284,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 @@ -296,10 +296,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) @@ -318,18 +318,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) @@ -347,9 +347,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) @@ -367,10 +367,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) @@ -388,9 +388,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()) @@ -417,26 +417,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()) @@ -464,11 +464,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 @@ -484,12 +484,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() @@ -553,7 +553,7 @@ func TestContractStakingIndexerCacheClean(t *testing.T) { // init bucket type height := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) activateBucketType(r, handler, 10, 10, height) activateBucketType(r, handler, 20, 20, height) // create bucket @@ -566,12 +566,12 @@ func TestContractStakingIndexerCacheClean(t *testing.T) { 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)) + r.NoError(indexer.commit(handler, height)) r.Len(indexer.cache.ActiveBucketTypes(), 2) r.Len(indexer.cache.Buckets(), 4) height++ - handler = newContractStakingEventHandler(indexer.cache, height) + handler = newContractStakingEventHandler(indexer.cache) changeDelegate(r, handler, delegate1, 3) transfer(r, handler, delegate1, 1) bt, ok := indexer.Bucket(3) @@ -580,7 +580,7 @@ func TestContractStakingIndexerCacheClean(t *testing.T) { bt, ok = indexer.Bucket(1) r.True(ok) r.Equal(owner.String(), bt.Owner.String()) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) bt, ok = indexer.Bucket(3) r.True(ok) r.Equal(delegate1.String(), bt.Candidate.String()) @@ -603,7 +603,7 @@ func TestContractStakingIndexerVotes(t *testing.T) { // init bucket type height := uint64(1) - handler := newContractStakingEventHandler(indexer.cache, height) + handler := newContractStakingEventHandler(indexer.cache) activateBucketType(r, handler, 10, 10, height) activateBucketType(r, handler, 20, 20, height) activateBucketType(r, handler, 30, 20, height) @@ -616,77 +616,77 @@ func TestContractStakingIndexerVotes(t *testing.T) { 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)) + 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, height) + handler = newContractStakingEventHandler(indexer.cache) changeDelegate(r, handler, delegate1, 3) - r.NoError(indexer.commit(handler)) + 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, height) + handler = newContractStakingEventHandler(indexer.cache) unlock(r, handler, 1, height) unlock(r, handler, 4, height) - r.NoError(indexer.commit(handler)) + 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, height) + handler = newContractStakingEventHandler(indexer.cache) unstake(r, handler, 1, height) lock(r, handler, 4, 20) - r.NoError(indexer.commit(handler)) + 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, height) + handler = newContractStakingEventHandler(indexer.cache) expandBucketType(r, handler, 2, 30, 20) - r.NoError(indexer.commit(handler)) + 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, height) + handler = newContractStakingEventHandler(indexer.cache) transfer(r, handler, delegate2, 4) - r.NoError(indexer.commit(handler)) + 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, 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)) + 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, height) + handler = newContractStakingEventHandler(indexer.cache) mergeBuckets(r, handler, []int64{5, 6, 7}, 60, 20) - r.NoError(indexer.commit(handler)) + 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, height) + handler = newContractStakingEventHandler(indexer.cache) unlock(r, handler, 5, height) unstake(r, handler, 5, height) - r.NoError(indexer.commit(handler)) + r.NoError(indexer.commit(handler, height)) r.EqualValues(50, indexer.CandidateVotes(delegate1).Uint64()) r.EqualValues(20, indexer.CandidateVotes(delegate2).Uint64()) From e1319463034db38aab91c72a47d0057f92181af7 Mon Sep 17 00:00:00 2001 From: envestcc Date: Mon, 19 Jun 2023 11:32:48 +0800 Subject: [PATCH 14/17] address comments --- blockindex/contractstaking/event_handler.go | 3 +- blockindex/contractstaking/indexer.go | 6 +- blockindex/contractstaking/indexer_test.go | 61 +++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/blockindex/contractstaking/event_handler.go b/blockindex/contractstaking/event_handler.go index f1fcda54d4..cf6c17c357 100644 --- a/blockindex/contractstaking/event_handler.go +++ b/blockindex/contractstaking/event_handler.go @@ -420,8 +420,7 @@ func (eh *contractStakingEventHandler) HandleEvent(ctx context.Context, blk *blo } func (eh *contractStakingEventHandler) Result() (batch.KVStoreBatch, *contractStakingDelta) { - batch, delta := eh.dirty.finalize() - return batch, delta + return eh.dirty.finalize() } func (eh *contractStakingEventHandler) handleTransferEvent(event eventParam) error { diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index 32d2687d85..c1b928f08c 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -143,7 +143,7 @@ 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 @@ -181,14 +181,14 @@ func (s *Indexer) commit(handler *contractStakingEventHandler, height uint64) er return err } s.cache.PutTotalBucketCount(s.cache.TotalBucketCount() + delta.AddedBucketCnt()) - // update indexer height cache - s.height.Store(height) // 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 } diff --git a/blockindex/contractstaking/indexer_test.go b/blockindex/contractstaking/indexer_test.go index 94e282b40d..f9e3d576a7 100644 --- a/blockindex/contractstaking/indexer_test.go +++ b/blockindex/contractstaking/indexer_test.go @@ -19,6 +19,7 @@ import ( "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" @@ -77,6 +78,12 @@ func TestContractStakingIndexerLoadCache(t *testing.T) { 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 @@ -801,6 +808,60 @@ func TestContractStakingIndexerVotes(t *testing.T) { }) } +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} From a504f59b614a6543cf43ab95218ed09511352b29 Mon Sep 17 00:00:00 2001 From: dustinxie Date: Sun, 18 Jun 2023 21:08:34 -0700 Subject: [PATCH 15/17] Update cache.go --- blockindex/contractstaking/cache.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index afe3166482..a11aa1219e 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -146,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 { @@ -315,10 +315,6 @@ func (s *contractStakingCache) getBucket(id uint64) (*Bucket, bool) { return assembleBucket(id, bi, bt, s.contractAddress), true } -func (s *contractStakingCache) getTotalBucketCount() uint64 { - return s.totalBucketCount -} - func (s *contractStakingCache) putBucketType(id uint64, bt *BucketType) { amount := bt.Amount.Int64() s.bucketTypeMap[id] = bt From a6e6d0341916817a9ad4d824e584022b86820316 Mon Sep 17 00:00:00 2001 From: dustinxie Date: Sun, 18 Jun 2023 21:35:59 -0700 Subject: [PATCH 16/17] Update cache.go --- blockindex/contractstaking/cache.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blockindex/contractstaking/cache.go b/blockindex/contractstaking/cache.go index a11aa1219e..c32ff685ef 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -187,7 +187,11 @@ 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) { From 325df6c5432fbd12411bcdc2541b1986c733cc98 Mon Sep 17 00:00:00 2001 From: dustinxie Date: Sun, 18 Jun 2023 21:38:58 -0700 Subject: [PATCH 17/17] Update indexer.go --- blockindex/contractstaking/indexer.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index c1b928f08c..09099800c1 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -79,7 +79,7 @@ func (s *Indexer) Start(ctx context.Context) error { if err := s.kvstore.Start(ctx); err != nil { return err } - return s.loadFromDB(s.kvstore) + return s.loadFromDB() } // Stop stops the indexer @@ -180,7 +180,6 @@ func (s *Indexer) commit(handler *contractStakingEventHandler, height uint64) er s.reloadCache() return err } - s.cache.PutTotalBucketCount(s.cache.TotalBucketCount() + delta.AddedBucketCnt()) // update db batch.Put(_StakingNS, _stakingHeightKey, byteutil.Uint64ToBytesBigEndian(height), "failed to put height") if err := s.kvstore.WriteBatch(batch); err != nil { @@ -194,13 +193,13 @@ func (s *Indexer) commit(handler *contractStakingEventHandler, height uint64) er func (s *Indexer) reloadCache() error { s.cache = newContractStakingCache(s.contractAddress) - return s.loadFromDB(s.kvstore) + return s.loadFromDB() } -func (s *Indexer) loadFromDB(kvstore db.KVStore) error { +func (s *Indexer) loadFromDB() error { // load height var height uint64 - h, err := kvstore.Get(_StakingNS, _stakingHeightKey) + h, err := s.kvstore.Get(_StakingNS, _stakingHeightKey) if err != nil { if !errors.Is(err, db.ErrNotExist) { return err @@ -212,5 +211,5 @@ func (s *Indexer) loadFromDB(kvstore db.KVStore) error { } s.height.Store(height) // load cache - return s.cache.LoadFromDB(kvstore) + return s.cache.LoadFromDB(s.kvstore) }