From ec6cd60a93f0d493acb6125900f31ba006cf8237 Mon Sep 17 00:00:00 2001 From: envestcc Date: Sat, 27 Sep 2025 22:26:07 +0800 Subject: [PATCH 01/12] replace stakeview with voteview --- .../protocol/staking/contractstake_indexer.go | 5 + .../staking/contractstake_indexer_mock.go | 62 +++++ action/protocol/staking/protocol.go | 73 ++++-- action/protocol/staking/viewdata.go | 54 ++++- blockindex/contractstaking/bucket.go | 26 +++ .../contractstaking/eventprocessor_builder.go | 23 ++ blockindex/contractstaking/indexer.go | 103 ++++----- blockindex/contractstaking/stakeview.go | 162 ------------- chainservice/builder.go | 10 +- .../stakingindex/candidate_votes.go | 218 ++++++++++++++++++ .../stakingindex/eventprocessor_builder.go | 30 +++ systemcontractindex/stakingindex/index.go | 138 +++++++---- systemcontractindex/stakingindex/stakeview.go | 126 ---------- .../stakingindex/stakingpb/staking.pb.go | 176 ++++++++++++-- .../stakingindex/stakingpb/staking.proto | 10 + .../stakingindex/vote_view_handler.go | 139 +++++++++++ systemcontractindex/stakingindex/voteview.go | 140 +++++++++++ 17 files changed, 1049 insertions(+), 446 deletions(-) create mode 100644 blockindex/contractstaking/eventprocessor_builder.go delete mode 100644 blockindex/contractstaking/stakeview.go create mode 100644 systemcontractindex/stakingindex/candidate_votes.go create mode 100644 systemcontractindex/stakingindex/eventprocessor_builder.go delete mode 100644 systemcontractindex/stakingindex/stakeview.go create mode 100644 systemcontractindex/stakingindex/vote_view_handler.go create mode 100644 systemcontractindex/stakingindex/voteview.go diff --git a/action/protocol/staking/contractstake_indexer.go b/action/protocol/staking/contractstake_indexer.go index f574ea80c8..7497f27a44 100644 --- a/action/protocol/staking/contractstake_indexer.go +++ b/action/protocol/staking/contractstake_indexer.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/iotexproject/iotex-address/address" + "github.com/iotexproject/iotex-core/v2/action" "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" @@ -64,6 +65,10 @@ type ( LoadStakeView(context.Context, protocol.StateReader) (ContractStakeView, error) // CreateEventProcessor creates a new event processor CreateEventProcessor(context.Context, EventHandler) EventProcessor + // ContractStakingBuckets returns all the contract staking buckets + ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) + + BucketReader } // ContractStakingIndexerWithBucketType defines the interface of contract staking reader with bucket type ContractStakingIndexerWithBucketType interface { diff --git a/action/protocol/staking/contractstake_indexer_mock.go b/action/protocol/staking/contractstake_indexer_mock.go index 7b6047db46..e1da4905a9 100644 --- a/action/protocol/staking/contractstake_indexer_mock.go +++ b/action/protocol/staking/contractstake_indexer_mock.go @@ -228,6 +228,22 @@ func (mr *MockContractStakingIndexerMockRecorder) ContractAddress() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractAddress", reflect.TypeOf((*MockContractStakingIndexer)(nil).ContractAddress)) } +// ContractStakingBuckets mocks base method. +func (m *MockContractStakingIndexer) ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ContractStakingBuckets") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(map[uint64]*contractstaking.Bucket) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ContractStakingBuckets indicates an expected call of ContractStakingBuckets. +func (mr *MockContractStakingIndexerMockRecorder) ContractStakingBuckets() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractStakingBuckets", reflect.TypeOf((*MockContractStakingIndexer)(nil).ContractStakingBuckets)) +} + // CreateEventProcessor mocks base method. func (m *MockContractStakingIndexer) CreateEventProcessor(arg0 context.Context, arg1 EventHandler) EventProcessor { m.ctrl.T.Helper() @@ -242,6 +258,21 @@ func (mr *MockContractStakingIndexerMockRecorder) CreateEventProcessor(arg0, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEventProcessor", reflect.TypeOf((*MockContractStakingIndexer)(nil).CreateEventProcessor), arg0, arg1) } +// DeductBucket mocks base method. +func (m *MockContractStakingIndexer) DeductBucket(arg0 address.Address, arg1 uint64) (*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeductBucket", arg0, arg1) + ret0, _ := ret[0].(*contractstaking.Bucket) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeductBucket indicates an expected call of DeductBucket. +func (mr *MockContractStakingIndexerMockRecorder) DeductBucket(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeductBucket", reflect.TypeOf((*MockContractStakingIndexer)(nil).DeductBucket), arg0, arg1) +} + // Height mocks base method. func (m *MockContractStakingIndexer) Height() (uint64, error) { m.ctrl.T.Helper() @@ -441,6 +472,22 @@ func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) ContractAddress( return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractAddress", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).ContractAddress)) } +// ContractStakingBuckets mocks base method. +func (m *MockContractStakingIndexerWithBucketType) ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ContractStakingBuckets") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(map[uint64]*contractstaking.Bucket) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ContractStakingBuckets indicates an expected call of ContractStakingBuckets. +func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) ContractStakingBuckets() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractStakingBuckets", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).ContractStakingBuckets)) +} + // CreateEventProcessor mocks base method. func (m *MockContractStakingIndexerWithBucketType) CreateEventProcessor(arg0 context.Context, arg1 EventHandler) EventProcessor { m.ctrl.T.Helper() @@ -455,6 +502,21 @@ func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) CreateEventProce return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEventProcessor", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).CreateEventProcessor), arg0, arg1) } +// DeductBucket mocks base method. +func (m *MockContractStakingIndexerWithBucketType) DeductBucket(arg0 address.Address, arg1 uint64) (*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeductBucket", arg0, arg1) + ret0, _ := ret[0].(*contractstaking.Bucket) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeductBucket indicates an expected call of DeductBucket. +func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) DeductBucket(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeductBucket", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).DeductBucket), arg0, arg1) +} + // Height mocks base method. func (m *MockContractStakingIndexerWithBucketType) Height() (uint64, error) { m.ctrl.T.Helper() diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index a192ce935d..cdea24d13d 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -479,6 +479,16 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager return err } vd := v.(*viewData) + indexers := []ContractStakingIndexer{} + if p.contractStakingIndexer != nil { + indexers = append(indexers, p.contractStakingIndexer) + } + if p.contractStakingIndexerV2 != nil { + indexers = append(indexers, p.contractStakingIndexerV2) + } + if p.contractStakingIndexerV3 != nil { + indexers = append(indexers, p.contractStakingIndexerV3) + } if blkCtx.BlockHeight == g.ToBeEnabledBlockHeight { handler, err := newNFTBucketEventHandler(sm, func(bucket *contractstaking.Bucket, height uint64) *big.Int { vb := p.convertToVoteBucket(bucket, height) @@ -487,14 +497,43 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager if err != nil { return err } - if err := vd.contractsStake.Migrate(handler); err != nil { + buckets := make([]map[uint64]*contractstaking.Bucket, 3) + for i, indexer := range indexers { + h, bs, err := indexer.ContractStakingBuckets() + if err != nil { + return err + } + if h != blkCtx.BlockHeight-1 { + return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) + } + buckets[i] = bs + } + if err := vd.contractsStake.Migrate(handler, buckets); err != nil { return errors.Wrap(err, "failed to flush buckets for contract staking") } } if featureCtx.StoreVoteOfNFTBucketIntoView { - if err := vd.contractsStake.CreatePreStates(ctx); err != nil { + brs := make([]BucketReader, len(indexers)) + for i, indexer := range indexers { + brs[i] = indexer + } + if err := vd.contractsStake.CreatePreStates(ctx, brs); err != nil { return err } + if blkCtx.BlockHeight == g.WakeBlockHeight { + buckets := make([]map[uint64]*contractstaking.Bucket, 3) + for i, indexer := range indexers { + h, bs, err := indexer.ContractStakingBuckets() + if err != nil { + return err + } + if h != blkCtx.BlockHeight-1 { + return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) + } + buckets[i] = bs + } + vd.contractsStake.Revise(buckets) + } } if p.candBucketsIndexer == nil { @@ -779,10 +818,6 @@ func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidiateStateCom // ActiveCandidates returns all active candidates in candidate center func (p *Protocol) ActiveCandidates(ctx context.Context, sr protocol.StateReader, height uint64) (state.CandidateList, error) { - srHeight, err := sr.Height() - if err != nil { - return nil, errors.Wrap(err, "failed to get StateReader height") - } c, err := ConstructBaseView(sr) if err != nil { return nil, errors.Wrap(err, "failed to get ActiveCandidates") @@ -792,20 +827,10 @@ func (p *Protocol) ActiveCandidates(ctx context.Context, sr protocol.StateReader for i := range list { if protocol.MustGetFeatureCtx(ctx).StoreVoteOfNFTBucketIntoView { var csVotes *big.Int - if protocol.MustGetFeatureCtx(ctx).CreatePostActionStates { - csVotes, err = p.contractStakingVotesFromView(ctx, list[i].GetIdentifier(), c.BaseView()) - if err != nil { - return nil, err - } - } else { - // specifying the height param instead of query latest from indexer directly, aims to cause error when indexer falls behind. - // the reason of using srHeight-1 is contract indexer is not updated before the block is committed. - csVotes, err = p.contractStakingVotesFromIndexer(ctx, list[i].GetIdentifier(), srHeight-1) - if err != nil { - return nil, err - } + csVotes, err = p.contractStakingVotesFromView(ctx, list[i].GetIdentifier(), c.BaseView()) + if err != nil { + return nil, err } - list[i].Votes.Add(list[i].Votes, csVotes) } active, err := p.isActiveCandidate(ctx, c, list[i]) @@ -1087,13 +1112,11 @@ func (p *Protocol) contractStakingVotesFromView(ctx context.Context, candidate a views = append(views, view.contractsStake.v3) } for _, cv := range views { - btks, err := cv.BucketsByCandidate(candidate) - if err != nil { - return nil, errors.Wrap(err, "failed to get BucketsByCandidate from contractStakingIndexer") - } - for _, b := range btks { - votes.Add(votes, p.contractBucketVotes(featureCtx, b)) + v := cv.CandidateStakeVotes(ctx, candidate) + if v == nil { + continue } + votes.Add(votes, v) } return votes, nil } diff --git a/action/protocol/staking/viewdata.go b/action/protocol/staking/viewdata.go index ff541fd6dd..901c017397 100644 --- a/action/protocol/staking/viewdata.go +++ b/action/protocol/staking/viewdata.go @@ -10,12 +10,19 @@ import ( "math/big" "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" + "github.com/iotexproject/iotex-core/v2/action" "github.com/iotexproject/iotex-core/v2/action/protocol" - "github.com/pkg/errors" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" ) type ( + // BucketReader defines the interface to read bucket info + BucketReader interface { + DeductBucket(address.Address, uint64) (*contractstaking.Bucket, error) + } + // ContractStakeView is the interface for contract stake view ContractStakeView interface { // Wrap wraps the contract stake view @@ -27,13 +34,15 @@ type ( // Commit commits the contract stake view Commit(context.Context, protocol.StateManager) error // CreatePreStates creates pre states for the contract stake view - CreatePreStates(ctx context.Context) error + CreatePreStates(ctx context.Context, br BucketReader) error // Handle handles the receipt for the contract stake view Handle(ctx context.Context, receipt *action.Receipt) error // Migrate writes the bucket types and buckets to the state manager - Migrate(EventHandler) error + Migrate(EventHandler, map[uint64]*contractstaking.Bucket) error + // Revise updates the contract stake view with the latest bucket data + Revise(map[uint64]*contractstaking.Bucket) // BucketsByCandidate returns the buckets by candidate address - BucketsByCandidate(ownerAddr address.Address) ([]*VoteBucket, error) + CandidateStakeVotes(ctx context.Context, id address.Address) *big.Int AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error } // viewData is the data that need to be stored in protocol's view @@ -126,19 +135,37 @@ func (v *viewData) Revert(snapshot int) error { return nil } -func (csv *contractStakeView) Migrate(nftHandler EventHandler) error { +func (csv *contractStakeView) Revise(buckets []map[uint64]*contractstaking.Bucket) { + idx := 0 + if csv.v1 != nil { + csv.v1.Revise(buckets[idx]) + idx++ + } + if csv.v2 != nil { + csv.v2.Revise(buckets[idx]) + idx++ + } + if csv.v3 != nil { + csv.v3.Revise(buckets[idx]) + } +} + +func (csv *contractStakeView) Migrate(nftHandler EventHandler, buckets []map[uint64]*contractstaking.Bucket) error { + idx := 0 if csv.v1 != nil { - if err := csv.v1.Migrate(nftHandler); err != nil { + if err := csv.v1.Migrate(nftHandler, buckets[idx]); err != nil { return err } + idx++ } if csv.v2 != nil { - if err := csv.v2.Migrate(nftHandler); err != nil { + if err := csv.v2.Migrate(nftHandler, buckets[idx]); err != nil { return err } + idx++ } if csv.v3 != nil { - if err := csv.v3.Migrate(nftHandler); err != nil { + if err := csv.v3.Migrate(nftHandler, buckets[idx]); err != nil { return err } } @@ -179,19 +206,22 @@ func (csv *contractStakeView) Fork() *contractStakeView { return clone } -func (csv *contractStakeView) CreatePreStates(ctx context.Context) error { +func (csv *contractStakeView) CreatePreStates(ctx context.Context, brs []BucketReader) error { + idx := 0 if csv.v1 != nil { - if err := csv.v1.CreatePreStates(ctx); err != nil { + if err := csv.v1.CreatePreStates(ctx, brs[idx]); err != nil { return err } + idx++ } if csv.v2 != nil { - if err := csv.v2.CreatePreStates(ctx); err != nil { + if err := csv.v2.CreatePreStates(ctx, brs[idx]); err != nil { return err } + idx++ } if csv.v3 != nil { - if err := csv.v3.CreatePreStates(ctx); err != nil { + if err := csv.v3.CreatePreStates(ctx, brs[idx]); err != nil { return err } } diff --git a/blockindex/contractstaking/bucket.go b/blockindex/contractstaking/bucket.go index c2e0f6c453..6cfed464eb 100644 --- a/blockindex/contractstaking/bucket.go +++ b/blockindex/contractstaking/bucket.go @@ -7,6 +7,7 @@ package contractstaking import ( "github.com/iotexproject/iotex-core/v2/action/protocol/staking" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" ) // Bucket defines the bucket struct for contract staking @@ -31,3 +32,28 @@ func assembleBucket(token uint64, bi *bucketInfo, bt *BucketType, contractAddr s } return &vb } + +func assembleContractBucket(bi *bucketInfo, bt *BucketType) *contractstaking.Bucket { + return &contractstaking.Bucket{ + Candidate: bi.Delegate, + Owner: bi.Owner, + StakedAmount: bt.Amount, + StakedDuration: bt.Duration, + CreatedAt: bi.CreatedAt, + UnlockedAt: bi.UnlockedAt, + UnstakedAt: bi.UnstakedAt, + } +} + +func contractBucketToVoteBucket(token uint64, b *contractstaking.Bucket, contractAddr string, blocksToDurationFn blocksDurationFn) *Bucket { + return assembleBucket(token, &bucketInfo{ + Owner: b.Owner, + Delegate: b.Candidate, + CreatedAt: b.CreatedAt, + UnlockedAt: b.UnlockedAt, + UnstakedAt: b.UnstakedAt, + }, &BucketType{ + Amount: b.StakedAmount, + Duration: b.StakedDuration, + }, contractAddr, blocksToDurationFn) +} diff --git a/blockindex/contractstaking/eventprocessor_builder.go b/blockindex/contractstaking/eventprocessor_builder.go new file mode 100644 index 0000000000..7127ea83dc --- /dev/null +++ b/blockindex/contractstaking/eventprocessor_builder.go @@ -0,0 +1,23 @@ +package contractstaking + +import ( + "context" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" +) + +type eventProcessorBuilder struct { + contractAddr address.Address +} + +func newEventProcessorBuilder(contractAddr address.Address) *eventProcessorBuilder { + return &eventProcessorBuilder{ + contractAddr: contractAddr, + } +} + +func (b *eventProcessorBuilder) Build(ctx context.Context, handler staking.EventHandler) staking.EventProcessor { + return newContractStakingEventProcessor(b.contractAddr, handler) +} diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index 4d5d347b8d..a87a19e6d2 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -21,6 +21,7 @@ import ( "github.com/iotexproject/iotex-core/v2/db" "github.com/iotexproject/iotex-core/v2/pkg/lifecycle" "github.com/iotexproject/iotex-core/v2/pkg/util/byteutil" + "github.com/iotexproject/iotex-core/v2/systemcontractindex/stakingindex" ) const ( @@ -107,66 +108,25 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s return nil, errors.New("indexer not started") } featureCtx, ok := protocol.GetFeatureCtx(ctx) - if !ok || featureCtx.StoreVoteOfNFTBucketIntoView { - return &stakeView{ - contractAddr: s.contractAddr, - config: s.config, - cache: s.cache.Clone(), - height: s.height, - genBlockDurationFn: s.genBlockDurationFn, - }, nil - } - cssr := contractstaking.NewStateReader(sr) - tids, types, err := cssr.BucketTypes(s.contractAddr) - if err != nil { - return nil, errors.Wrapf(err, "failed to get bucket types for contract %s", s.contractAddr) - } - if len(tids) != len(types) { - return nil, errors.Errorf("length of tids (%d) does not match length of types (%d)", len(tids), len(types)) - } - ids, buckets, err := contractstaking.NewStateReader(sr).Buckets(s.contractAddr) - if err != nil { - return nil, errors.Wrapf(err, "failed to get buckets for contract %s", s.contractAddr) - } - if len(ids) != len(buckets) { - return nil, errors.Errorf("length of ids (%d) does not match length of buckets (%d)", len(ids), len(buckets)) - } - cache := &contractStakingCache{} - for i, id := range tids { - if types[i] == nil { - return nil, errors.Errorf("bucket type %d is nil", id) - } - cache.PutBucketType(id, types[i]) + if ok && !featureCtx.StoreVoteOfNFTBucketIntoView { + return nil, nil } + + ids, typs, infos := s.cache.Buckets() + buckets := make(map[uint64]*contractstaking.Bucket) for i, id := range ids { - if buckets[i] == nil { - return nil, errors.New("bucket is nil") - } - tid, bt := cache.MatchBucketType(buckets[i].StakedAmount, buckets[i].StakedDuration) - if bt == nil { - return nil, errors.Errorf( - "no bucket type found for bucket %d with staked amount %s and duration %d", - id, - buckets[i].StakedAmount.String(), - buckets[i].StakedDuration, - ) - } - cache.PutBucketInfo(id, &bucketInfo{ - TypeIndex: tid, - CreatedAt: buckets[i].CreatedAt, - UnlockedAt: buckets[i].UnlockedAt, - UnstakedAt: buckets[i].UnstakedAt, - Delegate: buckets[i].Candidate, - Owner: buckets[i].Owner, - }) - } - - return &stakeView{ - cache: cache, - height: s.height, - config: s.config, - contractAddr: s.contractAddr, - }, nil + buckets[id] = assembleContractBucket(infos[i], typs[i]) + } + calculateUnmutedVoteWeightAt := func(b *contractstaking.Bucket, height uint64) *big.Int { + vb := contractBucketToVoteBucket(0, b, s.contractAddr.String(), s.genBlockDurationFn(height)) + return s.config.CalculateVoteWeight(vb) + } + cur := stakingindex.AggregateCandidateVotes(buckets, func(b *contractstaking.Bucket) *big.Int { + return calculateUnmutedVoteWeightAt(b, s.height) + }) + processorBuilder := newEventProcessorBuilder(s.contractAddr) + cfg := &stakingindex.VoteViewConfig{ContractAddr: s.contractAddr} + return stakingindex.NewVoteView(cfg, s.height, cur, processorBuilder, calculateUnmutedVoteWeightAt), nil } // Stop stops the indexer @@ -243,6 +203,20 @@ func (s *Indexer) genBlockDurationFn(height uint64) blocksDurationFn { } } +// DeductBucket deducts the bucket by address and id +func (s *Indexer) DeductBucket(addr address.Address, id uint64) (*contractstaking.Bucket, error) { + s.mu.RLock() + defer s.mu.RUnlock() + if s.contractAddr.String() != addr.String() { + return nil, errors.Wrapf(contractstaking.ErrBucketNotExist, "contract address not match: %s vs %s", s.contractAddr.String(), addr.String()) + } + bt, bi := s.cache.Bucket(id) + if bt == nil || bi == nil { + return nil, errors.Wrapf(contractstaking.ErrBucketNotExist, "bucket %d not found", id) + } + return assembleContractBucket(bi, bt), nil +} + // Buckets returns the buckets func (s *Indexer) Buckets(height uint64) ([]*Bucket, error) { if s.isIgnored(height) { @@ -374,6 +348,19 @@ func (s *Indexer) BucketTypes(height uint64) ([]*BucketType, error) { return bts, nil } +// ContractStakingBuckets returns all contract staking buckets +func (s *Indexer) ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + ids, typs, infos := s.cache.Buckets() + res := make(map[uint64]*contractstaking.Bucket) + for i, id := range ids { + res[id] = assembleContractBucket(infos[i], typs[i]) + } + return s.height, res, nil +} + // PutBlock puts a block into indexer func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error { if blk.Height() < s.config.ContractDeployHeight { diff --git a/blockindex/contractstaking/stakeview.go b/blockindex/contractstaking/stakeview.go deleted file mode 100644 index 1515c076d4..0000000000 --- a/blockindex/contractstaking/stakeview.go +++ /dev/null @@ -1,162 +0,0 @@ -package contractstaking - -import ( - "context" - "slices" - - "github.com/iotexproject/iotex-address/address" - "github.com/pkg/errors" - - "github.com/iotexproject/iotex-core/v2/action" - "github.com/iotexproject/iotex-core/v2/action/protocol" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" -) - -type stakeView struct { - contractAddr address.Address - config Config - cache stakingCache - genBlockDurationFn func(view uint64) blocksDurationFn - height uint64 -} - -func (s *stakeView) Wrap() staking.ContractStakeView { - return &stakeView{ - contractAddr: s.contractAddr, - config: s.config, - cache: newWrappedCache(s.cache), - height: s.height, - genBlockDurationFn: s.genBlockDurationFn, - } -} - -func (s *stakeView) Fork() staking.ContractStakeView { - return &stakeView{ - contractAddr: s.contractAddr, - cache: newWrappedCacheWithCloneInCommit(s.cache), - height: s.height, - genBlockDurationFn: s.genBlockDurationFn, - } -} - -func (s *stakeView) assembleBuckets(ids []uint64, types []*BucketType, infos []*bucketInfo) []*Bucket { - vbs := make([]*Bucket, 0, len(ids)) - for i, id := range ids { - bt := types[i] - info := infos[i] - if bt != nil && info != nil { - vbs = append(vbs, s.assembleBucket(id, info, bt)) - } - } - return vbs -} - -func (s *stakeView) IsDirty() bool { - return s.cache.IsDirty() -} - -func (s *stakeView) Migrate(handler staking.EventHandler) error { - bts := s.cache.BucketTypes() - tids := make([]uint64, 0, len(bts)) - for id := range bts { - tids = append(tids, id) - } - slices.Sort(tids) - for _, id := range tids { - if err := handler.PutBucketType(s.contractAddr, bts[id]); err != nil { - return err - } - } - ids, types, infos := s.cache.Buckets() - bucketMap := make(map[uint64]*bucketInfo, len(ids)) - typeMap := make(map[uint64]*BucketType, len(ids)) - for i, id := range ids { - bucketMap[id] = infos[i] - typeMap[id] = types[i] - } - slices.Sort(ids) - for _, id := range ids { - info, ok := bucketMap[id] - if !ok { - continue - } - bt := typeMap[id] - if err := handler.PutBucket(s.contractAddr, id, &contractstaking.Bucket{ - Candidate: info.Delegate, - Owner: info.Owner, - StakedAmount: bt.Amount, - StakedDuration: bt.Duration, - CreatedAt: info.CreatedAt, - UnstakedAt: info.UnstakedAt, - UnlockedAt: info.UnlockedAt, - Muted: false, - IsTimestampBased: false, - }); err != nil { - return err - } - } - return nil -} - -func (s *stakeView) BucketsByCandidate(candidate address.Address) ([]*Bucket, error) { - ids, types, infos := s.cache.BucketsByCandidate(candidate) - return s.assembleBuckets(ids, types, infos), nil -} - -func (s *stakeView) assembleBucket(token uint64, bi *bucketInfo, bt *BucketType) *Bucket { - return assembleBucket(token, bi, bt, s.contractAddr.String(), s.genBlockDurationFn(s.height)) -} - -func (s *stakeView) CreatePreStates(ctx context.Context) error { - blkCtx := protocol.MustGetBlockCtx(ctx) - s.height = blkCtx.BlockHeight - return nil -} - -func (s *stakeView) Handle(ctx context.Context, receipt *action.Receipt) error { - // new event handler for this receipt - handler := newContractStakingDirty(newWrappedCache(s.cache)) - processor := newContractStakingEventProcessor(s.contractAddr, handler) - if err := processor.ProcessReceipts(ctx, receipt); err != nil { - return err - } - _, delta := handler.Finalize() - s.cache = delta - - return nil -} - -func (s *stakeView) Commit(ctx context.Context, sm protocol.StateManager) error { - cache, err := s.cache.Commit(ctx, s.contractAddr, sm) - if err != nil { - return err - } - s.cache = cache - return nil -} - -func (s *stakeView) AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error { - blkCtx := protocol.MustGetBlockCtx(ctx) - height := blkCtx.BlockHeight - expectHeight := s.height + 1 - if expectHeight < s.config.ContractDeployHeight { - expectHeight = s.config.ContractDeployHeight - } - if height < expectHeight { - return nil - } - if height > expectHeight { - return errors.Errorf("invalid block height %d, expect %d", height, expectHeight) - } - - handler := newContractStakingDirty(newWrappedCache(s.cache)) - processor := newContractStakingEventProcessor(s.contractAddr, handler) - if err := processor.ProcessReceipts(ctx, receipts...); err != nil { - return err - } - _, delta := handler.Finalize() - s.cache = delta - s.height = height - return nil -} diff --git a/chainservice/builder.go b/chainservice/builder.go index 83be715310..667e5e47d0 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -368,13 +368,16 @@ func (builder *Builder) buildContractStakingIndexer(forTest bool) error { if err != nil { return errors.Wrapf(err, "failed to parse contract address %s", builder.cfg.Genesis.SystemStakingContractV2Address) } - indexer := stakingindex.NewIndexer( + indexer, err := stakingindex.NewIndexer( kvstore, contractAddr, builder.cfg.Genesis.SystemStakingContractV2Height, builder.blocksToDurationFn, stakingindex.WithMuteHeight(builder.cfg.Genesis.WakeBlockHeight), ) + if err != nil { + return err + } builder.cs.contractStakingIndexerV2 = indexer builder.cs.factory.AddDependency(indexer) } @@ -384,13 +387,16 @@ func (builder *Builder) buildContractStakingIndexer(forTest bool) error { if err != nil { return errors.Wrapf(err, "failed to parse contract address %s", builder.cfg.Genesis.SystemStakingContractV3Address) } - indexer := stakingindex.NewIndexer( + indexer, err := stakingindex.NewIndexer( kvstore, contractAddr, builder.cfg.Genesis.SystemStakingContractV3Height, builder.blocksToDurationFn, stakingindex.EnableTimestamped(), ) + if err != nil { + return err + } builder.cs.contractStakingIndexerV3 = indexer builder.cs.factory.AddDependency(indexer) } diff --git a/systemcontractindex/stakingindex/candidate_votes.go b/systemcontractindex/stakingindex/candidate_votes.go new file mode 100644 index 0000000000..ddd3b0bdd5 --- /dev/null +++ b/systemcontractindex/stakingindex/candidate_votes.go @@ -0,0 +1,218 @@ +package stakingindex + +import ( + "math/big" + + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" + + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/systemcontractindex/stakingindex/stakingpb" +) + +// CandidateVotes is the interface to manage candidate votes +type CandidateVotes interface { + Clone() CandidateVotes + Votes(fCtx protocol.FeatureCtx, cand string) *big.Int + Add(cand string, amount *big.Int, votes *big.Int) + Clear() + Commit() CandidateVotes + Base() CandidateVotes + IsDirty() bool + Serialize() ([]byte, error) + Deserialize(data []byte) error +} + +type candidate struct { + // total stake amount of candidate + amount *big.Int + // total weighted votes of candidate + votes *big.Int +} + +type candidateVotes struct { + cands map[string]*candidate +} + +type candidateVotesWraper struct { + base CandidateVotes + change *candidateVotes +} + +type candidateVotesWraperCommitInClone struct { + *candidateVotesWraper +} + +func newCandidate() *candidate { + return &candidate{ + amount: big.NewInt(0), + votes: big.NewInt(0), + } +} + +func (cv *candidateVotes) Clone() CandidateVotes { + newCands := make(map[string]*candidate) + for cand, c := range cv.cands { + newCands[cand] = &candidate{ + amount: new(big.Int).Set(c.amount), + votes: new(big.Int).Set(c.votes), + } + } + return &candidateVotes{ + cands: newCands, + } +} + +func (cv *candidateVotes) IsDirty() bool { + return false +} + +func (cv *candidateVotes) Votes(fCtx protocol.FeatureCtx, cand string) *big.Int { + c := cv.cands[cand] + if c == nil { + return nil + } + if !fCtx.FixContractStakingWeightedVotes { + return c.amount + } + return c.votes +} + +func (cv *candidateVotes) Add(cand string, amount *big.Int, votes *big.Int) { + if cv.cands[cand] == nil { + cv.cands[cand] = newCandidate() + } + if amount != nil { + cv.cands[cand].amount = new(big.Int).Add(cv.cands[cand].amount, amount) + } + if votes != nil { + cv.cands[cand].votes = new(big.Int).Add(cv.cands[cand].votes, votes) + } +} + +func (cv *candidateVotes) Clear() { + cv.cands = make(map[string]*candidate) +} + +func (cv *candidateVotes) Serialize() ([]byte, error) { + cl := stakingpb.CandidateList{} + for cand, c := range cv.cands { + cl.Candidates = append(cl.Candidates, &stakingpb.Candidate{ + Address: cand, + Votes: c.votes.String(), + Amount: c.amount.String(), + }) + } + return proto.Marshal(&cl) +} + +func (cv *candidateVotes) Deserialize(data []byte) error { + cl := stakingpb.CandidateList{} + if err := proto.Unmarshal(data, &cl); err != nil { + return errors.Wrap(err, "failed to unmarshal candidate list") + } + for _, c := range cl.Candidates { + votes, ok := new(big.Int).SetString(c.Votes, 10) + if !ok { + return errors.Errorf("failed to parse votes: %s", c.Votes) + } + amount, ok := new(big.Int).SetString(c.Amount, 10) + if !ok { + return errors.Errorf("failed to parse amount: %s", c.Amount) + } + cv.Add(c.Address, amount, votes) + } + return nil +} + +func (cv *candidateVotes) Commit() CandidateVotes { + return cv +} + +func (cv *candidateVotes) Base() CandidateVotes { + return cv +} + +func newCandidateVotes() *candidateVotes { + return &candidateVotes{ + cands: make(map[string]*candidate), + } +} + +func newCandidateVotesWrapper(base CandidateVotes) *candidateVotesWraper { + return &candidateVotesWraper{ + base: base, + change: newCandidateVotes(), + } +} + +func (cv *candidateVotesWraper) Clone() CandidateVotes { + return &candidateVotesWraper{ + base: cv.base.Clone(), + change: cv.change.Clone().(*candidateVotes), + } +} + +func (cv *candidateVotesWraper) IsDirty() bool { + return cv.change.IsDirty() || cv.base.IsDirty() +} + +func (cv *candidateVotesWraper) Votes(fCtx protocol.FeatureCtx, cand string) *big.Int { + base := cv.base.Votes(fCtx, cand) + change := cv.change.Votes(fCtx, cand) + if change == nil { + return base + } + if base == nil { + return change + } + return new(big.Int).Add(base, change) +} + +func (cv *candidateVotesWraper) Add(cand string, amount *big.Int, votes *big.Int) { + cv.change.Add(cand, amount, votes) +} + +func (cv *candidateVotesWraper) Clear() { + cv.change.Clear() + cv.base.Clear() +} + +func (cv *candidateVotesWraper) Commit() CandidateVotes { + // Commit the changes to the base + for cand, change := range cv.change.cands { + cv.base.Add(cand, change.amount, change.votes) + } + cv.change = newCandidateVotes() + // base commit + return cv.base.Commit() +} + +func (cv *candidateVotesWraper) Serialize() ([]byte, error) { + return nil, errors.New("not implemented") +} + +func (cv *candidateVotesWraper) Deserialize(data []byte) error { + return errors.New("not implemented") +} + +func (cv *candidateVotesWraper) Base() CandidateVotes { + return cv.base +} + +func newCandidateVotesWrapperCommitInClone(base CandidateVotes) *candidateVotesWraperCommitInClone { + return &candidateVotesWraperCommitInClone{ + candidateVotesWraper: newCandidateVotesWrapper(base), + } +} + +func (cv *candidateVotesWraperCommitInClone) Clone() CandidateVotes { + return &candidateVotesWraperCommitInClone{ + candidateVotesWraper: cv.candidateVotesWraper.Clone().(*candidateVotesWraper), + } +} + +func (cv *candidateVotesWraperCommitInClone) Commit() CandidateVotes { + cv.base = cv.base.Clone() + return cv.candidateVotesWraper.Commit() +} diff --git a/systemcontractindex/stakingindex/eventprocessor_builder.go b/systemcontractindex/stakingindex/eventprocessor_builder.go new file mode 100644 index 0000000000..b81ba25df6 --- /dev/null +++ b/systemcontractindex/stakingindex/eventprocessor_builder.go @@ -0,0 +1,30 @@ +package stakingindex + +import ( + "context" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" +) + +type eventProcessorBuilder struct { + contractAddr address.Address + timestamped bool + muteHeight uint64 +} + +func newEventProcessorBuilder(contractAddr address.Address, timestamped bool, muteHeight uint64) *eventProcessorBuilder { + return &eventProcessorBuilder{ + contractAddr: contractAddr, + timestamped: timestamped, + muteHeight: muteHeight, + } +} + +func (b *eventProcessorBuilder) Build(ctx context.Context, handler staking.EventHandler) staking.EventProcessor { + blkCtx := protocol.MustGetBlockCtx(ctx) + muted := b.muteHeight > 0 && blkCtx.BlockHeight >= b.muteHeight + return newEventProcessor(b.contractAddr, blkCtx, handler, b.timestamped, muted) +} diff --git a/systemcontractindex/stakingindex/index.go b/systemcontractindex/stakingindex/index.go index f8e0fb77ed..b545af706f 100644 --- a/systemcontractindex/stakingindex/index.go +++ b/systemcontractindex/stakingindex/index.go @@ -2,6 +2,7 @@ package stakingindex import ( "context" + "math/big" "sync" "time" @@ -42,23 +43,27 @@ type ( PutBlock(ctx context.Context, blk *block.Block) error LoadStakeView(context.Context, protocol.StateReader) (staking.ContractStakeView, error) CreateEventProcessor(context.Context, staking.EventHandler) staking.EventProcessor + ContractStakingBuckets() (uint64, map[uint64]*Bucket, error) + staking.BucketReader } // Indexer is the staking indexer Indexer struct { - common *systemcontractindex.IndexerCommon - cache *base // in-memory cache, used to query index data - mutex sync.RWMutex - blocksToDuration blocksDurationAtFn // function to calculate duration from block range - bucketNS string - ns string - muteHeight uint64 - timestamped bool + common *systemcontractindex.IndexerCommon + cache *base // in-memory cache, used to query index data + mutex sync.RWMutex + blocksToDuration blocksDurationAtFn // function to calculate duration from block range + bucketNS string + ns string + muteHeight uint64 + timestamped bool + calculateVoteWeight CalculateVoteWeightFunc } // IndexerOption is the option to create an indexer IndexerOption func(*Indexer) - blocksDurationFn func(start uint64, end uint64) time.Duration - blocksDurationAtFn func(start uint64, end uint64, viewAt uint64) time.Duration + blocksDurationFn func(start uint64, end uint64) time.Duration + blocksDurationAtFn func(start uint64, end uint64, viewAt uint64) time.Duration + CalculateVoteWeightFunc func(v *VoteBucket) *big.Int ) // WithMuteHeight sets the mute height @@ -75,8 +80,15 @@ func EnableTimestamped() IndexerOption { } } +// WithCalculateUnmutedVoteWeightFn sets the function to calculate unmuted vote weight +func WithCalculateUnmutedVoteWeightFn(f CalculateVoteWeightFunc) IndexerOption { + return func(s *Indexer) { + s.calculateVoteWeight = f + } +} + // NewIndexer creates a new staking indexer -func NewIndexer(kvstore db.KVStore, contractAddr address.Address, startHeight uint64, blocksToDurationFn blocksDurationAtFn, opts ...IndexerOption) *Indexer { +func NewIndexer(kvstore db.KVStore, contractAddr address.Address, startHeight uint64, blocksToDurationFn blocksDurationAtFn, opts ...IndexerOption) (*Indexer, error) { bucketNS := contractAddr.String() + "#" + stakingBucketNS ns := contractAddr.String() + "#" + stakingNS idx := &Indexer{ @@ -89,7 +101,10 @@ func NewIndexer(kvstore db.KVStore, contractAddr address.Address, startHeight ui for _, opt := range opts { opt(idx) } - return idx + if idx.calculateVoteWeight == nil { + return nil, errors.New("calculateVoteWeight function is not set") + } + return idx, nil } // Start starts the indexer @@ -127,6 +142,20 @@ func (s *Indexer) CreateEventProcessor(ctx context.Context, handler staking.Even ) } +// DeductBucket deducts the bucket from the indexer +func (s *Indexer) DeductBucket(addr address.Address, id uint64) (*contractstaking.Bucket, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + if s.ContractAddress().String() != addr.String() { + return nil, errors.Wrap(contractstaking.ErrBucketNotExist, "contract address not match") + } + bkt := s.cache.Bucket(id) + if bkt == nil { + return nil, errors.Wrap(contractstaking.ErrBucketNotExist, "bucket not exist") + } + return bkt, nil +} + // LoadStakeView loads the contract stake view from state reader func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (staking.ContractStakeView, error) { s.mutex.RLock() @@ -134,44 +163,35 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s if !s.common.Started() { return nil, errors.New("indexer not started") } - if protocol.MustGetFeatureCtx(ctx).StoreVoteOfNFTBucketIntoView { - return &stakeView{ - cache: s.cache.Clone(), - height: s.common.Height(), - contractAddr: s.common.ContractAddress(), - muteHeight: s.muteHeight, - timestamped: s.timestamped, - startHeight: s.common.StartHeight(), - bucketNS: s.bucketNS, - genBlockDurationFn: s.genBlockDurationFn, - }, nil - } - contractAddr := s.common.ContractAddress() - ids, buckets, err := contractstaking.NewStateReader(sr).Buckets(contractAddr) + if !protocol.MustGetFeatureCtx(ctx).StoreVoteOfNFTBucketIntoView { + return nil, nil + } + srHeight, err := sr.Height() if err != nil { - return nil, errors.Wrapf(err, "failed to get buckets for contract %s", contractAddr) + return nil, errors.Wrap(err, "failed to get state reader height") } - if len(ids) != len(buckets) { - return nil, errors.Errorf("length of ids (%d) does not match length of buckets (%d)", len(ids), len(buckets)) + if srHeight != s.common.Height() { + return nil, errors.New("state reader height does not match indexer height") } - cache := &base{} - for i, b := range buckets { - if b == nil { - return nil, errors.New("bucket is nil") - } - b.IsTimestampBased = s.timestamped - cache.PutBucket(ids[i], b) - } - return &stakeView{ - cache: cache, - height: s.common.Height(), - contractAddr: s.common.ContractAddress(), - muteHeight: s.muteHeight, - startHeight: s.common.StartHeight(), - timestamped: s.timestamped, - bucketNS: s.bucketNS, - genBlockDurationFn: s.genBlockDurationFn, - }, nil + cfg := &VoteViewConfig{ + ContractAddr: s.common.ContractAddress(), + } + processorBuilder := newEventProcessorBuilder(s.common.ContractAddress(), s.timestamped, s.muteHeight) + return NewVoteView(cfg, s.common.Height(), s.createCandidateVotes(s.cache.buckets), processorBuilder, s.calculateContractVoteWeight), nil +} + +// ContractStakingBuckets returns all the contract staking buckets +func (s *Indexer) ContractStakingBuckets() (uint64, map[uint64]*Bucket, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + + idxs := s.cache.BucketIdxs() + bkts := s.cache.Buckets(idxs) + res := make(map[uint64]*Bucket) + for i, id := range idxs { + res[id] = bkts[i] + } + return s.common.Height(), res, nil } // StartHeight returns the start height of the indexer @@ -358,3 +378,27 @@ func (s *Indexer) genBlockDurationFn(view uint64) blocksDurationFn { return s.blocksToDuration(start, end, view) } } + +func (s *Indexer) createCandidateVotes(bkts map[uint64]*Bucket) CandidateVotes { + return AggregateCandidateVotes(bkts, func(b *contractstaking.Bucket) *big.Int { + return s.calculateContractVoteWeight(b, s.common.Height()) + }) +} + +func (s *Indexer) calculateContractVoteWeight(b *Bucket, height uint64) *big.Int { + vb := assembleVoteBucket(0, b, s.common.ContractAddress().String(), s.genBlockDurationFn(height)) + return s.calculateVoteWeight(vb) +} + +// AggregateCandidateVotes aggregates the votes for each candidate from the given buckets +func AggregateCandidateVotes(bkts map[uint64]*Bucket, calculateUnmutedVoteWeight CalculateUnmutedVoteWeightFn) CandidateVotes { + res := newCandidateVotes() + for _, bkt := range bkts { + if bkt.Muted || bkt.UnstakedAt < maxStakingNumber { + continue + } + votes := calculateUnmutedVoteWeight(bkt) + res.Add(bkt.Candidate.String(), bkt.StakedAmount, votes) + } + return res +} diff --git a/systemcontractindex/stakingindex/stakeview.go b/systemcontractindex/stakingindex/stakeview.go deleted file mode 100644 index 0f0fb053e8..0000000000 --- a/systemcontractindex/stakingindex/stakeview.go +++ /dev/null @@ -1,126 +0,0 @@ -package stakingindex - -import ( - "context" - "slices" - - "github.com/iotexproject/iotex-address/address" - "github.com/pkg/errors" - - "github.com/iotexproject/iotex-core/v2/action" - "github.com/iotexproject/iotex-core/v2/action/protocol" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking" -) - -type stakeView struct { - cache indexerCache - height uint64 - startHeight uint64 - contractAddr address.Address - muteHeight uint64 - timestamped bool - bucketNS string - genBlockDurationFn func(view uint64) blocksDurationFn -} - -func (s *stakeView) Wrap() staking.ContractStakeView { - return &stakeView{ - cache: newWrappedCache(s.cache), - height: s.height, - startHeight: s.startHeight, - contractAddr: s.contractAddr, - muteHeight: s.muteHeight, - timestamped: s.timestamped, - bucketNS: s.bucketNS, - genBlockDurationFn: s.genBlockDurationFn, - } -} - -func (s *stakeView) Fork() staking.ContractStakeView { - return &stakeView{ - cache: newWrappedCacheWithCloneInCommit(s.cache), - height: s.height, - startHeight: s.startHeight, - contractAddr: s.contractAddr, - muteHeight: s.muteHeight, - timestamped: s.timestamped, - bucketNS: s.bucketNS, - genBlockDurationFn: s.genBlockDurationFn, - } -} - -func (s *stakeView) IsDirty() bool { - return s.cache.IsDirty() -} - -func (s *stakeView) Migrate(handler staking.EventHandler) error { - ids := s.cache.BucketIdxs() - slices.Sort(ids) - buckets := s.cache.Buckets(ids) - for _, id := range ids { - if err := handler.PutBucket(s.contractAddr, id, buckets[id]); err != nil { - return err - } - } - return nil -} - -func (s *stakeView) BucketsByCandidate(candidate address.Address) ([]*VoteBucket, error) { - idxs := s.cache.BucketIdsByCandidate(candidate) - bkts := s.cache.Buckets(idxs) - // filter out muted buckets - idxsFiltered := make([]uint64, 0, len(bkts)) - bktsFiltered := make([]*Bucket, 0, len(bkts)) - for i := range bkts { - if !bkts[i].Muted { - idxsFiltered = append(idxsFiltered, idxs[i]) - bktsFiltered = append(bktsFiltered, bkts[i]) - } - } - vbs := batchAssembleVoteBucket(idxsFiltered, bktsFiltered, s.contractAddr.String(), s.genBlockDurationFn(s.height)) - return vbs, nil -} - -func (s *stakeView) CreatePreStates(ctx context.Context) error { - blkCtx := protocol.MustGetBlockCtx(ctx) - s.height = blkCtx.BlockHeight - return nil -} - -func (s *stakeView) Handle(ctx context.Context, receipt *action.Receipt) error { - blkCtx := protocol.MustGetBlockCtx(ctx) - muted := s.muteHeight > 0 && blkCtx.BlockHeight >= s.muteHeight - return newEventProcessor( - s.contractAddr, blkCtx, newEventHandler(s.bucketNS, s.cache), s.timestamped, muted, - ).ProcessReceipts(ctx, receipt) -} - -func (s *stakeView) AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error { - blkCtx := protocol.MustGetBlockCtx(ctx) - height := blkCtx.BlockHeight - if height < s.startHeight { - return nil - } - if height != s.height+1 && height != s.startHeight { - return errors.Errorf("block height %d does not match stake view height %d", height, s.height+1) - } - ctx = protocol.WithBlockCtx(ctx, blkCtx) - muted := s.muteHeight > 0 && height >= s.muteHeight - if err := newEventProcessor( - s.contractAddr, blkCtx, newEventHandler(s.bucketNS, s.cache), s.timestamped, muted, - ).ProcessReceipts(ctx, receipts...); err != nil { - return errors.Wrapf(err, "failed to handle receipts at height %d", height) - } - s.height = height - return nil -} - -func (s *stakeView) Commit(ctx context.Context, sm protocol.StateManager) error { - cache, err := s.cache.Commit(ctx, s.contractAddr, s.timestamped, sm) - if err != nil { - return err - } - s.cache = cache - - return nil -} diff --git a/systemcontractindex/stakingindex/stakingpb/staking.pb.go b/systemcontractindex/stakingindex/stakingpb/staking.pb.go index 84271a9666..f5617bfe43 100644 --- a/systemcontractindex/stakingindex/stakingpb/staking.pb.go +++ b/systemcontractindex/stakingindex/stakingpb/staking.pb.go @@ -139,6 +139,116 @@ func (x *Bucket) GetTimestamped() bool { return false } +type Candidate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Votes string `protobuf:"bytes,2,opt,name=votes,proto3" json:"votes,omitempty"` + Amount string `protobuf:"bytes,3,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (x *Candidate) Reset() { + *x = Candidate{} + if protoimpl.UnsafeEnabled { + mi := &file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Candidate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Candidate) ProtoMessage() {} + +func (x *Candidate) ProtoReflect() protoreflect.Message { + mi := &file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Candidate.ProtoReflect.Descriptor instead. +func (*Candidate) Descriptor() ([]byte, []int) { + return file_systemcontractindex_stakingindex_stakingpb_staking_proto_rawDescGZIP(), []int{1} +} + +func (x *Candidate) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *Candidate) GetVotes() string { + if x != nil { + return x.Votes + } + return "" +} + +func (x *Candidate) GetAmount() string { + if x != nil { + return x.Amount + } + return "" +} + +type CandidateList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Candidates []*Candidate `protobuf:"bytes,1,rep,name=candidates,proto3" json:"candidates,omitempty"` +} + +func (x *CandidateList) Reset() { + *x = CandidateList{} + if protoimpl.UnsafeEnabled { + mi := &file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CandidateList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CandidateList) ProtoMessage() {} + +func (x *CandidateList) ProtoReflect() protoreflect.Message { + mi := &file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CandidateList.ProtoReflect.Descriptor instead. +func (*CandidateList) Descriptor() ([]byte, []int) { + return file_systemcontractindex_stakingindex_stakingpb_staking_proto_rawDescGZIP(), []int{2} +} + +func (x *CandidateList) GetCandidates() []*Candidate { + if x != nil { + return x.Candidates + } + return nil +} + var File_systemcontractindex_stakingindex_stakingpb_staking_proto protoreflect.FileDescriptor var file_systemcontractindex_stakingindex_stakingpb_staking_proto_rawDesc = []byte{ @@ -163,12 +273,23 @@ var file_systemcontractindex_stakingindex_stakingpb_staking_proto_rawDesc = []by 0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x65, 0x64, 0x42, 0x4f, 0x5a, 0x4d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6f, 0x74, 0x65, 0x78, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x2f, 0x69, 0x6f, 0x74, 0x65, 0x78, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x73, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6e, 0x64, 0x65, 0x78, - 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2f, 0x73, 0x74, - 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x61, 0x6d, 0x70, 0x65, 0x64, 0x22, 0x53, 0x0a, 0x09, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x6f, + 0x74, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4d, 0x0a, 0x0d, 0x43, + 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x0a, + 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x73, 0x74, 0x61, 0x6b, 0x69, + 0x6e, 0x67, 0x70, 0x62, 0x2e, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0a, + 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x73, 0x42, 0x4f, 0x5a, 0x4d, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6f, 0x74, 0x65, 0x78, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6f, 0x74, 0x65, 0x78, 0x2d, 0x63, 0x6f, 0x72, 0x65, + 0x2f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x69, + 0x6e, 0x64, 0x65, 0x78, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x69, 0x6e, 0x64, 0x65, + 0x78, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -183,16 +304,19 @@ func file_systemcontractindex_stakingindex_stakingpb_staking_proto_rawDescGZIP() return file_systemcontractindex_stakingindex_stakingpb_staking_proto_rawDescData } -var file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_systemcontractindex_stakingindex_stakingpb_staking_proto_goTypes = []interface{}{ - (*Bucket)(nil), // 0: contractstakingpb.Bucket + (*Bucket)(nil), // 0: contractstakingpb.Bucket + (*Candidate)(nil), // 1: contractstakingpb.Candidate + (*CandidateList)(nil), // 2: contractstakingpb.CandidateList } var file_systemcontractindex_stakingindex_stakingpb_staking_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 1, // 0: contractstakingpb.CandidateList.candidates:type_name -> contractstakingpb.Candidate + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_systemcontractindex_stakingindex_stakingpb_staking_proto_init() } @@ -213,6 +337,30 @@ func file_systemcontractindex_stakingindex_stakingpb_staking_proto_init() { return nil } } + file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Candidate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_systemcontractindex_stakingindex_stakingpb_staking_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CandidateList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -220,7 +368,7 @@ func file_systemcontractindex_stakingindex_stakingpb_staking_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_systemcontractindex_stakingindex_stakingpb_staking_proto_rawDesc, NumEnums: 0, - NumMessages: 1, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/systemcontractindex/stakingindex/stakingpb/staking.proto b/systemcontractindex/stakingindex/stakingpb/staking.proto index f9263e724b..181933ee5c 100644 --- a/systemcontractindex/stakingindex/stakingpb/staking.proto +++ b/systemcontractindex/stakingindex/stakingpb/staking.proto @@ -19,4 +19,14 @@ message Bucket { uint64 unstakedAt = 7; bool muted = 8; bool timestamped = 9; +} + +message Candidate { + string address = 1; + string votes = 2; + string amount = 3; +} + +message CandidateList { + repeated Candidate candidates = 1; } \ No newline at end of file diff --git a/systemcontractindex/stakingindex/vote_view_handler.go b/systemcontractindex/stakingindex/vote_view_handler.go new file mode 100644 index 0000000000..7ec9f0e7ce --- /dev/null +++ b/systemcontractindex/stakingindex/vote_view_handler.go @@ -0,0 +1,139 @@ +package stakingindex + +import ( + "math/big" + + "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" +) + +type ( + CalculateUnmutedVoteWeightFn func(*contractstaking.Bucket) *big.Int + CalculateUnmutedVoteWeightAtFn func(*contractstaking.Bucket, uint64) *big.Int + BucketReader = staking.BucketReader + + voteViewEventHandler struct { + BucketStore + view CandidateVotes + + calculateUnmutedVoteWeight CalculateUnmutedVoteWeightFn + } +) + +// NewVoteViewEventHandler creates a new vote view event handler wrapper +func NewVoteViewEventHandler(store BucketStore, view CandidateVotes, fn CalculateUnmutedVoteWeightFn) (BucketStore, error) { + return newVoteViewEventHandler(store, view, fn) +} + +func newVoteViewEventHandler(store BucketStore, view CandidateVotes, fn CalculateUnmutedVoteWeightFn) (*voteViewEventHandler, error) { + return &voteViewEventHandler{ + BucketStore: store, + view: view, + calculateUnmutedVoteWeight: fn, + }, nil +} + +func (s *voteViewEventHandler) PutBucket(addr address.Address, id uint64, bucket *contractstaking.Bucket) error { + org, err := s.BucketStore.DeductBucket(addr, id) + switch errors.Cause(err) { + case nil, contractstaking.ErrBucketNotExist: + default: + return errors.Wrapf(err, "failed to deduct bucket") + } + + deltaVotes, deltaAmount := s.calculateBucket(bucket) + if org != nil { + orgVotes, orgAmount := s.calculateBucket(org) + if org.Candidate.String() != bucket.Candidate.String() { + s.view.Add(org.Candidate.String(), new(big.Int).Neg(orgAmount), new(big.Int).Neg(orgVotes)) + } else { + deltaVotes = new(big.Int).Sub(deltaVotes, orgVotes) + deltaAmount = new(big.Int).Sub(deltaAmount, orgAmount) + } + } + s.view.Add(bucket.Candidate.String(), deltaAmount, deltaVotes) + + s.BucketStore.PutBucket(addr, id, bucket) + return nil +} + +func (s *voteViewEventHandler) DeleteBucket(addr address.Address, id uint64) error { + org, err := s.BucketStore.DeductBucket(addr, id) + switch errors.Cause(err) { + case nil: + // subtract original votes + deltaVotes, deltaAmount := s.calculateBucket(org) + s.view.Add(org.Candidate.String(), deltaAmount.Neg(deltaAmount), deltaVotes.Neg(deltaVotes)) + case contractstaking.ErrBucketNotExist: + // do nothing + default: + return errors.Wrapf(err, "failed to deduct bucket") + } + return s.BucketStore.DeleteBucket(addr, id) +} + +func (s *voteViewEventHandler) calculateBucket(bucket *contractstaking.Bucket) (votes *big.Int, amount *big.Int) { + if bucket.Muted || bucket.UnstakedAt < maxStakingNumber { + return big.NewInt(0), big.NewInt(0) + } + return s.calculateUnmutedVoteWeight(bucket), bucket.StakedAmount +} + +type bucketStore struct { + store BucketReader + dirty map[string]map[uint64]*Bucket +} + +func newBucketStore(store BucketReader) *bucketStore { + return &bucketStore{ + store: store, + dirty: make(map[string]map[uint64]*Bucket), + } +} + +func (swb *bucketStore) PutBucketType(addr address.Address, bt *contractstaking.BucketType) error { + return nil +} + +func (swb *bucketStore) DeductBucket(addr address.Address, id uint64) (*contractstaking.Bucket, error) { + dirty, ok := swb.dirtyBucket(addr, id) + if ok { + if dirty == nil { + return nil, errors.Wrap(contractstaking.ErrBucketNotExist, "bucket not exist") + } + return dirty.Clone(), nil + } + bucket, err := swb.store.DeductBucket(addr, id) + if err != nil { + return nil, errors.Wrap(err, "failed to get bucket") + } + return bucket, nil +} + +func (swb *bucketStore) PutBucket(addr address.Address, id uint64, bkt *contractstaking.Bucket) error { + if _, ok := swb.dirty[addr.String()]; !ok { + swb.dirty[addr.String()] = make(map[uint64]*Bucket) + } + swb.dirty[addr.String()][id] = bkt.Clone() + return nil +} + +func (swb *bucketStore) DeleteBucket(addr address.Address, id uint64) error { + if _, ok := swb.dirty[addr.String()]; !ok { + swb.dirty[addr.String()] = make(map[uint64]*Bucket) + } + swb.dirty[addr.String()][id] = nil + return nil +} + +func (swb *bucketStore) dirtyBucket(addr address.Address, id uint64) (*Bucket, bool) { + if buckets, ok := swb.dirty[addr.String()]; ok { + if bkt, ok := buckets[id]; ok { + return bkt, true + } + } + return nil, false +} diff --git a/systemcontractindex/stakingindex/voteview.go b/systemcontractindex/stakingindex/voteview.go new file mode 100644 index 0000000000..ba5802ce54 --- /dev/null +++ b/systemcontractindex/stakingindex/voteview.go @@ -0,0 +1,140 @@ +package stakingindex + +import ( + "context" + "math/big" + + "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/v2/action" + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" +) + +type ( + // BucketStore is the interface to manage buckets in the event handler + BucketStore staking.EventHandler + // VoteViewConfig is the configuration for the vote view + VoteViewConfig struct { + ContractAddr address.Address + } + // EventProcessorBuilder is the interface to build event processor + EventProcessorBuilder interface { + Build(context.Context, staking.EventHandler) staking.EventProcessor + } + voteView struct { + config *VoteViewConfig + height uint64 + cur CandidateVotes + store BucketStore + processorBuilder EventProcessorBuilder + calculateVoteWeightFn CalculateUnmutedVoteWeightAtFn + } +) + +// NewVoteView creates a new vote view +func NewVoteView(cfg *VoteViewConfig, + height uint64, + cur CandidateVotes, + processorBuilder EventProcessorBuilder, + fn CalculateUnmutedVoteWeightAtFn, +) staking.ContractStakeView { + return &voteView{ + config: cfg, + height: height, + cur: cur, + processorBuilder: processorBuilder, + calculateVoteWeightFn: fn, + } +} + +func (s *voteView) Height() uint64 { + return s.height +} + +func (s *voteView) Wrap() staking.ContractStakeView { + cur := newCandidateVotesWrapper(s.cur) + var store BucketStore + if s.store != nil { + store = newBucketStore(s.store) + } + return &voteView{ + config: s.config, + height: s.height, + cur: cur, + store: store, + processorBuilder: s.processorBuilder, + calculateVoteWeightFn: s.calculateVoteWeightFn, + } +} + +func (s *voteView) Fork() staking.ContractStakeView { + cur := newCandidateVotesWrapperCommitInClone(s.cur) + var store BucketStore + if s.store != nil { + store = newBucketStore(s.store) + } + return &voteView{ + config: s.config, + height: s.height, + cur: cur, + store: store, + processorBuilder: s.processorBuilder, + calculateVoteWeightFn: s.calculateVoteWeightFn, + } +} + +func (s *voteView) IsDirty() bool { + return s.cur.IsDirty() +} + +func (s *voteView) Migrate(handler staking.EventHandler, buckets map[uint64]*contractstaking.Bucket) error { + for id := range buckets { + if err := handler.PutBucket(s.config.ContractAddr, id, buckets[id]); err != nil { + return err + } + } + return nil +} + +func (s *voteView) Revise(buckets map[uint64]*contractstaking.Bucket) { + s.cur = AggregateCandidateVotes(buckets, func(b *contractstaking.Bucket) *big.Int { + return s.calculateVoteWeightFn(b, s.height) + }) +} + +func (s *voteView) CandidateStakeVotes(ctx context.Context, candidate address.Address) *big.Int { + featureCtx := protocol.MustGetFeatureCtx(ctx) + if !featureCtx.CreatePostActionStates { + return s.cur.Base().Votes(featureCtx, candidate.String()) + } + return s.cur.Votes(featureCtx, candidate.String()) +} + +func (s *voteView) CreatePreStates(ctx context.Context, br BucketReader) error { + blkCtx := protocol.MustGetBlockCtx(ctx) + s.height = blkCtx.BlockHeight + s.store = newBucketStore(br) + return nil +} + +func (s *voteView) Handle(ctx context.Context, receipt *action.Receipt) error { + handler, err := newVoteViewEventHandler(s.store, s.cur, func(b *contractstaking.Bucket) *big.Int { + return s.calculateVoteWeightFn(b, s.height) + }) + if err != nil { + return errors.Wrap(err, "failed to create event handler") + } + return s.processorBuilder.Build(ctx, handler).ProcessReceipts(ctx, receipt) +} + +func (s *voteView) AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error { + return errors.New("not supported") +} + +func (s *voteView) Commit(ctx context.Context, sm protocol.StateManager) error { + s.cur = s.cur.Commit() + return nil +} From d0d1fb554fe85f3826c0c496817266d0b79c4e7f Mon Sep 17 00:00:00 2001 From: envestcc Date: Sun, 28 Sep 2025 10:05:00 +0800 Subject: [PATCH 02/12] fix test --- .../staking/contractstakeview_mock.go | 223 ++++++++++ action/protocol/staking/protocol.go | 4 +- action/protocol/staking/protocol_test.go | 107 ----- blockindex/contractstaking/indexer.go | 8 +- chainservice/builder.go | 13 +- chainservice/chainservice.go | 3 +- e2etest/contract_staking_v2_test.go | 408 +++++++++++++++++- e2etest/native_staking_test.go | 151 +++++++ misc/scripts/mockgen.sh | 7 + .../stakingindex/candidate_votes_test.go | 62 +++ systemcontractindex/stakingindex/index.go | 2 +- 11 files changed, 869 insertions(+), 119 deletions(-) create mode 100644 action/protocol/staking/contractstakeview_mock.go create mode 100644 systemcontractindex/stakingindex/candidate_votes_test.go diff --git a/action/protocol/staking/contractstakeview_mock.go b/action/protocol/staking/contractstakeview_mock.go new file mode 100644 index 0000000000..003199b4c3 --- /dev/null +++ b/action/protocol/staking/contractstakeview_mock.go @@ -0,0 +1,223 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./action/protocol/staking/viewdata.go +// +// Generated by this command: +// +// mockgen -destination=./action/protocol/staking/contractstakeview_mock.go -source=./action/protocol/staking/viewdata.go -package=staking ContractStakeView +// + +// Package staking is a generated GoMock package. +package staking + +import ( + context "context" + big "math/big" + reflect "reflect" + + address "github.com/iotexproject/iotex-address/address" + action "github.com/iotexproject/iotex-core/v2/action" + protocol "github.com/iotexproject/iotex-core/v2/action/protocol" + contractstaking "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" + gomock "go.uber.org/mock/gomock" +) + +// MockBucketReader is a mock of BucketReader interface. +type MockBucketReader struct { + ctrl *gomock.Controller + recorder *MockBucketReaderMockRecorder + isgomock struct{} +} + +// MockBucketReaderMockRecorder is the mock recorder for MockBucketReader. +type MockBucketReaderMockRecorder struct { + mock *MockBucketReader +} + +// NewMockBucketReader creates a new mock instance. +func NewMockBucketReader(ctrl *gomock.Controller) *MockBucketReader { + mock := &MockBucketReader{ctrl: ctrl} + mock.recorder = &MockBucketReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBucketReader) EXPECT() *MockBucketReaderMockRecorder { + return m.recorder +} + +// DeductBucket mocks base method. +func (m *MockBucketReader) DeductBucket(arg0 address.Address, arg1 uint64) (*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeductBucket", arg0, arg1) + ret0, _ := ret[0].(*contractstaking.Bucket) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeductBucket indicates an expected call of DeductBucket. +func (mr *MockBucketReaderMockRecorder) DeductBucket(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeductBucket", reflect.TypeOf((*MockBucketReader)(nil).DeductBucket), arg0, arg1) +} + +// MockContractStakeView is a mock of ContractStakeView interface. +type MockContractStakeView struct { + ctrl *gomock.Controller + recorder *MockContractStakeViewMockRecorder + isgomock struct{} +} + +// MockContractStakeViewMockRecorder is the mock recorder for MockContractStakeView. +type MockContractStakeViewMockRecorder struct { + mock *MockContractStakeView +} + +// NewMockContractStakeView creates a new mock instance. +func NewMockContractStakeView(ctrl *gomock.Controller) *MockContractStakeView { + mock := &MockContractStakeView{ctrl: ctrl} + mock.recorder = &MockContractStakeViewMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockContractStakeView) EXPECT() *MockContractStakeViewMockRecorder { + return m.recorder +} + +// AddBlockReceipts mocks base method. +func (m *MockContractStakeView) AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddBlockReceipts", ctx, receipts) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddBlockReceipts indicates an expected call of AddBlockReceipts. +func (mr *MockContractStakeViewMockRecorder) AddBlockReceipts(ctx, receipts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBlockReceipts", reflect.TypeOf((*MockContractStakeView)(nil).AddBlockReceipts), ctx, receipts) +} + +// CandidateStakeVotes mocks base method. +func (m *MockContractStakeView) CandidateStakeVotes(ctx context.Context, id address.Address) *big.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CandidateStakeVotes", ctx, id) + ret0, _ := ret[0].(*big.Int) + return ret0 +} + +// CandidateStakeVotes indicates an expected call of CandidateStakeVotes. +func (mr *MockContractStakeViewMockRecorder) CandidateStakeVotes(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CandidateStakeVotes", reflect.TypeOf((*MockContractStakeView)(nil).CandidateStakeVotes), ctx, id) +} + +// Commit mocks base method. +func (m *MockContractStakeView) Commit(arg0 context.Context, arg1 protocol.StateManager) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *MockContractStakeViewMockRecorder) Commit(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockContractStakeView)(nil).Commit), arg0, arg1) +} + +// CreatePreStates mocks base method. +func (m *MockContractStakeView) CreatePreStates(ctx context.Context, br BucketReader) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePreStates", ctx, br) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreatePreStates indicates an expected call of CreatePreStates. +func (mr *MockContractStakeViewMockRecorder) CreatePreStates(ctx, br any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePreStates", reflect.TypeOf((*MockContractStakeView)(nil).CreatePreStates), ctx, br) +} + +// Fork mocks base method. +func (m *MockContractStakeView) Fork() ContractStakeView { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fork") + ret0, _ := ret[0].(ContractStakeView) + return ret0 +} + +// Fork indicates an expected call of Fork. +func (mr *MockContractStakeViewMockRecorder) Fork() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fork", reflect.TypeOf((*MockContractStakeView)(nil).Fork)) +} + +// Handle mocks base method. +func (m *MockContractStakeView) Handle(ctx context.Context, receipt *action.Receipt) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Handle", ctx, receipt) + ret0, _ := ret[0].(error) + return ret0 +} + +// Handle indicates an expected call of Handle. +func (mr *MockContractStakeViewMockRecorder) Handle(ctx, receipt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*MockContractStakeView)(nil).Handle), ctx, receipt) +} + +// IsDirty mocks base method. +func (m *MockContractStakeView) IsDirty() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDirty") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsDirty indicates an expected call of IsDirty. +func (mr *MockContractStakeViewMockRecorder) IsDirty() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDirty", reflect.TypeOf((*MockContractStakeView)(nil).IsDirty)) +} + +// Migrate mocks base method. +func (m *MockContractStakeView) Migrate(arg0 EventHandler, arg1 map[uint64]*contractstaking.Bucket) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Migrate", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Migrate indicates an expected call of Migrate. +func (mr *MockContractStakeViewMockRecorder) Migrate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockContractStakeView)(nil).Migrate), arg0, arg1) +} + +// Revise mocks base method. +func (m *MockContractStakeView) Revise(arg0 map[uint64]*contractstaking.Bucket) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Revise", arg0) +} + +// Revise indicates an expected call of Revise. +func (mr *MockContractStakeViewMockRecorder) Revise(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Revise", reflect.TypeOf((*MockContractStakeView)(nil).Revise), arg0) +} + +// Wrap mocks base method. +func (m *MockContractStakeView) Wrap() ContractStakeView { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Wrap") + ret0, _ := ret[0].(ContractStakeView) + return ret0 +} + +// Wrap indicates an expected call of Wrap. +func (mr *MockContractStakeViewMockRecorder) Wrap() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wrap", reflect.TypeOf((*MockContractStakeView)(nil).Wrap)) +} diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index cdea24d13d..5deea9dfcd 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -503,7 +503,7 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager if err != nil { return err } - if h != blkCtx.BlockHeight-1 { + if indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) } buckets[i] = bs @@ -527,7 +527,7 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager if err != nil { return err } - if h != blkCtx.BlockHeight-1 { + if indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) } buckets[i] = bs diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index 5f373b48a0..e620a6c7c0 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -428,113 +428,6 @@ func Test_CreateGenesisStates(t *testing.T) { } } -func TestProtocol_ActiveCandidates(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - sm := testdb.NewMockStateManagerWithoutHeightFunc(ctrl) - csIndexer := NewMockContractStakingIndexerWithBucketType(ctrl) - - selfStake, _ := new(big.Int).SetString("1200000000000000000000000", 10) - g := genesis.TestDefault() - cfg := g.Staking - cfg.BootstrapCandidates = []genesis.BootstrapCandidate{ - { - OwnerAddress: identityset.Address(22).String(), - OperatorAddress: identityset.Address(23).String(), - RewardAddress: identityset.Address(23).String(), - Name: "test1", - SelfStakingTokens: selfStake.String(), - }, - } - p, err := NewProtocol(HelperCtx{ - DepositGas: nil, - BlockInterval: getBlockInterval, - }, &BuilderConfig{ - Staking: cfg, - PersistStakingPatchBlock: math.MaxUint64, - SkipContractStakingViewHeight: math.MaxUint64, - Revise: ReviseConfig{ - VoteWeight: g.Staking.VoteWeightCalConsts, - }, - }, nil, nil, csIndexer, nil) - require.NoError(err) - - blkHeight := g.QuebecBlockHeight + 1 - ctx := protocol.WithBlockCtx( - genesis.WithGenesisContext(context.Background(), g), - protocol.BlockCtx{ - BlockHeight: blkHeight, - }, - ) - ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) - sm.EXPECT().Height().DoAndReturn(func() (uint64, error) { - return blkHeight, nil - }).AnyTimes() - // csIndexer.EXPECT().StartView(gomock.Any()).Return(nil, nil) - csIndexer.EXPECT().Start(gomock.Any()).Return(nil).AnyTimes() - csIndexer.EXPECT().StartHeight().Return(uint64(blkHeight - 3)).AnyTimes() - csIndexer.EXPECT().Height().Return(uint64(blkHeight), nil).AnyTimes() - csIndexer.EXPECT().LoadStakeView(gomock.Any(), gomock.Any()).Return(nil, nil) - - v, err := p.Start(ctx, sm) - require.NoError(err) - require.NoError(sm.WriteView(_protocolID, v)) - - err = p.CreateGenesisStates(ctx, sm) - require.NoError(err) - - var csIndexerHeight, csVotes uint64 - csIndexer.EXPECT().Height().Return(uint64(0), nil).AnyTimes() - csIndexer.EXPECT().BucketsByCandidate(gomock.Any(), gomock.Any()).DoAndReturn(func(ownerAddr address.Address, height uint64) ([]*VoteBucket, error) { - if height != csIndexerHeight { - return nil, errors.Errorf("invalid height %d", height) - } - return []*VoteBucket{ - NewVoteBucket(identityset.Address(22), identityset.Address(22), big.NewInt(int64(csVotes)), 1, time.Now(), true), - }, nil - }).AnyTimes() - - t.Run("contract staking indexer falls behind", func(t *testing.T) { - _, err := p.ActiveCandidates(ctx, sm, 0) - require.ErrorContains(err, "invalid height") - }) - - t.Run("contract staking votes before Redsea", func(t *testing.T) { - csIndexerHeight = blkHeight - 1 - csVotes = 0 - cands, err := p.ActiveCandidates(ctx, sm, 0) - require.NoError(err) - require.Len(cands, 1) - originCandVotes := cands[0].Votes - csVotes = 100 - cands, err = p.ActiveCandidates(ctx, sm, 0) - require.NoError(err) - require.Len(cands, 1) - require.EqualValues(100, cands[0].Votes.Sub(cands[0].Votes, originCandVotes).Uint64()) - }) - t.Run("contract staking votes after Redsea", func(t *testing.T) { - blkHeight = g.RedseaBlockHeight - ctx := protocol.WithBlockCtx( - genesis.WithGenesisContext(context.Background(), g), - protocol.BlockCtx{ - BlockHeight: blkHeight, - }, - ) - ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) - csIndexerHeight = blkHeight - 1 - csVotes = 0 - cands, err := p.ActiveCandidates(ctx, sm, 0) - require.NoError(err) - require.Len(cands, 1) - originCandVotes := cands[0].Votes - csVotes = 100 - cands, err = p.ActiveCandidates(ctx, sm, 0) - require.NoError(err) - require.Len(cands, 1) - require.EqualValues(103, cands[0].Votes.Sub(cands[0].Votes, originCandVotes).Uint64()) - }) -} - func TestIsSelfStakeBucket(t *testing.T) { r := require.New(t) ctrl := gomock.NewController(t) diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index a87a19e6d2..aa9a0ff0a8 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -111,7 +111,13 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s if ok && !featureCtx.StoreVoteOfNFTBucketIntoView { return nil, nil } - + srHeight, err := sr.Height() + if err != nil { + return nil, errors.Wrap(err, "failed to get state reader height") + } + if s.config.ContractDeployHeight <= srHeight && srHeight != s.height { + return nil, errors.New("state reader height does not match indexer height") + } ids, typs, infos := s.cache.Buckets() buckets := make(map[uint64]*contractstaking.Bucket) for i, id := range ids { diff --git a/chainservice/builder.go b/chainservice/builder.go index 667e5e47d0..8e7af4575a 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -343,18 +343,19 @@ func (builder *Builder) buildContractStakingIndexer(forTest bool) error { dbConfig := builder.cfg.DB dbConfig.DbPath = builder.cfg.Chain.ContractStakingIndexDBPath kvstore := db.NewBoltDB(dbConfig) + voteCalcConsts := builder.cfg.Genesis.VoteWeightCalConsts + calculateVotesWeight := func(v *staking.VoteBucket) *big.Int { + return staking.CalculateVoteWeight(voteCalcConsts, v, false) + } // build contract staking indexer if builder.cs.contractStakingIndexer == nil && len(builder.cfg.Genesis.SystemStakingContractAddress) > 0 { - voteCalcConsts := builder.cfg.Genesis.VoteWeightCalConsts indexer, err := contractstaking.NewContractStakingIndexer( kvstore, contractstaking.Config{ ContractAddress: builder.cfg.Genesis.SystemStakingContractAddress, ContractDeployHeight: builder.cfg.Genesis.SystemStakingContractHeight, - CalculateVoteWeight: func(v *staking.VoteBucket) *big.Int { - return staking.CalculateVoteWeight(voteCalcConsts, v, false) - }, - BlocksToDuration: builder.blocksToDurationFn, + CalculateVoteWeight: calculateVotesWeight, + BlocksToDuration: builder.blocksToDurationFn, }) if err != nil { return err @@ -374,6 +375,7 @@ func (builder *Builder) buildContractStakingIndexer(forTest bool) error { builder.cfg.Genesis.SystemStakingContractV2Height, builder.blocksToDurationFn, stakingindex.WithMuteHeight(builder.cfg.Genesis.WakeBlockHeight), + stakingindex.WithCalculateUnmutedVoteWeightFn(calculateVotesWeight), ) if err != nil { return err @@ -393,6 +395,7 @@ func (builder *Builder) buildContractStakingIndexer(forTest bool) error { builder.cfg.Genesis.SystemStakingContractV3Height, builder.blocksToDurationFn, stakingindex.EnableTimestamped(), + stakingindex.WithCalculateUnmutedVoteWeightFn(calculateVotesWeight), ) if err != nil { return err diff --git a/chainservice/chainservice.go b/chainservice/chainservice.go index 0eee347530..3790e3bddd 100644 --- a/chainservice/chainservice.go +++ b/chainservice/chainservice.go @@ -32,7 +32,6 @@ import ( "github.com/iotexproject/iotex-core/v2/blockchain/block" "github.com/iotexproject/iotex-core/v2/blockchain/blockdao" "github.com/iotexproject/iotex-core/v2/blockindex" - "github.com/iotexproject/iotex-core/v2/blockindex/contractstaking" "github.com/iotexproject/iotex-core/v2/blocksync" "github.com/iotexproject/iotex-core/v2/consensus" "github.com/iotexproject/iotex-core/v2/nodeinfo" @@ -74,7 +73,7 @@ type ChainService struct { bfIndexer blockindex.BloomFilterIndexer candidateIndexer *poll.CandidateIndexer candBucketsIndexer *staking.CandidatesBucketsIndexer - contractStakingIndexer *contractstaking.Indexer + contractStakingIndexer staking.ContractStakingIndexerWithBucketType contractStakingIndexerV2 stakingindex.StakingIndexer contractStakingIndexerV3 stakingindex.StakingIndexer registry *protocol.Registry diff --git a/e2etest/contract_staking_v2_test.go b/e2etest/contract_staking_v2_test.go index c4ba1e8559..1cae05aeac 100644 --- a/e2etest/contract_staking_v2_test.go +++ b/e2etest/contract_staking_v2_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "math" "math/big" + "slices" "strings" "testing" "time" @@ -29,6 +30,7 @@ import ( "github.com/iotexproject/iotex-core/v2/pkg/unit" "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" "github.com/iotexproject/iotex-core/v2/pkg/util/byteutil" + "github.com/iotexproject/iotex-core/v2/state" "github.com/iotexproject/iotex-core/v2/systemcontractindex/stakingindex" "github.com/iotexproject/iotex-core/v2/test/identityset" "github.com/iotexproject/iotex-core/v2/testutil" @@ -48,6 +50,279 @@ var ( gasPrice1559 = big.NewInt(unit.Qev) ) +func TestContractStakingV1(t *testing.T) { + require := require.New(t) + contractAddress := "io1dkqh5mu9djfas3xyrmzdv9frsmmytel4mp7a64" + cfg := initCfg(require) + cfg.Genesis.UpernavikBlockHeight = 1 + cfg.Genesis.VanuatuBlockHeight = 100 + cfg.Genesis.WakeBlockHeight = 120 // mute staking v2 + cfg.Genesis.SystemStakingContractAddress = contractAddress + cfg.Genesis.SystemStakingContractHeight = 1 + cfg.Genesis.SystemStakingContractV2Address = "" + cfg.Genesis.SystemStakingContractV3Address = "" + cfg.DardanellesUpgrade.BlockInterval = time.Second * 8640 + cfg.Plugins[config.GatewayPlugin] = nil + test := newE2ETest(t, cfg) + + var ( + successExpect = &basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_Success), ""} + chainID = test.cfg.Chain.ID + contractCreator = 1 + stakerID = 2 + beneficiaryID = 10 + stakeAmount = unit.ConvertIotxToRau(10000) + registerAmount = unit.ConvertIotxToRau(1200000) + stakeTime = time.Now() + unlockTime = stakeTime.Add(time.Hour) + candOwnerID = 3 + candOwnerID2 = 4 + blocksPerDay = 24 * time.Hour / cfg.DardanellesUpgrade.BlockInterval + stakeDurationBlocks = big.NewInt(int64(blocksPerDay)) + // blocksToWithdraw = 3 * blocksPerDay + // minAmount = unit.ConvertIotxToRau(1000) + + tmpVotes = big.NewInt(0) + // tmpVotes2 = big.NewInt(0) + tmpBalance = big.NewInt(0) + // tmpBkt = &iotextypes.VoteBucket{} + ) + bytecode, err := hex.DecodeString(_stakingContractByteCode) + require.NoError(err) + stkABI, err := abi.JSON(strings.NewReader(_stakingContractABI)) + require.NoError(err) + mustCallData := func(m string, args ...any) []byte { + data, err := abiCall(stkABI, m, args...) + require.NoError(err) + return data + } + genTransferActionsWithPrice := func(n int, price *big.Int) []*actionWithTime { + acts := make([]*actionWithTime, n) + for i := 0; i < n; i++ { + acts[i] = &actionWithTime{mustNoErr(action.SignedTransfer(identityset.Address(1).String(), identityset.PrivateKey(2), test.nonceMgr.pop(identityset.Address(2).String()), unit.ConvertIotxToRau(1), nil, gasLimit, price, action.WithChainID(chainID))), time.Now()} + } + return acts + } + genTransferActions := func(n int) []*actionWithTime { + return genTransferActionsWithPrice(n, gasPrice) + } + test.run([]*testcase{ + { + name: "deploy staking contract", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(candOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(candOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(candOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + act: &actionWithTime{mustNoErr(action.SignedExecution("", identityset.PrivateKey(contractCreator), test.nonceMgr.pop(identityset.Address(contractCreator).String()), big.NewInt(0), gasLimit, gasPrice, append(bytecode, mustCallData("")...), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, &executionExpect{contractAddress}}, + }, + }) + bucketTypes := []struct { + amount string + duration int64 + }{ + {"10", 100}, + {"10", 10}, + {"100", 100}, + {"100", 10}, + {"10000", 30 * 17280}, + {stakeAmount.String(), stakeDurationBlocks.Int64()}, + {stakeAmount.String(), new(big.Int).Mul(stakeDurationBlocks, big.NewInt(2)).Int64()}, + {new(big.Int).Mul(stakeAmount, big.NewInt(3)).String(), stakeDurationBlocks.Int64()}, + {new(big.Int).Mul(stakeAmount, big.NewInt(4)).String(), new(big.Int).Mul(stakeDurationBlocks, big.NewInt(2)).Int64()}, + } + acts := make([]*actionWithTime, len(bucketTypes)) + for i := range bucketTypes { + amount, ok := big.NewInt(0).SetString(bucketTypes[i].amount, 10) + require.True(ok) + acts[i] = &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(contractCreator), test.nonceMgr.pop(identityset.Address(contractCreator).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("addBucketType(uint256,uint256)", amount, big.NewInt(bucketTypes[i].duration)), action.WithChainID(chainID))), time.Now()} + } + test.run([]*testcase{ + { + name: "init bucket types", + acts: acts, + blockExpect: func(test *e2etest, blk *block.Block, err error) { + require.NoError(err) + require.Equal(len(bucketTypes)+1, len(blk.Actions)) + for i := range blk.Receipts { + require.Equal(uint64(iotextypes.ReceiptStatus_Success), blk.Receipts[i].Status) + } + }, + }, + { + name: "stake", + preFunc: func(e *e2etest) { + candidate, err := e.getCandidateByName("cand1") + require.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + require.True(ok) + }, + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice, mustCallData("stake(uint256,address)", stakeDurationBlocks, common.BytesToAddress(identityset.Address(candOwnerID).Bytes())), action.WithChainID(chainID))), stakeTime}, + expect: []actionExpect{successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 1, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64(), CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 4, CreateBlockHeight: 4, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: uint64(math.MaxUint64), StakedAmount: stakeAmount.String(), AutoStake: true}}, + &candidateExpect{"cand1", &iotextypes.CandidateV2{OwnerAddress: identityset.Address(candOwnerID).String(), Id: identityset.Address(candOwnerID).String(), OperatorAddress: identityset.Address(1).String(), RewardAddress: identityset.Address(1).String(), Name: "cand1", TotalWeightedVotes: "1256001586604779503009155", SelfStakingTokens: registerAmount.String(), SelfStakeBucketIdx: 0}}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + require.Equal(tmpVotes.Add(tmpVotes, deltaVotes).String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") + }}, + }, + }, + { + name: "unlock", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("unlock(uint256)", big.NewInt(1)), action.WithChainID(chainID))), unlockTime}, + expect: []actionExpect{successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 1, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64(), CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 5, CreateBlockHeight: 4, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: stakeAmount.String(), AutoStake: false}}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + lockedStakeVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + unlockedVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: false, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + tmpVotes.Sub(tmpVotes, lockedStakeVotes) + tmpVotes.Add(tmpVotes, unlockedVotes) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") + }}, + }, + }, + { + name: "lock", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("lock(uint256,uint256)", big.NewInt(1), big.NewInt(0).Mul(big.NewInt(2), stakeDurationBlocks)), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 1, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)) * 2, StakedDurationBlockNumber: stakeDurationBlocks.Uint64() * 2, CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 4, CreateBlockHeight: 4, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: stakeAmount.String(), AutoStake: true}}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + preStakeVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: false, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + postVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(2*stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + tmpVotes.Sub(tmpVotes, preStakeVotes) + tmpVotes.Add(tmpVotes, postVotes) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") + }}, + }, + }, + { + name: "unstake", + preActs: append([]*actionWithTime{ + {mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("unlock(uint256)", big.NewInt(1)), action.WithChainID(chainID))), unlockTime}, + }, genTransferActions(20)...), + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("unstake(uint256)", big.NewInt(1)), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 1, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID).String(), StakedDuration: uint32(2 * stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64() * 2, CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 7, CreateBlockHeight: 4, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: 28, StakedAmount: stakeAmount.String(), AutoStake: false}}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + preStakeVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(2*stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + tmpVotes.Sub(tmpVotes, preStakeVotes) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") + }}, + }, + }, + { + name: "withdraw", + preFunc: func(e *e2etest) { + acc, err := e.api.GetAccount(context.Background(), &iotexapi.GetAccountRequest{Address: identityset.Address(beneficiaryID).String()}) + require.NoError(err) + _, ok := tmpBalance.SetString(acc.AccountMeta.Balance, 10) + require.True(ok) + }, + preActs: genTransferActions(30), + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("withdraw(uint256,address)", big.NewInt(1), common.BytesToAddress(identityset.Address(beneficiaryID).Bytes())), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &noBucketExpect{1, contractAddress}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + acc, err := test.api.GetAccount(context.Background(), &iotexapi.GetAccountRequest{Address: identityset.Address(beneficiaryID).String()}) + require.NoError(err) + tmpBalance.Add(tmpBalance, stakeAmount) + require.Equal(tmpBalance.String(), acc.AccountMeta.Balance) + + checkStakingVoteView(test, require, "cand1") + }}, + }, + }, + { + name: "change candidate", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(candOwnerID2).String()), "cand2", identityset.Address(2).String(), identityset.Address(2).String(), identityset.Address(candOwnerID2).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(candOwnerID2), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice, mustCallData("stake(uint256,address)", stakeDurationBlocks, common.BytesToAddress(identityset.Address(candOwnerID).Bytes())), action.WithChainID(chainID))), stakeTime}, + }, + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("changeDelegate(uint256,address)", big.NewInt(2), common.BytesToAddress(identityset.Address(candOwnerID2).Bytes())), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 2, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID2).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64(), CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 61, CreateBlockHeight: 61, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: stakeAmount.String(), AutoStake: true}}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") + }}, + }, + }, + { + name: "batch stake", + preFunc: func(e *e2etest) { + candidate, err := e.getCandidateByName("cand2") + require.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + require.True(ok) + }, + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0).Mul(big.NewInt(10), stakeAmount), gasLimit, gasPrice, mustCallData("stake(uint256,uint256,address,uint256)", stakeAmount, stakeDurationBlocks, common.BytesToAddress(identityset.Address(candOwnerID2).Bytes()), big.NewInt(10)), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 3, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID2).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64(), CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 63, CreateBlockHeight: 63, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: stakeAmount.String(), AutoStake: true}}, + &bucketExpect{&iotextypes.VoteBucket{Index: 12, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID2).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64(), CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 63, CreateBlockHeight: 63, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: stakeAmount.String(), AutoStake: true}}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand2") + require.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + tmpVotes.Add(tmpVotes, deltaVotes.Mul(deltaVotes, big.NewInt(10))) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand2") + }}, + }, + }, + { + name: "merge", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, mustCallData("merge(uint256[],uint256)", []*big.Int{big.NewInt(3), big.NewInt(4), big.NewInt(5)}, stakeDurationBlocks), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &bucketExpect{&iotextypes.VoteBucket{Index: 3, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID2).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64(), CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 63, CreateBlockHeight: 63, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: big.NewInt(0).Mul(stakeAmount, big.NewInt(3)).String(), AutoStake: true}}, + &noBucketExpect{4, contractAddress}, &noBucketExpect{5, contractAddress}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand2") + require.NoError(err) + subVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) + addVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: big.NewInt(0).Mul(stakeAmount, big.NewInt(3))}, false) + tmpVotes.Sub(tmpVotes, subVotes.Mul(subVotes, big.NewInt(3))) + tmpVotes.Add(tmpVotes, addVotes) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand2") + }}, + }, + }, + // { + // name: "expand", + // act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice, mustCallData("expandBucket(uint256,uint256,uint256)", big.NewInt(3), new(big.Int).Mul(stakeAmount, big.NewInt(4)), big.NewInt(0).Mul(stakeDurationBlocks, big.NewInt(2))), action.WithChainID(chainID))), time.Now()}, + // expect: []actionExpect{successExpect, + // &bucketExpect{&iotextypes.VoteBucket{Index: 3, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID2).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)) * 2, StakedDurationBlockNumber: stakeDurationBlocks.Uint64() * 2, CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 63, CreateBlockHeight: 63, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: big.NewInt(0).Mul(stakeAmount, big.NewInt(4)).String(), AutoStake: true}}, + // &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + // checkStakingVoteView(test, require, "cand2") + // }}, + // }, + // }, + }) + + checkStakingViewInit(test, require) +} + func TestContractStakingV2(t *testing.T) { require := require.New(t) contractAddress := stakingContractV2Address @@ -55,8 +330,10 @@ func TestContractStakingV2(t *testing.T) { cfg.Genesis.UpernavikBlockHeight = 1 cfg.Genesis.VanuatuBlockHeight = 100 cfg.Genesis.WakeBlockHeight = 120 // mute staking v2 + cfg.Genesis.SystemStakingContractAddress = "" cfg.Genesis.SystemStakingContractV2Address = contractAddress cfg.Genesis.SystemStakingContractV2Height = 1 + cfg.Genesis.SystemStakingContractV3Address = "" cfg.DardanellesUpgrade.BlockInterval = time.Second * 8640 cfg.Plugins[config.GatewayPlugin] = nil test := newE2ETest(t, cfg) @@ -127,6 +404,8 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) require.Equal(tmpVotes.Add(tmpVotes, deltaVotes).String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") }}, }, }, @@ -143,6 +422,8 @@ func TestContractStakingV2(t *testing.T) { tmpVotes.Sub(tmpVotes, lockedStakeVotes) tmpVotes.Add(tmpVotes, unlockedVotes) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") }}, }, }, @@ -159,6 +440,8 @@ func TestContractStakingV2(t *testing.T) { tmpVotes.Sub(tmpVotes, preStakeVotes) tmpVotes.Add(tmpVotes, postVotes) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") }}, }, }, @@ -176,6 +459,8 @@ func TestContractStakingV2(t *testing.T) { preStakeVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(2*stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) tmpVotes.Sub(tmpVotes, preStakeVotes) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") }}, }, }, @@ -196,6 +481,8 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) tmpBalance.Add(tmpBalance, stakeAmount) require.Equal(tmpBalance.String(), acc.AccountMeta.Balance) + + checkStakingVoteView(test, require, "cand1") }}, }, }, @@ -212,6 +499,9 @@ func TestContractStakingV2(t *testing.T) { candidate, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") }}, }, }, @@ -233,6 +523,8 @@ func TestContractStakingV2(t *testing.T) { deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)*24) * (time.Hour), StakedAmount: stakeAmount}, false) tmpVotes.Add(tmpVotes, deltaVotes.Mul(deltaVotes, big.NewInt(10))) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand2") }}, }, }, @@ -250,6 +542,8 @@ func TestContractStakingV2(t *testing.T) { tmpVotes.Sub(tmpVotes, subVotes.Mul(subVotes, big.NewInt(3))) tmpVotes.Add(tmpVotes, addVotes) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand2") }}, }, }, @@ -258,6 +552,9 @@ func TestContractStakingV2(t *testing.T) { act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice, mustCallData("expandBucket(uint256,uint256)", big.NewInt(3), big.NewInt(0).Mul(stakeDurationBlocks, big.NewInt(2))), action.WithChainID(chainID))), time.Now()}, expect: []actionExpect{successExpect, &bucketExpect{&iotextypes.VoteBucket{Index: 3, ContractAddress: contractAddress, Owner: identityset.Address(stakerID).String(), CandidateAddress: identityset.Address(candOwnerID2).String(), StakedDuration: uint32(stakeDurationBlocks.Uint64()/uint64(blocksPerDay)) * 2, StakedDurationBlockNumber: stakeDurationBlocks.Uint64() * 2, CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 63, CreateBlockHeight: 63, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: math.MaxUint64, StakedAmount: big.NewInt(0).Mul(stakeAmount, big.NewInt(4)).String(), AutoStake: true}}, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + checkStakingVoteView(test, require, "cand2") + }}, }, }, { @@ -276,6 +573,7 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) tmpBalance.Add(tmpBalance, stakeAmount) require.Equal(tmpBalance.String(), resp.AccountMeta.Balance) + checkStakingVoteView(test, require, "cand2") }}, }, }, @@ -313,6 +611,8 @@ func TestContractStakingV2(t *testing.T) { _, err := test.getBucket(idx, contractAddress) require.NoError(err) } + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") }, }, }) @@ -327,6 +627,7 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) require.True(ok) + checkStakingVoteView(test, require, "cand2") }, preActs: genTransferActionsWithPrice(int(cfg.Genesis.WakeBlockHeight-tipHeight), gasPrice1559), acts: []*actionWithTime{ @@ -349,6 +650,8 @@ func TestContractStakingV2(t *testing.T) { candidate, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") }, }, { @@ -381,6 +684,7 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) require.Equal(candidate.Id, bkt.CandidateAddress) require.Equal(new(big.Int).Add(tmpVotes, new(big.Int).Sub(bktVotes, bktVotesOrg)).String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -408,6 +712,7 @@ func TestContractStakingV2(t *testing.T) { candidate, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(new(big.Int).Sub(tmpVotes, bktVotes).String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -435,6 +740,7 @@ func TestContractStakingV2(t *testing.T) { candidate, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(new(big.Int).Sub(tmpVotes, bktVotes).String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -468,6 +774,7 @@ func TestContractStakingV2(t *testing.T) { candidate, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -496,6 +803,7 @@ func TestContractStakingV2(t *testing.T) { candidate, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -519,6 +827,7 @@ func TestContractStakingV2(t *testing.T) { candidate, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, }) @@ -553,6 +862,7 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) require.Equal(candidate.Id, bkt.CandidateAddress) require.Equal(new(big.Int).Add(tmpVotes, new(big.Int).Sub(bktVotes, bktVotesOrg)).String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -582,6 +892,7 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) require.Equal(candidate.Id, bkt.CandidateAddress) require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -611,6 +922,7 @@ func TestContractStakingV2(t *testing.T) { bkt, err := test.getBucket(legacyBucketIdxs[5], contractAddress) require.NoError(err) require.Nil(bkt) + checkStakingVoteView(test, require, "cand1") }, }, { @@ -632,6 +944,8 @@ func TestContractStakingV2(t *testing.T) { _, ok = tmpVotes2.SetString(cand2.TotalWeightedVotes, 10) require.True(ok) tmpVotes2.Add(tmpVotes2, bktVotes) + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") }, act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("changeDelegate(uint256,address)", big.NewInt(int64(legacyBucketIdxs[6])), common.BytesToAddress(identityset.Address(candOwnerID2).Bytes())), action.WithChainID(chainID))), time.Now()}, blockExpect: func(test *e2etest, blk *block.Block, err error) { @@ -649,6 +963,8 @@ func TestContractStakingV2(t *testing.T) { require.NoError(err) require.Equal(tmpVotes.String(), cand1.TotalWeightedVotes) require.Equal(tmpVotes2.String(), cand2.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") }, }, { @@ -672,6 +988,7 @@ func TestContractStakingV2(t *testing.T) { cand1, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(tmpVotes.String(), cand1.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, }) @@ -689,6 +1006,7 @@ func TestContractStakingV3(t *testing.T) { cfg.Genesis.UpernavikBlockHeight = 1 cfg.Genesis.VanuatuBlockHeight = 100 cfg.Genesis.WakeBlockHeight = 120 // mute staking v2 & enable staking v3 + cfg.Genesis.SystemStakingContractAddress = "" cfg.Genesis.SystemStakingContractV2Address = contractV2Address cfg.Genesis.SystemStakingContractV2Height = 1 cfg.Genesis.SystemStakingContractV3Address = contractV3Address @@ -716,6 +1034,7 @@ func TestContractStakingV3(t *testing.T) { secondsPerDay = 24 * 3600 stakeDurationSeconds = big.NewInt(int64(secondsPerDay)) // 1 day minAmount = unit.ConvertIotxToRau(1000) + bktIdx uint64 tmpVotes = big.NewInt(0) tmpBalance = big.NewInt(0) @@ -956,13 +1275,74 @@ func TestContractStakingV3(t *testing.T) { &bucketExpect{&iotextypes.VoteBucket{Index: 1, ContractAddress: contractV2Address, Owner: contractV3Address, CandidateAddress: address.ZeroAddress, StakedDuration: uint32(stakeDurationBlocks.Uint64() / uint64(blocksPerDay)), StakedDurationBlockNumber: stakeDurationBlocks.Uint64(), CreateTime: timestamppb.New(time.Time{}), StakeStartTime: timestamppb.New(time.Time{}), StakeStartBlockHeight: 17, CreateBlockHeight: 17, UnstakeStartTime: timestamppb.New(time.Time{}), UnstakeStartBlockHeight: uint64(math.MaxUint64), StakedAmount: stakeAmount.String(), AutoStake: true}}, }, }, + { + name: "stake", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV2Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice, mustCallData("stake(uint256,address)", stakeDurationBlocks, common.BytesToAddress(identityset.Address(candOwnerID).Bytes())), action.WithChainID(chainID))), stakeTime}, + expect: []actionExpect{successExpect, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + bktIdxs, err := parseV2StakedBucketIdx(contractV2Address, receipt) + require.NoError(err) + require.Equal(1, len(bktIdxs)) + bktIdx = bktIdxs[0] + }}, + }, + }, }) - test.run([]*testcase{ { preActs: genTransferActionsWithPrice(int(cfg.Genesis.WakeBlockHeight), gasPrice1559), }, }) + // revise the votes at wake height + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") + + // migrate the v2 bucket to v3 + test.run([]*testcase{ + { + name: "migrate", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedExecution(contractV2Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("approve(address,uint256)", contractV3AddressEth, big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), stakeTime}, + }, + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallDataV3("migrateLegacyBucket(uint256)", big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), stakeTime}, + expect: []actionExpect{successExpect, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") + }}, + }, + }, + }) + + test.run([]*testcase{ + { + name: "stake", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice1559, mustCallDataV3("stake(uint256,address)", stakeDurationSeconds, common.BytesToAddress(identityset.Address(candOwnerID).Bytes())), action.WithChainID(chainID))), stakeTime}, + expect: []actionExpect{successExpect, &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + require.NoError(err) + idxs, err := parseV2StakedBucketIdx(contractV3Address, receipt) + require.NoError(err) + require.Equal(1, len(idxs)) + bktIdx = idxs[0] + }}}, + }, + }) + test.run([]*testcase{ + { + name: "change candidate", + acts: []*actionWithTime{ + {mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice1559, mustCallData("expandBucket(uint256,uint256)", big.NewInt(int64(bktIdx)), big.NewInt(0).Mul(stakeDurationSeconds, big.NewInt(2))), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallDataV3("changeDelegate(uint256,address)", big.NewInt(int64(bktIdx)), common.BytesToAddress(identityset.Address(candOwnerID2).Bytes())), action.WithChainID(chainID))), time.Now()}, + }, + expect: []actionExpect{successExpect, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") + }}, + }, + }, + }) + checkStakingViewInit(test, require) } @@ -1324,6 +1704,32 @@ func checkStakingViewInit(test *e2etest, require *require.Assertions) { require.ElementsMatch(cands, newCands, "candidates should be the same after restart") } +func checkStakingVoteView(test *e2etest, require *require.Assertions, candName string) { + tipHeight, err := test.cs.BlockDAO().Height() + require.NoError(err) + test.t.Log("tip height:", tipHeight) + tipHeader, err := test.cs.BlockDAO().HeaderByHeight(tipHeight) + require.NoError(err) + stkPtl := staking.FindProtocol(test.svr.ChainService(test.cfg.Chain.ID).Registry()) + ctx := context.Background() + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: tipHeight, + BlockTimeStamp: tipHeader.Timestamp(), + }) + ctx = genesis.WithGenesisContext(ctx, test.cfg.Genesis) + ctx = protocol.WithFeatureCtx(ctx) + cands, err := stkPtl.ActiveCandidates(ctx, test.cs.StateFactory(), 0) + require.NoError(err) + cand1 := slices.IndexFunc(cands, func(c *state.Candidate) bool { + return string(c.CanName) == candName + }) + require.Greater(cand1, -1) + + candidate, err := test.getCandidateByName(candName) + require.NoError(err) + require.Equal(candidate.TotalWeightedVotes, cands[cand1].Votes.String()) +} + func methodSignToID(sign string) []byte { hash := crypto.Keccak256Hash([]byte(sign)) return hash.Bytes()[:4] diff --git a/e2etest/native_staking_test.go b/e2etest/native_staking_test.go index 6e93bf8fa3..6ce78be9bc 100644 --- a/e2etest/native_staking_test.go +++ b/e2etest/native_staking_test.go @@ -3,6 +3,7 @@ package e2etest import ( "context" "encoding/hex" + "fmt" "math" "math/big" "testing" @@ -22,6 +23,7 @@ import ( "github.com/iotexproject/iotex-core/v2/action/protocol" accountutil "github.com/iotexproject/iotex-core/v2/action/protocol/account/util" "github.com/iotexproject/iotex-core/v2/action/protocol/staking" + "github.com/iotexproject/iotex-core/v2/blockchain/block" "github.com/iotexproject/iotex-core/v2/blockchain/genesis" "github.com/iotexproject/iotex-core/v2/config" "github.com/iotexproject/iotex-core/v2/pkg/unit" @@ -1433,3 +1435,152 @@ func TestCandidateOwnerCollision(t *testing.T) { }, }) } + +func TestNativeStakingVoteBug(t *testing.T) { + r := require.New(t) + cfg := initCfg(r) + cfg.Genesis.LordHoweBlockHeight = 1 + cfg.Genesis.MidwayBlockHeight = 1000 + cfg.Genesis.SystemStakingContractAddress = "" + cfg.Genesis.SystemStakingContractV2Address = "" + cfg.Genesis.SystemStakingContractV3Address = "" + cfg.DardanellesUpgrade.BlockInterval = time.Second * 8640 + cfg.Plugins[config.GatewayPlugin] = nil + test := newE2ETest(t, cfg) + + var ( + // successExpect = &basicActionExpect{nil, uint64(iotextypes.ReceiptStatus_Success), ""} + chainID = test.cfg.Chain.ID + // contractCreator = 1 + stakerID = 2 + // beneficiaryID = 10 + stakeAmount = unit.ConvertIotxToRau(10000) + registerAmount = unit.ConvertIotxToRau(1200000) + stakeTime = time.Now() + // unlockTime = stakeTime.Add(time.Hour) + candOwnerID = 3 + // candOwnerID2 = 4 + // blocksPerDay = 24 * time.Hour / cfg.DardanellesUpgrade.BlockInterval + // stakeDurationBlocks = big.NewInt(int64(blocksPerDay)) + stakeDurationDays = 91 + // blocksToWithdraw = 3 * blocksPerDay + minAmount = unit.ConvertIotxToRau(1000) + + tmpVotes = big.NewInt(0) + // tmpVotes2 = big.NewInt(0) + // tmpBalance = big.NewInt(0) + // tmpBkt = &iotextypes.VoteBucket{} + bktIdx uint64 + ) + bytecode, err := hex.DecodeString(stakingContractV2Bytecode) + r.NoError(err) + mustCallData := func(m string, args ...any) []byte { + data, err := abiCall(staking.StakingContractABI, m, args...) + r.NoError(err) + return data + } + contractAddress := "io137rkkuxzdgsfw0gdmae88gpq349vwlsljy2ycm" + test.run([]*testcase{ + { + name: "register candidate", + preFunc: func(*e2etest) { + fmt.Printf("newblock\n") + }, + acts: []*actionWithTime{ + {mustNoErr(action.SignedCandidateRegister(test.nonceMgr.pop(identityset.Address(candOwnerID).String()), "cand1", identityset.Address(1).String(), identityset.Address(1).String(), identityset.Address(candOwnerID).String(), registerAmount.String(), 1, true, nil, gasLimit, gasPrice, identityset.PrivateKey(candOwnerID), action.WithChainID(chainID))), time.Now()}, + }, + blockExpect: func(test *e2etest, blk *block.Block, err error) { + r.NoError(err) + }, + }, + { + name: "create stake", + preFunc: func(e *e2etest) { + candidate, err := e.getCandidateByName("cand1") + r.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + r.True(ok) + fmt.Printf("newblock\n") + }, + acts: []*actionWithTime{ + {mustNoErr(action.SignedCreateStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), "cand1", stakeAmount.String(), uint32(stakeDurationDays), true, nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), stakeTime}, + }, + blockExpect: func(test *e2etest, blk *block.Block, err error) { + candidate, err := test.getCandidateByName("cand1") + r.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationDays*24) * (time.Hour), StakedAmount: stakeAmount}, false) + r.Equal(tmpVotes.Add(tmpVotes, deltaVotes).String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, r, "cand1") + idxs := parseNativeStakedBucketIndex(blk.Receipts[0]) + r.Len(idxs, 1) + bktIdx = idxs[0] + bkt, err := test.getBucket(bktIdx, "") + r.NoError(err) + r.Equal(stakeAmount.String(), bkt.StakedAmount) + }, + }, + }) + test.run([]*testcase{ + { + name: "deposit to stake", + preFunc: func(e *e2etest) { + fmt.Printf("newblock\n") + }, + acts: []*actionWithTime{ + {mustNoErr(action.SignedDepositToStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), bktIdx, stakeAmount.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedDepositToStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), bktIdx, stakeAmount.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedExecution("", identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice, append(bytecode, mustCallData("", minAmount)...), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedDepositToStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), bktIdx, stakeAmount.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + }, + blockExpect: func(test *e2etest, blk *block.Block, err error) { + r.NoError(err) + r.Len(blk.Receipts, 5) + candidate, err := test.getCandidateByName("cand1") + r.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationDays*24) * (time.Hour), StakedAmount: new(big.Int).Mul(stakeAmount, big.NewInt(4))}, false) + deltaVotes.Sub(deltaVotes, staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationDays*24) * (time.Hour), StakedAmount: stakeAmount}, false)) + r.Equal(tmpVotes.Add(tmpVotes, deltaVotes).String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, r, "cand1") + bkt, err := test.getBucket(bktIdx, "") + r.NoError(err) + r.Equal(new(big.Int).Mul(stakeAmount, big.NewInt(4)).String(), bkt.StakedAmount) + r.EqualValues(iotextypes.ReceiptStatus_Success, blk.Receipts[2].Status) + r.Equal(contractAddress, blk.Receipts[2].ContractAddress) + }, + }, + { + name: "deposit to stake II", + preFunc: func(e *e2etest) { + fmt.Printf("newblock\n") + }, + acts: []*actionWithTime{ + {mustNoErr(action.SignedDepositToStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), bktIdx, stakeAmount.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedDepositToStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), bktIdx, stakeAmount.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + {mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(1), gasLimit, gasPrice, mustCallData("unlock(uint256)", big.NewInt(1)), action.WithChainID(chainID))), stakeTime}, + {mustNoErr(action.SignedDepositToStake(test.nonceMgr.pop(identityset.Address(stakerID).String()), bktIdx, stakeAmount.String(), nil, gasLimit, gasPrice, identityset.PrivateKey(stakerID), action.WithChainID(chainID))), time.Now()}, + }, + blockExpect: func(test *e2etest, blk *block.Block, err error) { + r.NoError(err) + r.Len(blk.Receipts, 5) + r.EqualValues(iotextypes.ReceiptStatus_ErrExecutionReverted, blk.Receipts[2].Status) + candidate, err := test.getCandidateByName("cand1") + r.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationDays*24) * (time.Hour), StakedAmount: new(big.Int).Mul(stakeAmount, big.NewInt(7))}, false) + deltaVotes.Sub(deltaVotes, staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationDays*24) * (time.Hour), StakedAmount: new(big.Int).Mul(stakeAmount, big.NewInt(4))}, false)) + r.Equal(tmpVotes.Add(tmpVotes, deltaVotes).String(), candidate.TotalWeightedVotes) + checkStakingVoteView(test, r, "cand1") + }, + }, + }) +} + +func parseNativeStakedBucketIndex(receipt *action.Receipt) []uint64 { + var bucketIndexes []uint64 + for _, log := range receipt.Logs() { + if log.Address == address.StakingProtocolAddr && len(log.Topics) > 1 { + bucketIndex := new(big.Int).SetBytes(log.Topics[1][:]) + bucketIndexes = append(bucketIndexes, bucketIndex.Uint64()) + } + } + return bucketIndexes +} diff --git a/misc/scripts/mockgen.sh b/misc/scripts/mockgen.sh index 2c0c0b0c1c..94f4eea8e7 100755 --- a/misc/scripts/mockgen.sh +++ b/misc/scripts/mockgen.sh @@ -164,6 +164,13 @@ mockgen -destination=./action/protocol/staking/contractstake_indexer_mock.go \ -package=staking \ ContractStakingIndexer +mkdir -p ./action/protocol/staking +mockgen -destination=./action/protocol/staking/contractstakeview_mock.go \ + -source=./action/protocol/staking/viewdata.go \ + -package=staking \ + ContractStakeView + + mkdir -p ./test/mock/mock_blockdao mockgen -destination=./test/mock/mock_blockdao/mock_blockindexer.go \ -package=mock_blockdao \ diff --git a/systemcontractindex/stakingindex/candidate_votes_test.go b/systemcontractindex/stakingindex/candidate_votes_test.go new file mode 100644 index 0000000000..ce1bd33a18 --- /dev/null +++ b/systemcontractindex/stakingindex/candidate_votes_test.go @@ -0,0 +1,62 @@ +package stakingindex + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/blockchain/genesis" +) + +func TestCandidateVotes(t *testing.T) { + require := require.New(t) + g := genesis.TestDefault() + t.Run("contract staking votes before Redsea", func(t *testing.T) { + blkHeight := g.QuebecBlockHeight + 1 + ctx := protocol.WithBlockCtx( + genesis.WithGenesisContext(context.Background(), g), + protocol.BlockCtx{ + BlockHeight: blkHeight, + }, + ) + ctx = protocol.WithFeatureCtx(ctx) + csVotes := newCandidateVotes() + cand := "candidate" + csVotes.Add(cand, big.NewInt(0), big.NewInt(0)) + originCandVotes := csVotes.Votes(protocol.MustGetFeatureCtx(ctx), cand) + if originCandVotes == nil { + originCandVotes = big.NewInt(0) + } + csVotes.Add(cand, big.NewInt(100), big.NewInt(120)) + newVotes := csVotes.Votes(protocol.MustGetFeatureCtx(ctx), cand) + if newVotes == nil { + newVotes = big.NewInt(0) + } + require.EqualValues(100, newVotes.Sub(newVotes, originCandVotes).Uint64()) + }) + t.Run("contract staking votes after Redsea", func(t *testing.T) { + blkHeight := g.RedseaBlockHeight + ctx := protocol.WithBlockCtx( + genesis.WithGenesisContext(context.Background(), g), + protocol.BlockCtx{ + BlockHeight: blkHeight, + }, + ) + ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) + cand := "candidate" + csVotes := newCandidateVotes() + originCandVotes := csVotes.Votes(protocol.MustGetFeatureCtx(ctx), cand) + if originCandVotes == nil { + originCandVotes = big.NewInt(0) + } + csVotes.Add(cand, big.NewInt(100), big.NewInt(120)) + newVotes := csVotes.Votes(protocol.MustGetFeatureCtx(ctx), cand) + if newVotes == nil { + newVotes = big.NewInt(0) + } + require.EqualValues(120, newVotes.Sub(newVotes, originCandVotes).Uint64()) + }) +} diff --git a/systemcontractindex/stakingindex/index.go b/systemcontractindex/stakingindex/index.go index b545af706f..19e6b98a45 100644 --- a/systemcontractindex/stakingindex/index.go +++ b/systemcontractindex/stakingindex/index.go @@ -170,7 +170,7 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s if err != nil { return nil, errors.Wrap(err, "failed to get state reader height") } - if srHeight != s.common.Height() { + if s.common.StartHeight() <= srHeight && srHeight != s.common.Height() { return nil, errors.New("state reader height does not match indexer height") } cfg := &VoteViewConfig{ From 89f50e4b2e18c353cd207639abb259f055f401c7 Mon Sep 17 00:00:00 2001 From: zhi Date: Mon, 29 Sep 2025 14:44:33 +0800 Subject: [PATCH 03/12] refactor view --- .../staking/contractstakeview_mock.go | 12 +-- action/protocol/staking/protocol.go | 81 +------------------ action/protocol/staking/viewdata.go | 39 ++++----- blockindex/contractstaking/indexer.go | 2 +- e2etest/rewarding_test.go | 2 + e2etest/staking_test.go | 2 + systemcontractindex/stakingindex/index.go | 2 +- systemcontractindex/stakingindex/voteview.go | 41 ++++++++-- 8 files changed, 66 insertions(+), 115 deletions(-) diff --git a/action/protocol/staking/contractstakeview_mock.go b/action/protocol/staking/contractstakeview_mock.go index 003199b4c3..b50bf487ce 100644 --- a/action/protocol/staking/contractstakeview_mock.go +++ b/action/protocol/staking/contractstakeview_mock.go @@ -127,17 +127,17 @@ func (mr *MockContractStakeViewMockRecorder) Commit(arg0, arg1 any) *gomock.Call } // CreatePreStates mocks base method. -func (m *MockContractStakeView) CreatePreStates(ctx context.Context, br BucketReader) error { +func (m *MockContractStakeView) CreatePreStates(ctx context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePreStates", ctx, br) + ret := m.ctrl.Call(m, "CreatePreStates", ctx) ret0, _ := ret[0].(error) return ret0 } // CreatePreStates indicates an expected call of CreatePreStates. -func (mr *MockContractStakeViewMockRecorder) CreatePreStates(ctx, br any) *gomock.Call { +func (mr *MockContractStakeViewMockRecorder) CreatePreStates(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePreStates", reflect.TypeOf((*MockContractStakeView)(nil).CreatePreStates), ctx, br) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePreStates", reflect.TypeOf((*MockContractStakeView)(nil).CreatePreStates), ctx) } // Fork mocks base method. @@ -183,7 +183,7 @@ func (mr *MockContractStakeViewMockRecorder) IsDirty() *gomock.Call { } // Migrate mocks base method. -func (m *MockContractStakeView) Migrate(arg0 EventHandler, arg1 map[uint64]*contractstaking.Bucket) error { +func (m *MockContractStakeView) Migrate(arg0 context.Context, arg1 EventHandler) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Migrate", arg0, arg1) ret0, _ := ret[0].(error) @@ -197,7 +197,7 @@ func (mr *MockContractStakeViewMockRecorder) Migrate(arg0, arg1 any) *gomock.Cal } // Revise mocks base method. -func (m *MockContractStakeView) Revise(arg0 map[uint64]*contractstaking.Bucket) { +func (m *MockContractStakeView) Revise(arg0 context.Context) { m.ctrl.T.Helper() m.ctrl.Call(m, "Revise", arg0) } diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 5deea9dfcd..fd00d77e79 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -479,16 +479,6 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager return err } vd := v.(*viewData) - indexers := []ContractStakingIndexer{} - if p.contractStakingIndexer != nil { - indexers = append(indexers, p.contractStakingIndexer) - } - if p.contractStakingIndexerV2 != nil { - indexers = append(indexers, p.contractStakingIndexerV2) - } - if p.contractStakingIndexerV3 != nil { - indexers = append(indexers, p.contractStakingIndexerV3) - } if blkCtx.BlockHeight == g.ToBeEnabledBlockHeight { handler, err := newNFTBucketEventHandler(sm, func(bucket *contractstaking.Bucket, height uint64) *big.Int { vb := p.convertToVoteBucket(bucket, height) @@ -497,42 +487,16 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager if err != nil { return err } - buckets := make([]map[uint64]*contractstaking.Bucket, 3) - for i, indexer := range indexers { - h, bs, err := indexer.ContractStakingBuckets() - if err != nil { - return err - } - if indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { - return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) - } - buckets[i] = bs - } - if err := vd.contractsStake.Migrate(handler, buckets); err != nil { + if err := vd.contractsStake.Migrate(ctx, handler); err != nil { return errors.Wrap(err, "failed to flush buckets for contract staking") } } if featureCtx.StoreVoteOfNFTBucketIntoView { - brs := make([]BucketReader, len(indexers)) - for i, indexer := range indexers { - brs[i] = indexer - } - if err := vd.contractsStake.CreatePreStates(ctx, brs); err != nil { + if err := vd.contractsStake.CreatePreStates(ctx); err != nil { return err } if blkCtx.BlockHeight == g.WakeBlockHeight { - buckets := make([]map[uint64]*contractstaking.Bucket, 3) - for i, indexer := range indexers { - h, bs, err := indexer.ContractStakingBuckets() - if err != nil { - return err - } - if indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { - return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) - } - buckets[i] = bs - } - vd.contractsStake.Revise(buckets) + vd.contractsStake.Revise(ctx) } } @@ -1072,32 +1036,6 @@ func (p *Protocol) needToWriteCandsMap(ctx context.Context, height uint64) bool return height >= p.config.PersistStakingPatchBlock && fCtx.CandCenterHasAlias(height) } -func (p *Protocol) contractStakingVotesFromIndexer(ctx context.Context, candidate address.Address, height uint64) (*big.Int, error) { - featureCtx := protocol.MustGetFeatureCtx(ctx) - votes := big.NewInt(0) - indexers := []ContractStakingIndexer{} - if p.contractStakingIndexer != nil && featureCtx.AddContractStakingVotes { - indexers = append(indexers, p.contractStakingIndexer) - } - if p.contractStakingIndexerV2 != nil && !featureCtx.LimitedStakingContract { - indexers = append(indexers, p.contractStakingIndexerV2) - } - if p.contractStakingIndexerV3 != nil && featureCtx.TimestampedStakingContract { - indexers = append(indexers, p.contractStakingIndexerV3) - } - - for _, indexer := range indexers { - btks, err := indexer.BucketsByCandidate(candidate, height) - if err != nil { - return nil, errors.Wrap(err, "failed to get BucketsByCandidate from contractStakingIndexer") - } - for _, b := range btks { - votes.Add(votes, p.contractBucketVotes(featureCtx, b)) - } - } - return votes, nil -} - func (p *Protocol) contractStakingVotesFromView(ctx context.Context, candidate address.Address, view *viewData) (*big.Int, error) { featureCtx := protocol.MustGetFeatureCtx(ctx) votes := big.NewInt(0) @@ -1121,19 +1059,6 @@ func (p *Protocol) contractStakingVotesFromView(ctx context.Context, candidate a return votes, nil } -func (p *Protocol) contractBucketVotes(fCtx protocol.FeatureCtx, bkt *VoteBucket) *big.Int { - votes := big.NewInt(0) - if bkt.isUnstaked() { - return votes - } - if fCtx.FixContractStakingWeightedVotes { - votes.Add(votes, p.calculateVoteWeight(bkt, false)) - } else { - votes.Add(votes, bkt.StakedAmount) - } - return votes -} - func readCandCenterStateFromStateDB(sr protocol.StateReader) (CandidateList, CandidateList, CandidateList, error) { var ( name, operator, owner CandidateList diff --git a/action/protocol/staking/viewdata.go b/action/protocol/staking/viewdata.go index 901c017397..1cb7393b3f 100644 --- a/action/protocol/staking/viewdata.go +++ b/action/protocol/staking/viewdata.go @@ -34,13 +34,13 @@ type ( // Commit commits the contract stake view Commit(context.Context, protocol.StateManager) error // CreatePreStates creates pre states for the contract stake view - CreatePreStates(ctx context.Context, br BucketReader) error + CreatePreStates(ctx context.Context) error // Handle handles the receipt for the contract stake view Handle(ctx context.Context, receipt *action.Receipt) error // Migrate writes the bucket types and buckets to the state manager - Migrate(EventHandler, map[uint64]*contractstaking.Bucket) error + Migrate(context.Context, EventHandler) error // Revise updates the contract stake view with the latest bucket data - Revise(map[uint64]*contractstaking.Bucket) + Revise(context.Context) // BucketsByCandidate returns the buckets by candidate address CandidateStakeVotes(ctx context.Context, id address.Address) *big.Int AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error @@ -135,37 +135,31 @@ func (v *viewData) Revert(snapshot int) error { return nil } -func (csv *contractStakeView) Revise(buckets []map[uint64]*contractstaking.Bucket) { - idx := 0 +func (csv *contractStakeView) Revise(ctx context.Context) { if csv.v1 != nil { - csv.v1.Revise(buckets[idx]) - idx++ + csv.v1.Revise(ctx) } if csv.v2 != nil { - csv.v2.Revise(buckets[idx]) - idx++ + csv.v2.Revise(ctx) } if csv.v3 != nil { - csv.v3.Revise(buckets[idx]) + csv.v3.Revise(ctx) } } -func (csv *contractStakeView) Migrate(nftHandler EventHandler, buckets []map[uint64]*contractstaking.Bucket) error { - idx := 0 +func (csv *contractStakeView) Migrate(ctx context.Context, nftHandler EventHandler) error { if csv.v1 != nil { - if err := csv.v1.Migrate(nftHandler, buckets[idx]); err != nil { + if err := csv.v1.Migrate(ctx, nftHandler); err != nil { return err } - idx++ } if csv.v2 != nil { - if err := csv.v2.Migrate(nftHandler, buckets[idx]); err != nil { + if err := csv.v2.Migrate(ctx, nftHandler); err != nil { return err } - idx++ } if csv.v3 != nil { - if err := csv.v3.Migrate(nftHandler, buckets[idx]); err != nil { + if err := csv.v3.Migrate(ctx, nftHandler); err != nil { return err } } @@ -206,22 +200,19 @@ func (csv *contractStakeView) Fork() *contractStakeView { return clone } -func (csv *contractStakeView) CreatePreStates(ctx context.Context, brs []BucketReader) error { - idx := 0 +func (csv *contractStakeView) CreatePreStates(ctx context.Context) error { if csv.v1 != nil { - if err := csv.v1.CreatePreStates(ctx, brs[idx]); err != nil { + if err := csv.v1.CreatePreStates(ctx); err != nil { return err } - idx++ } if csv.v2 != nil { - if err := csv.v2.CreatePreStates(ctx, brs[idx]); err != nil { + if err := csv.v2.CreatePreStates(ctx); err != nil { return err } - idx++ } if csv.v3 != nil { - if err := csv.v3.CreatePreStates(ctx, brs[idx]); err != nil { + if err := csv.v3.CreatePreStates(ctx); err != nil { return err } } diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index aa9a0ff0a8..619d254336 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -132,7 +132,7 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s }) processorBuilder := newEventProcessorBuilder(s.contractAddr) cfg := &stakingindex.VoteViewConfig{ContractAddr: s.contractAddr} - return stakingindex.NewVoteView(cfg, s.height, cur, processorBuilder, calculateUnmutedVoteWeightAt), nil + return stakingindex.NewVoteView(s, cfg, s.height, cur, processorBuilder, calculateUnmutedVoteWeightAt), nil } // Stop stops the indexer diff --git a/e2etest/rewarding_test.go b/e2etest/rewarding_test.go index 4a7e34aba4..4b8c2a077f 100644 --- a/e2etest/rewarding_test.go +++ b/e2etest/rewarding_test.go @@ -69,6 +69,8 @@ func TestBlockReward(t *testing.T) { cfg.Genesis = genesis.TestDefault() initDBPaths(r, &cfg) defer func() { clearDBPaths(&cfg) }() + cfg.API.GRPCPort = 0 + cfg.API.HTTPPort = 0 cfg.Consensus.Scheme = config.RollDPoSScheme cfg.Genesis.NumDelegates = 1 cfg.Genesis.NumSubEpochs = 10 diff --git a/e2etest/staking_test.go b/e2etest/staking_test.go index 11e218c76c..23e83ed8d3 100644 --- a/e2etest/staking_test.go +++ b/e2etest/staking_test.go @@ -196,6 +196,8 @@ func TestStakingContract(t *testing.T) { delete(cfg.Plugins, config.GatewayPlugin) }() + cfg.API.GRPCPort = 0 + cfg.API.HTTPPort = 0 cfg.ActPool.MinGasPriceStr = "0" cfg.Chain.TrieDBPatchFile = "" cfg.Chain.ProducerPrivKey = "a000000000000000000000000000000000000000000000000000000000000000" diff --git a/systemcontractindex/stakingindex/index.go b/systemcontractindex/stakingindex/index.go index 19e6b98a45..741b63f74f 100644 --- a/systemcontractindex/stakingindex/index.go +++ b/systemcontractindex/stakingindex/index.go @@ -177,7 +177,7 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s ContractAddr: s.common.ContractAddress(), } processorBuilder := newEventProcessorBuilder(s.common.ContractAddress(), s.timestamped, s.muteHeight) - return NewVoteView(cfg, s.common.Height(), s.createCandidateVotes(s.cache.buckets), processorBuilder, s.calculateContractVoteWeight), nil + return NewVoteView(s, cfg, s.common.Height(), s.createCandidateVotes(s.cache.buckets), processorBuilder, s.calculateContractVoteWeight), nil } // ContractStakingBuckets returns all the contract staking buckets diff --git a/systemcontractindex/stakingindex/voteview.go b/systemcontractindex/stakingindex/voteview.go index ba5802ce54..283bfb298c 100644 --- a/systemcontractindex/stakingindex/voteview.go +++ b/systemcontractindex/stakingindex/voteview.go @@ -25,6 +25,7 @@ type ( Build(context.Context, staking.EventHandler) staking.EventProcessor } voteView struct { + indexer staking.ContractStakingIndexer config *VoteViewConfig height uint64 cur CandidateVotes @@ -35,17 +36,21 @@ type ( ) // NewVoteView creates a new vote view -func NewVoteView(cfg *VoteViewConfig, +func NewVoteView( + indexer staking.ContractStakingIndexer, + cfg *VoteViewConfig, height uint64, cur CandidateVotes, processorBuilder EventProcessorBuilder, fn CalculateUnmutedVoteWeightAtFn, ) staking.ContractStakeView { return &voteView{ + indexer: indexer, config: cfg, height: height, cur: cur, processorBuilder: processorBuilder, + store: newBucketStore(indexer), calculateVoteWeightFn: fn, } } @@ -61,6 +66,7 @@ func (s *voteView) Wrap() staking.ContractStakeView { store = newBucketStore(s.store) } return &voteView{ + indexer: s.indexer, config: s.config, height: s.height, cur: cur, @@ -77,6 +83,7 @@ func (s *voteView) Fork() staking.ContractStakeView { store = newBucketStore(s.store) } return &voteView{ + indexer: s.indexer, config: s.config, height: s.height, cur: cur, @@ -90,7 +97,28 @@ func (s *voteView) IsDirty() bool { return s.cur.IsDirty() } -func (s *voteView) Migrate(handler staking.EventHandler, buckets map[uint64]*contractstaking.Bucket) error { +func (s *voteView) buckets(ctx context.Context) (map[uint64]*contractstaking.Bucket, error) { + h, buckets, err := s.indexer.ContractStakingBuckets() + if err != nil { + return nil, err + } + blkCtx := protocol.MustGetBlockCtx(ctx) + if s.indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { + return nil, errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) + } + return buckets, nil +} + +func (s *voteView) Migrate(ctx context.Context, handler staking.EventHandler) error { + h, buckets, err := s.indexer.ContractStakingBuckets() + if err != nil { + return err + } + blkCtx := protocol.MustGetBlockCtx(ctx) + if s.indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { + return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) + } + for id := range buckets { if err := handler.PutBucket(s.config.ContractAddr, id, buckets[id]); err != nil { return err @@ -99,7 +127,11 @@ func (s *voteView) Migrate(handler staking.EventHandler, buckets map[uint64]*con return nil } -func (s *voteView) Revise(buckets map[uint64]*contractstaking.Bucket) { +func (s *voteView) Revise(ctx context.Context) { + buckets, err := s.buckets(ctx) + if err != nil { + return + } s.cur = AggregateCandidateVotes(buckets, func(b *contractstaking.Bucket) *big.Int { return s.calculateVoteWeightFn(b, s.height) }) @@ -113,10 +145,9 @@ func (s *voteView) CandidateStakeVotes(ctx context.Context, candidate address.Ad return s.cur.Votes(featureCtx, candidate.String()) } -func (s *voteView) CreatePreStates(ctx context.Context, br BucketReader) error { +func (s *voteView) CreatePreStates(ctx context.Context) error { blkCtx := protocol.MustGetBlockCtx(ctx) s.height = blkCtx.BlockHeight - s.store = newBucketStore(br) return nil } From 440a17e7b2544e15185e3241e5a34ff0d603f9cb Mon Sep 17 00:00:00 2001 From: zhi Date: Mon, 29 Sep 2025 16:27:28 +0800 Subject: [PATCH 04/12] address comment --- systemcontractindex/stakingindex/voteview.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systemcontractindex/stakingindex/voteview.go b/systemcontractindex/stakingindex/voteview.go index 283bfb298c..d222cc5b30 100644 --- a/systemcontractindex/stakingindex/voteview.go +++ b/systemcontractindex/stakingindex/voteview.go @@ -50,7 +50,6 @@ func NewVoteView( height: height, cur: cur, processorBuilder: processorBuilder, - store: newBucketStore(indexer), calculateVoteWeightFn: fn, } } @@ -148,6 +147,7 @@ func (s *voteView) CandidateStakeVotes(ctx context.Context, candidate address.Ad func (s *voteView) CreatePreStates(ctx context.Context) error { blkCtx := protocol.MustGetBlockCtx(ctx) s.height = blkCtx.BlockHeight + s.store = newBucketStore(s.indexer) return nil } From 5f6a019558f4c48d42f4fd9af3ee001f09b95e1e Mon Sep 17 00:00:00 2001 From: zhi Date: Mon, 29 Sep 2025 19:47:49 +0800 Subject: [PATCH 05/12] add unit test and fix a log error --- action/protocol/staking/protocol.go | 2 +- action/protocol/staking/protocol_test.go | 53 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index fd00d77e79..8f03fd1d01 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -299,7 +299,7 @@ func (p *Protocol) Start(ctx context.Context, sr protocol.StateReader) (protocol } view, err := NewContractStakeViewBuilder(indexer, p.blockStore).Build(ctx, sr, height) if err != nil { - errChan <- errors.Wrapf(err, "failed to create stake view for contract %s", p.contractStakingIndexer.ContractAddress()) + errChan <- errors.Wrapf(err, "failed to create stake view for contract %s", indexer.ContractAddress()) return } callback(view) diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index e620a6c7c0..94fb4c4a1b 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -314,6 +314,59 @@ func Test_CreatePreStatesWithRegisterProtocol(t *testing.T) { require.NoError(p.CreatePreStates(ctx, sm)) } +func TestCreatePreStatesMigration(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + sm := testdb.NewMockStateManager(ctrl) + g := genesis.TestDefault() + mockView := NewMockContractStakeView(ctrl) + mockContractStaking := NewMockContractStakingIndexer(ctrl) + mockContractStaking.EXPECT().ContractAddress().Return(identityset.Address(1)).Times(1) + mockContractStaking.EXPECT().LoadStakeView(gomock.Any(), gomock.Any()).Return(mockView, nil).Times(1) + mockContractStaking.EXPECT().StartHeight().Return(uint64(0)).Times(1) + mockContractStaking.EXPECT().Height().Return(uint64(0), nil).Times(1) + p, err := NewProtocol(HelperCtx{ + DepositGas: nil, + BlockInterval: getBlockInterval, + }, &BuilderConfig{ + Staking: g.Staking, + PersistStakingPatchBlock: math.MaxUint64, + SkipContractStakingViewHeight: math.MaxUint64, + Revise: ReviseConfig{ + VoteWeight: g.Staking.VoteWeightCalConsts, + ReviseHeights: []uint64{g.GreenlandBlockHeight}}, + }, nil, nil, nil, mockContractStaking) + require.NoError(err) + ctx := genesis.WithGenesisContext(context.Background(), g) + ctx = protocol.WithBlockCtx( + ctx, + protocol.BlockCtx{ + BlockHeight: g.ToBeEnabledBlockHeight, + }, + ) + ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) + v, err := p.Start(ctx, sm) + require.NoError(err) + require.NoError(sm.WriteView(_protocolID, v)) + mockView.EXPECT().Migrate(gomock.Any(), gomock.Any()).Return(errors.New("migration error")).Times(1) + require.ErrorContains(p.CreatePreStates(ctx, sm), "migration error") + mockView.EXPECT().Migrate(gomock.Any(), gomock.Any()).Return(nil).Times(1) + require.NoError(p.CreatePreStates(ctx, sm)) + require.NoError(p.CreatePreStates(protocol.WithBlockCtx( + ctx, + protocol.BlockCtx{ + BlockHeight: g.ToBeEnabledBlockHeight - 1, + }, + ), sm)) + require.NoError(p.CreatePreStates(protocol.WithBlockCtx( + ctx, + protocol.BlockCtx{ + BlockHeight: g.ToBeEnabledBlockHeight + 1, + }, + ), sm)) +} + func Test_CreateGenesisStates(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) From 0a53f236e5178d23b8978f5b21382b07d29c6e45 Mon Sep 17 00:00:00 2001 From: envestcc Date: Wed, 17 Sep 2025 18:38:12 +0800 Subject: [PATCH 06/12] fix build --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 24e4e3f2d2..56098b1868 100644 --- a/Makefile +++ b/Makefile @@ -256,7 +256,7 @@ recover: .PHONY: ioctl ioctl: - $(GOBUILD) -ldflags "$(PackageFlags)" -o ./bin/$(BUILD_TARGET_IOCTL) -v ./tools/ioctl + $(GOBUILD) -tags $(BUILD_TAGS) -ldflags "$(PackageFlags)" -o ./bin/$(BUILD_TARGET_IOCTL) -v ./tools/ioctl .PHONY: newioctl newioctl: From 0cdc9dedae303fcae2e3bbf270ba7f34cd70726b Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 30 Sep 2025 12:14:39 +0800 Subject: [PATCH 07/12] add migrate unit test --- .../staking/contractstaking/bucket.go | 2 +- .../stakingindex/voteview_test.go | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 systemcontractindex/stakingindex/voteview_test.go diff --git a/action/protocol/staking/contractstaking/bucket.go b/action/protocol/staking/contractstaking/bucket.go index 703becee88..99c19ff76d 100644 --- a/action/protocol/staking/contractstaking/bucket.go +++ b/action/protocol/staking/contractstaking/bucket.go @@ -23,7 +23,7 @@ type ( StakedDuration uint64 // in seconds if timestamped, in block number if not // CreatedAt is the time when the bucket was created. CreatedAt uint64 // in unix timestamp if timestamped, in block height if not - // UnlockedAt is the time when the bucket can be unlocked. + // UnlockedAt is the time when the bucket was unlocked. UnlockedAt uint64 // in unix timestamp if timestamped, in block height if not // UnstakedAt is the time when the bucket was unstaked. UnstakedAt uint64 // in unix timestamp if timestamped, in block height if not diff --git a/systemcontractindex/stakingindex/voteview_test.go b/systemcontractindex/stakingindex/voteview_test.go new file mode 100644 index 0000000000..4110747749 --- /dev/null +++ b/systemcontractindex/stakingindex/voteview_test.go @@ -0,0 +1,76 @@ +package stakingindex + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" + "github.com/iotexproject/iotex-core/v2/test/identityset" +) + +func TestVoteView(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIndexer := staking.NewMockContractStakingIndexer(ctrl) + vv := NewVoteView( + mockIndexer, + &VoteViewConfig{ + ContractAddr: identityset.Address(0), + }, + 100, + nil, + nil, + func(b *contractstaking.Bucket, h uint64) *big.Int { + return big.NewInt(1) + }, + ) + mockHandler := staking.NewMockEventHandler(ctrl) + t.Run("Migrate", func(t *testing.T) { + buckets := make(map[uint64]*contractstaking.Bucket) + buckets[1] = &contractstaking.Bucket{ + Candidate: identityset.Address(1), + Owner: identityset.Address(11), + StakedAmount: big.NewInt(1000000000000000000), + StakedDuration: 10, + CreatedAt: 123456, + UnlockedAt: 0, + UnstakedAt: 0, + IsTimestampBased: false, + Muted: false, + } + buckets[2] = &contractstaking.Bucket{ + Candidate: identityset.Address(2), + Owner: identityset.Address(12), + StakedAmount: big.NewInt(1000000000000000000), + StakedDuration: 10, + CreatedAt: 123456, + UnlockedAt: 234567, + UnstakedAt: 0, + IsTimestampBased: true, + Muted: false, + } + migratedBuckets := make(map[uint64]*contractstaking.Bucket) + ctx := protocol.WithBlockCtx(context.Background(), protocol.BlockCtx{ + BlockHeight: 100, + }) + mockIndexer.EXPECT().StartHeight().Return(uint64(0)).Times(1) + mockIndexer.EXPECT().ContractStakingBuckets().Return(uint64(99), buckets, nil) + mockHandler.EXPECT().PutBucket(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(addr address.Address, id uint64, bucket *contractstaking.Bucket) error { + require.Equal(identityset.Address(0), addr) + migratedBuckets[id] = bucket + return nil + }).Times(len(buckets)) + require.NoError(vv.Migrate(ctx, mockHandler)) + require.Equal(buckets, migratedBuckets) + }) +} From 5a6fff9e2f42e3e5ae9b4d03866be26379fdebc4 Mon Sep 17 00:00:00 2001 From: envestcc Date: Sun, 28 Sep 2025 23:29:50 +0800 Subject: [PATCH 08/12] store candidate votes in erigondb --- blockindex/contractstaking/indexer.go | 3 +- state/factory/erigonstore/registry.go | 1 + state/factory/statedb.go | 18 ++++-- state/tables.go | 4 ++ .../stakingindex/candidate_votes.go | 13 ++++ .../stakingindex/candidate_votes_manager.go | 60 +++++++++++++++++++ systemcontractindex/stakingindex/index.go | 3 +- systemcontractindex/stakingindex/voteview.go | 7 ++- 8 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 systemcontractindex/stakingindex/candidate_votes_manager.go diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index 619d254336..51653f43e2 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -132,7 +132,8 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s }) processorBuilder := newEventProcessorBuilder(s.contractAddr) cfg := &stakingindex.VoteViewConfig{ContractAddr: s.contractAddr} - return stakingindex.NewVoteView(s, cfg, s.height, cur, processorBuilder, calculateUnmutedVoteWeightAt), nil + mgr := stakingindex.NewCandidateVotesManager(s.ContractAddress()) + return stakingindex.NewVoteView(s, cfg, s.height, cur, processorBuilder, mgr, calculateUnmutedVoteWeightAt), nil } // Stop stops the indexer diff --git a/state/factory/erigonstore/registry.go b/state/factory/erigonstore/registry.go index b7df0b71c1..f9b6f0384a 100644 --- a/state/factory/erigonstore/registry.go +++ b/state/factory/erigonstore/registry.go @@ -37,6 +37,7 @@ func init() { assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandidateNamespace, CandidatesContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandsMapNamespace, CandidateMapContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingNamespace, BucketPoolContractIndex)) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingViewNamespace, StakingViewContractIndex)) assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.AccountKVNamespace, &state.Account{}, AccountIndex)) assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.AccountKVNamespace, &state.CandidateList{}, PollLegacyCandidateListContractIndex)) diff --git a/state/factory/statedb.go b/state/factory/statedb.go index 0db3aa5811..0351edf7bc 100644 --- a/state/factory/statedb.go +++ b/state/factory/statedb.go @@ -8,6 +8,7 @@ package factory import ( "context" "fmt" + "slices" "strconv" "sync" "time" @@ -567,15 +568,22 @@ func (sdb *stateDB) flusherOptions(preEaster bool) []db.KVStoreFlusherOption { return wi.Serialize() }), } - if !preEaster { - return opts + serializeFilterNs := []string{state.StakingViewNamespace} + if preEaster { + serializeFilterNs = append(serializeFilterNs, evm.CodeKVNameSpace, staking.CandsMapNS) } - return append( - opts, + opts = append(opts, + db.FlushTranslateOption(func(wi *batch.WriteInfo) *batch.WriteInfo { + if wi.Namespace() == state.StakingViewNamespace { + return nil + } + return wi + }), db.SerializeFilterOption(func(wi *batch.WriteInfo) bool { - return wi.Namespace() == evm.CodeKVNameSpace || wi.Namespace() == staking.CandsMapNS + return slices.Contains(serializeFilterNs, wi.Namespace()) }), ) + return opts } func (sdb *stateDB) state(h uint64, ns string, addr []byte, s interface{}) error { diff --git a/state/tables.go b/state/tables.go index 97e9486d84..4a3e944c3c 100644 --- a/state/tables.go +++ b/state/tables.go @@ -38,6 +38,10 @@ const ( // - "4" + --> Endorsement StakingNamespace = "Staking" + // StakingViewNamespace is the namespace to store staking view information + // - "voteview" + --> CandidateVotes + StakingViewNamespace = "StakingView" + // CandidateNamespace is the namespace to store candidate information // - --> Candidate CandidateNamespace = "Candidate" diff --git a/systemcontractindex/stakingindex/candidate_votes.go b/systemcontractindex/stakingindex/candidate_votes.go index ddd3b0bdd5..8fc8b8dc91 100644 --- a/systemcontractindex/stakingindex/candidate_votes.go +++ b/systemcontractindex/stakingindex/candidate_votes.go @@ -8,6 +8,7 @@ import ( "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/systemcontractindex/stakingindex/stakingpb" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) // CandidateVotes is the interface to manage candidate votes @@ -125,6 +126,18 @@ func (cv *candidateVotes) Deserialize(data []byte) error { return nil } +func (cv *candidateVotes) Encode() (systemcontracts.GenericValue, error) { + data, err := cv.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, err + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +func (cv *candidateVotes) Decode(data systemcontracts.GenericValue) error { + return cv.Deserialize(data.PrimaryData) +} + func (cv *candidateVotes) Commit() CandidateVotes { return cv } diff --git a/systemcontractindex/stakingindex/candidate_votes_manager.go b/systemcontractindex/stakingindex/candidate_votes_manager.go new file mode 100644 index 0000000000..d217735bf6 --- /dev/null +++ b/systemcontractindex/stakingindex/candidate_votes_manager.go @@ -0,0 +1,60 @@ +package stakingindex + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/state" +) + +var ( + voteViewKeyPrefix = []byte("voteview") + voteViewNS = state.StakingViewNamespace +) + +// CandidateVotesManager defines the interface to manage candidate votes +type CandidateVotesManager interface { + Load(ctx context.Context, sr protocol.StateReader) (CandidateVotes, error) + Store(ctx context.Context, sm protocol.StateManager, candVotes CandidateVotes) error +} + +type candidateVotesManager struct { + contractAddr address.Address +} + +// NewCandidateVotesManager creates a new instance of CandidateVotesManager +func NewCandidateVotesManager(contractAddr address.Address) CandidateVotesManager { + return &candidateVotesManager{ + contractAddr: contractAddr, + } +} + +func (s *candidateVotesManager) Store(ctx context.Context, sm protocol.StateManager, candVotes CandidateVotes) error { + if _, err := sm.PutState(candVotes, + protocol.KeyOption(s.key()), + protocol.NamespaceOption(voteViewNS), + ); err != nil { + return errors.Wrap(err, "failed to put candidate votes state") + } + return nil +} + +func (s *candidateVotesManager) Load(ctx context.Context, sr protocol.StateReader) (CandidateVotes, error) { + cur := newCandidateVotes() + _, err := sr.State(cur, + protocol.KeyOption(s.key()), + protocol.NamespaceOption(voteViewNS), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to get candidate votes state") + } + return cur, nil +} + +func (s *candidateVotesManager) key() []byte { + return append(voteViewKeyPrefix, s.contractAddr.Bytes()...) +} diff --git a/systemcontractindex/stakingindex/index.go b/systemcontractindex/stakingindex/index.go index 741b63f74f..5a706d14f1 100644 --- a/systemcontractindex/stakingindex/index.go +++ b/systemcontractindex/stakingindex/index.go @@ -176,8 +176,9 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s cfg := &VoteViewConfig{ ContractAddr: s.common.ContractAddress(), } + mgr := NewCandidateVotesManager(s.ContractAddress()) processorBuilder := newEventProcessorBuilder(s.common.ContractAddress(), s.timestamped, s.muteHeight) - return NewVoteView(s, cfg, s.common.Height(), s.createCandidateVotes(s.cache.buckets), processorBuilder, s.calculateContractVoteWeight), nil + return NewVoteView(s, cfg, s.common.Height(), s.createCandidateVotes(s.cache.buckets), processorBuilder, mgr, s.calculateContractVoteWeight), nil } // ContractStakingBuckets returns all the contract staking buckets diff --git a/systemcontractindex/stakingindex/voteview.go b/systemcontractindex/stakingindex/voteview.go index d222cc5b30..7931ab4e75 100644 --- a/systemcontractindex/stakingindex/voteview.go +++ b/systemcontractindex/stakingindex/voteview.go @@ -30,6 +30,7 @@ type ( height uint64 cur CandidateVotes store BucketStore + cvm CandidateVotesManager processorBuilder EventProcessorBuilder calculateVoteWeightFn CalculateUnmutedVoteWeightAtFn } @@ -42,6 +43,7 @@ func NewVoteView( height uint64, cur CandidateVotes, processorBuilder EventProcessorBuilder, + cvm CandidateVotesManager, fn CalculateUnmutedVoteWeightAtFn, ) staking.ContractStakeView { return &voteView{ @@ -50,6 +52,7 @@ func NewVoteView( height: height, cur: cur, processorBuilder: processorBuilder, + cvm: cvm, calculateVoteWeightFn: fn, } } @@ -71,6 +74,7 @@ func (s *voteView) Wrap() staking.ContractStakeView { cur: cur, store: store, processorBuilder: s.processorBuilder, + cvm: s.cvm, calculateVoteWeightFn: s.calculateVoteWeightFn, } } @@ -88,6 +92,7 @@ func (s *voteView) Fork() staking.ContractStakeView { cur: cur, store: store, processorBuilder: s.processorBuilder, + cvm: s.cvm, calculateVoteWeightFn: s.calculateVoteWeightFn, } } @@ -167,5 +172,5 @@ func (s *voteView) AddBlockReceipts(ctx context.Context, receipts []*action.Rece func (s *voteView) Commit(ctx context.Context, sm protocol.StateManager) error { s.cur = s.cur.Commit() - return nil + return s.cvm.Store(ctx, sm, s.cur) } From 65630749cecd6e6b4d62b9f068cc6f425b2f26af Mon Sep 17 00:00:00 2001 From: zhi Date: Sun, 28 Sep 2025 12:04:20 +0800 Subject: [PATCH 09/12] update matchContractIndex and move registrations into erigonstore --- state/factory/erigonstore/registry.go | 1 + 1 file changed, 1 insertion(+) diff --git a/state/factory/erigonstore/registry.go b/state/factory/erigonstore/registry.go index f9b6f0384a..7ea96d38ac 100644 --- a/state/factory/erigonstore/registry.go +++ b/state/factory/erigonstore/registry.go @@ -38,6 +38,7 @@ func init() { assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandsMapNamespace, CandidateMapContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingNamespace, BucketPoolContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingViewNamespace, StakingViewContractIndex)) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingNamespace, BucketPoolContractIndex)) assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.AccountKVNamespace, &state.Account{}, AccountIndex)) assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.AccountKVNamespace, &state.CandidateList{}, PollLegacyCandidateListContractIndex)) From 8458a59ee43ebf8b81e99126b8a5e70c9fb202de Mon Sep 17 00:00:00 2001 From: envestcc Date: Mon, 29 Sep 2025 00:32:54 +0800 Subject: [PATCH 10/12] store contract buckets in erigondb --- .../staking/contractstaking/bucket.go | 18 ++++++- .../staking/contractstaking/bucket_type.go | 18 ++++++- .../staking/contractstaking/contract.go | 18 ++++++- .../staking/contractstaking/statereader.go | 9 ++-- action/protocol/staking/protocol.go | 4 +- state/factory/erigonstore/registry.go | 48 +++++++++++++++---- state/factory/statedb.go | 35 ++++++++++++-- state/tables.go | 9 ++++ 8 files changed, 138 insertions(+), 21 deletions(-) diff --git a/action/protocol/staking/contractstaking/bucket.go b/action/protocol/staking/contractstaking/bucket.go index 99c19ff76d..fb4ffc4dec 100644 --- a/action/protocol/staking/contractstaking/bucket.go +++ b/action/protocol/staking/contractstaking/bucket.go @@ -4,9 +4,11 @@ import ( "math/big" "github.com/iotexproject/iotex-address/address" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/pkg/errors" "google.golang.org/protobuf/proto" + + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) type ( @@ -113,3 +115,17 @@ func (b *Bucket) Clone() *Bucket { Muted: b.Muted, } } + +// Encode encodes the bucket into a GenericValue +func (b *Bucket) Encode() (systemcontracts.GenericValue, error) { + data, err := b.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize bucket") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes the bucket from a GenericValue +func (b *Bucket) Decode(gv systemcontracts.GenericValue) error { + return b.Deserialize(gv.PrimaryData) +} diff --git a/action/protocol/staking/contractstaking/bucket_type.go b/action/protocol/staking/contractstaking/bucket_type.go index 489f4245df..4315f6ec36 100644 --- a/action/protocol/staking/contractstaking/bucket_type.go +++ b/action/protocol/staking/contractstaking/bucket_type.go @@ -3,9 +3,11 @@ package contractstaking import ( "math/big" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/pkg/errors" "google.golang.org/protobuf/proto" + + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) type ( @@ -65,3 +67,17 @@ func (bt *BucketType) Clone() *BucketType { ActivatedAt: bt.ActivatedAt, } } + +// Encode encodes the bucket type into a GenericValue +func (bt *BucketType) Encode() (systemcontracts.GenericValue, error) { + data, err := bt.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize bucket type") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes the bucket type from a GenericValue +func (bt *BucketType) Decode(gv systemcontracts.GenericValue) error { + return bt.Deserialize(gv.PrimaryData) +} diff --git a/action/protocol/staking/contractstaking/contract.go b/action/protocol/staking/contractstaking/contract.go index 81bfb1ac60..e6b72a22e7 100644 --- a/action/protocol/staking/contractstaking/contract.go +++ b/action/protocol/staking/contractstaking/contract.go @@ -1,9 +1,11 @@ package contractstaking import ( - "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/pkg/errors" "google.golang.org/protobuf/proto" + + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) // StakingContract represents the staking contract in the system @@ -50,3 +52,17 @@ func (sc *StakingContract) Deserialize(b []byte) error { *sc = *loaded return nil } + +// Encode encodes the staking contract into a GenericValue +func (sc *StakingContract) Encode() (systemcontracts.GenericValue, error) { + data, err := sc.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize staking contract") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes the staking contract from a GenericValue +func (sc *StakingContract) Decode(gv systemcontracts.GenericValue) error { + return sc.Deserialize(gv.PrimaryData) +} diff --git a/action/protocol/staking/contractstaking/statereader.go b/action/protocol/staking/contractstaking/statereader.go index 98b0d542d2..69da1261ca 100644 --- a/action/protocol/staking/contractstaking/statereader.go +++ b/action/protocol/staking/contractstaking/statereader.go @@ -3,11 +3,12 @@ package contractstaking import ( "fmt" + "github.com/pkg/errors" + "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/iotexproject/iotex-core/v2/pkg/util/byteutil" "github.com/iotexproject/iotex-core/v2/state" - "github.com/pkg/errors" "github.com/iotexproject/iotex-address/address" ) @@ -25,11 +26,11 @@ func NewStateReader(sr protocol.StateReader) *ContractStakingStateReader { } func contractNamespaceOption(contractAddr address.Address) protocol.StateOption { - return protocol.NamespaceOption(fmt.Sprintf("cs_bucket_%x", contractAddr.Bytes())) + return protocol.NamespaceOption(fmt.Sprintf("%s%x", state.ContractStakingBucketNamespacePrefix, contractAddr.Bytes())) } func bucketTypeNamespaceOption(contractAddr address.Address) protocol.StateOption { - return protocol.NamespaceOption(fmt.Sprintf("cs_bucket_type_%x", contractAddr.Bytes())) + return protocol.NamespaceOption(fmt.Sprintf("%s%x", state.ContractStakingBucketTypeNamespacePrefix, contractAddr.Bytes())) } func contractKeyOption(contractAddr address.Address) protocol.StateOption { @@ -42,7 +43,7 @@ func bucketIDKeyOption(bucketID uint64) protocol.StateOption { // metaNamespaceOption is the namespace for meta information (e.g., total number of buckets). func metaNamespaceOption() protocol.StateOption { - return protocol.NamespaceOption("staking_contract_meta") + return protocol.NamespaceOption(state.StakingContractMetaNamespace) } func (r *ContractStakingStateReader) contract(contractAddr address.Address) (*StakingContract, error) { diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 8f03fd1d01..7f6310ae42 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -683,7 +683,9 @@ func (p *Protocol) HandleReceipt(ctx context.Context, elp action.Envelope, sm pr if err != nil { return err } - return v.(*viewData).contractsStake.Handle(ctx, receipt) + if err := v.(*viewData).contractsStake.Handle(ctx, receipt); err != nil { + return err + } } handler, err := newNFTBucketEventHandler(sm, func(bucket *contractstaking.Bucket, height uint64) *big.Int { vb := p.convertToVoteBucket(bucket, height) diff --git a/state/factory/erigonstore/registry.go b/state/factory/erigonstore/registry.go index 7ea96d38ac..468b171149 100644 --- a/state/factory/erigonstore/registry.go +++ b/state/factory/erigonstore/registry.go @@ -2,6 +2,7 @@ package erigonstore import ( "reflect" + "strings" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" @@ -28,7 +29,8 @@ var ( // ObjectStorageRegistry is a registry for object storage type ObjectStorageRegistry struct { contracts map[string]map[reflect.Type]int - fallback map[string]int + ns map[string]int + nsPrefix map[string]int } func init() { @@ -38,7 +40,9 @@ func init() { assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandsMapNamespace, CandidateMapContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingNamespace, BucketPoolContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingViewNamespace, StakingViewContractIndex)) - assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingNamespace, BucketPoolContractIndex)) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingContractMetaNamespace, StakingViewContractIndex)) + assertions.MustNoError(storageRegistry.RegisterNamespacePrefix(state.ContractStakingBucketNamespacePrefix, StakingViewContractIndex)) + assertions.MustNoError(storageRegistry.RegisterNamespacePrefix(state.ContractStakingBucketTypeNamespacePrefix, StakingViewContractIndex)) assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.AccountKVNamespace, &state.Account{}, AccountIndex)) assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.AccountKVNamespace, &state.CandidateList{}, PollLegacyCandidateListContractIndex)) @@ -59,7 +63,8 @@ func GetObjectStorageRegistry() *ObjectStorageRegistry { func newObjectStorageRegistry() *ObjectStorageRegistry { return &ObjectStorageRegistry{ contracts: make(map[string]map[reflect.Type]int), - fallback: make(map[string]int), + ns: make(map[string]int), + nsPrefix: make(map[string]int), } } @@ -109,12 +114,28 @@ func (osr *ObjectStorageRegistry) RegisterNamespace(ns string, index int) error return osr.register(ns, nil, index) } +// RegisterNamespacePrefix registers a namespace prefix object storage +func (osr *ObjectStorageRegistry) RegisterNamespacePrefix(prefix string, index int) error { + if index < AccountIndex || index >= SystemContractCount { + return errors.Errorf("invalid system contract index %d", index) + } + return osr.registerPrefix(prefix, index) +} + +func (osr *ObjectStorageRegistry) registerPrefix(ns string, index int) error { + if _, exists := osr.nsPrefix[ns]; exists { + return errors.Wrapf(ErrObjectStorageAlreadyRegistered, "registered: %v", osr.nsPrefix[ns]) + } + osr.nsPrefix[ns] = index + return nil +} + func (osr *ObjectStorageRegistry) register(ns string, obj any, index int) error { if obj == nil { - if _, exists := osr.fallback[ns]; exists { - return errors.Wrapf(ErrObjectStorageAlreadyRegistered, "registered: %v", osr.fallback[ns]) + if _, exists := osr.ns[ns]; exists { + return errors.Wrapf(ErrObjectStorageAlreadyRegistered, "registered: %v", osr.ns[ns]) } - osr.fallback[ns] = index + osr.ns[ns] = index return nil } types, ok := osr.contracts[ns] @@ -130,6 +151,7 @@ func (osr *ObjectStorageRegistry) register(ns string, obj any, index int) error } func (osr *ObjectStorageRegistry) matchContractIndex(ns string, obj any) (int, bool) { + // object specific storage if obj != nil { types, ok := osr.contracts[ns] if ok { @@ -139,6 +161,16 @@ func (osr *ObjectStorageRegistry) matchContractIndex(ns string, obj any) (int, b } } } - index, exist := osr.fallback[ns] - return index, exist + // namespace specific storage + index, exist := osr.ns[ns] + if exist { + return index, true + } + // namespace prefix specific storage + for prefix, index := range osr.nsPrefix { + if strings.HasPrefix(ns, prefix) { + return index, true + } + } + return 0, false } diff --git a/state/factory/statedb.go b/state/factory/statedb.go index 0351edf7bc..091c7f8121 100644 --- a/state/factory/statedb.go +++ b/state/factory/statedb.go @@ -10,6 +10,7 @@ import ( "fmt" "slices" "strconv" + "strings" "sync" "time" @@ -283,7 +284,7 @@ func (sdb *stateDB) createWorkingSetStore(ctx context.Context, height uint64, kv flusher, err := db.NewKVStoreFlusher( kvstore, batch.NewCachedBatch(), - sdb.flusherOptions(!g.IsEaster(height))..., + sdb.flusherOptions(!g.IsEaster(height), g.IsToBeEnabled(height))..., ) if err != nil { return nil, err @@ -559,7 +560,7 @@ func (sdb *stateDB) StateReaderAt(blkHeight uint64, blkHash hash.Hash256) (proto // private trie constructor functions //====================================== -func (sdb *stateDB) flusherOptions(preEaster bool) []db.KVStoreFlusherOption { +func (sdb *stateDB) flusherOptions(preEaster, storeContractStaking bool) []db.KVStoreFlusherOption { opts := []db.KVStoreFlusherOption{ db.SerializeOption(func(wi *batch.WriteInfo) []byte { if preEaster { @@ -568,19 +569,43 @@ func (sdb *stateDB) flusherOptions(preEaster bool) []db.KVStoreFlusherOption { return wi.Serialize() }), } - serializeFilterNs := []string{state.StakingViewNamespace} + var ( + serializeFilterNs = []string{state.StakingViewNamespace} + serializeFilterNsPrefixes = []string{} + flushFilterNs = []string{state.StakingViewNamespace} + flushFilterNsPrefixes = []string{} + ) if preEaster { serializeFilterNs = append(serializeFilterNs, evm.CodeKVNameSpace, staking.CandsMapNS) } + if !storeContractStaking { + serializeFilterNs = append(serializeFilterNs, state.StakingContractMetaNamespace) + serializeFilterNsPrefixes = append(serializeFilterNsPrefixes, + state.ContractStakingBucketNamespacePrefix, + state.ContractStakingBucketTypeNamespacePrefix, + ) + flushFilterNs = append(flushFilterNs, state.StakingContractMetaNamespace) + flushFilterNsPrefixes = append(flushFilterNsPrefixes, + state.ContractStakingBucketNamespacePrefix, + state.ContractStakingBucketTypeNamespacePrefix, + ) + } opts = append(opts, db.FlushTranslateOption(func(wi *batch.WriteInfo) *batch.WriteInfo { - if wi.Namespace() == state.StakingViewNamespace { + if slices.Contains(flushFilterNs, wi.Namespace()) || + slices.ContainsFunc(flushFilterNsPrefixes, func(prefix string) bool { + return strings.HasPrefix(wi.Namespace(), prefix) + }) { + // skip flushing the write return nil } return wi }), db.SerializeFilterOption(func(wi *batch.WriteInfo) bool { - return slices.Contains(serializeFilterNs, wi.Namespace()) + return slices.Contains(serializeFilterNs, wi.Namespace()) || + slices.ContainsFunc(serializeFilterNsPrefixes, func(prefix string) bool { + return strings.HasPrefix(wi.Namespace(), prefix) + }) }), ) return opts diff --git a/state/tables.go b/state/tables.go index 4a3e944c3c..ab5e6ddf88 100644 --- a/state/tables.go +++ b/state/tables.go @@ -42,6 +42,15 @@ const ( // - "voteview" + --> CandidateVotes StakingViewNamespace = "StakingView" + // ContractStakingBucketNamespacePrefix is the namespace to store staking contract buckets + // - --> --> Bucket + ContractStakingBucketNamespacePrefix = "cs_bucket_" + // ContractStakingBucketTypeNamespacePrefix is the namespace to store staking contract bucket types + // - --> --> BucketType + ContractStakingBucketTypeNamespacePrefix = "cs_bucket_type_" + // StakingContractMetaNamespace is the namespace to store staking contract meta information + StakingContractMetaNamespace = "staking_contract_meta" + // CandidateNamespace is the namespace to store candidate information // - --> Candidate CandidateNamespace = "Candidate" From ee3380bd5d0edfa25bcb4d14dafad5f696583603 Mon Sep 17 00:00:00 2001 From: envestcc Date: Tue, 30 Sep 2025 19:13:39 +0800 Subject: [PATCH 11/12] fix wrapper base --- .../stakingindex/candidate_votes.go | 111 +++++++++---- .../stakingindex/candidate_votes_manager.go | 2 +- .../stakingindex/candidate_votes_test.go | 152 ++++++++++++++++++ systemcontractindex/stakingindex/index.go | 2 +- .../stakingindex/voteview_test.go | 2 + 5 files changed, 239 insertions(+), 30 deletions(-) diff --git a/systemcontractindex/stakingindex/candidate_votes.go b/systemcontractindex/stakingindex/candidate_votes.go index 8fc8b8dc91..f4bfe46552 100644 --- a/systemcontractindex/stakingindex/candidate_votes.go +++ b/systemcontractindex/stakingindex/candidate_votes.go @@ -11,12 +11,16 @@ import ( "github.com/iotexproject/iotex-core/v2/systemcontracts" ) +var ( + // ErrCandidateVotesIsDirty is returned when candidate votes are dirty + ErrCandidateVotesIsDirty = errors.New("candidate votes is dirty") +) + // CandidateVotes is the interface to manage candidate votes type CandidateVotes interface { Clone() CandidateVotes Votes(fCtx protocol.FeatureCtx, cand string) *big.Int Add(cand string, amount *big.Int, votes *big.Int) - Clear() Commit() CandidateVotes Base() CandidateVotes IsDirty() bool @@ -35,6 +39,11 @@ type candidateVotes struct { cands map[string]*candidate } +type candidateVotesWithBuffer struct { + base *candidateVotes + change *candidateVotes +} + type candidateVotesWraper struct { base CandidateVotes change *candidateVotes @@ -51,7 +60,7 @@ func newCandidate() *candidate { } } -func (cv *candidateVotes) Clone() CandidateVotes { +func (cv *candidateVotes) Clone() *candidateVotes { newCands := make(map[string]*candidate) for cand, c := range cv.cands { newCands[cand] = &candidate{ @@ -64,10 +73,6 @@ func (cv *candidateVotes) Clone() CandidateVotes { } } -func (cv *candidateVotes) IsDirty() bool { - return false -} - func (cv *candidateVotes) Votes(fCtx protocol.FeatureCtx, cand string) *big.Int { c := cv.cands[cand] if c == nil { @@ -91,10 +96,6 @@ func (cv *candidateVotes) Add(cand string, amount *big.Int, votes *big.Int) { } } -func (cv *candidateVotes) Clear() { - cv.cands = make(map[string]*candidate) -} - func (cv *candidateVotes) Serialize() ([]byte, error) { cl := stakingpb.CandidateList{} for cand, c := range cv.cands { @@ -138,14 +139,6 @@ func (cv *candidateVotes) Decode(data systemcontracts.GenericValue) error { return cv.Deserialize(data.PrimaryData) } -func (cv *candidateVotes) Commit() CandidateVotes { - return cv -} - -func (cv *candidateVotes) Base() CandidateVotes { - return cv -} - func newCandidateVotes() *candidateVotes { return &candidateVotes{ cands: make(map[string]*candidate), @@ -162,12 +155,12 @@ func newCandidateVotesWrapper(base CandidateVotes) *candidateVotesWraper { func (cv *candidateVotesWraper) Clone() CandidateVotes { return &candidateVotesWraper{ base: cv.base.Clone(), - change: cv.change.Clone().(*candidateVotes), + change: cv.change.Clone(), } } func (cv *candidateVotesWraper) IsDirty() bool { - return cv.change.IsDirty() || cv.base.IsDirty() + return len(cv.change.cands) > 0 || cv.base.IsDirty() } func (cv *candidateVotesWraper) Votes(fCtx protocol.FeatureCtx, cand string) *big.Int { @@ -186,11 +179,6 @@ func (cv *candidateVotesWraper) Add(cand string, amount *big.Int, votes *big.Int cv.change.Add(cand, amount, votes) } -func (cv *candidateVotesWraper) Clear() { - cv.change.Clear() - cv.base.Clear() -} - func (cv *candidateVotesWraper) Commit() CandidateVotes { // Commit the changes to the base for cand, change := range cv.change.cands { @@ -202,15 +190,19 @@ func (cv *candidateVotesWraper) Commit() CandidateVotes { } func (cv *candidateVotesWraper) Serialize() ([]byte, error) { - return nil, errors.New("not implemented") + if cv.IsDirty() { + return nil, errors.Wrap(ErrCandidateVotesIsDirty, "cannot serialize dirty candidate votes") + } + return cv.base.Serialize() } func (cv *candidateVotesWraper) Deserialize(data []byte) error { - return errors.New("not implemented") + cv.change = newCandidateVotes() + return cv.base.Deserialize(data) } func (cv *candidateVotesWraper) Base() CandidateVotes { - return cv.base + return cv.base.Base() } func newCandidateVotesWrapperCommitInClone(base CandidateVotes) *candidateVotesWraperCommitInClone { @@ -229,3 +221,66 @@ func (cv *candidateVotesWraperCommitInClone) Commit() CandidateVotes { cv.base = cv.base.Clone() return cv.candidateVotesWraper.Commit() } + +func (cv *candidateVotesWraperCommitInClone) Base() CandidateVotes { + return cv.base +} + +func newCandidateVotesWithBuffer(base *candidateVotes) *candidateVotesWithBuffer { + return &candidateVotesWithBuffer{ + base: base, + change: newCandidateVotes(), + } +} + +func (cv *candidateVotesWithBuffer) Clone() CandidateVotes { + return &candidateVotesWithBuffer{ + base: cv.base.Clone(), + change: cv.change.Clone(), + } +} + +func (cv *candidateVotesWithBuffer) IsDirty() bool { + return len(cv.change.cands) > 0 +} + +func (cv *candidateVotesWithBuffer) Votes(fCtx protocol.FeatureCtx, cand string) *big.Int { + base := cv.base.Votes(fCtx, cand) + change := cv.change.Votes(fCtx, cand) + if change == nil { + return base + } + if base == nil { + return change + } + return new(big.Int).Add(base, change) +} + +func (cv *candidateVotesWithBuffer) Add(cand string, amount *big.Int, votes *big.Int) { + cv.change.Add(cand, amount, votes) +} + +func (cv *candidateVotesWithBuffer) Commit() CandidateVotes { + // Commit the changes to the base + for cand, change := range cv.change.cands { + cv.base.Add(cand, change.amount, change.votes) + } + cv.change = newCandidateVotes() + return cv +} + +func (cv *candidateVotesWithBuffer) Serialize() ([]byte, error) { + if cv.IsDirty() { + return nil, errors.Wrap(ErrCandidateVotesIsDirty, "cannot serialize dirty candidate votes") + } + return cv.base.Serialize() +} + +func (cv *candidateVotesWithBuffer) Deserialize(data []byte) error { + cv.change = newCandidateVotes() + return cv.base.Deserialize(data) +} + +func (cv *candidateVotesWithBuffer) Base() CandidateVotes { + return newCandidateVotesWithBuffer(cv.base) +} diff --git a/systemcontractindex/stakingindex/candidate_votes_manager.go b/systemcontractindex/stakingindex/candidate_votes_manager.go index d217735bf6..0abe85364a 100644 --- a/systemcontractindex/stakingindex/candidate_votes_manager.go +++ b/systemcontractindex/stakingindex/candidate_votes_manager.go @@ -52,7 +52,7 @@ func (s *candidateVotesManager) Load(ctx context.Context, sr protocol.StateReade if err != nil { return nil, errors.Wrap(err, "failed to get candidate votes state") } - return cur, nil + return newCandidateVotesWithBuffer(cur), nil } func (s *candidateVotesManager) key() []byte { diff --git a/systemcontractindex/stakingindex/candidate_votes_test.go b/systemcontractindex/stakingindex/candidate_votes_test.go index ce1bd33a18..217519cc1c 100644 --- a/systemcontractindex/stakingindex/candidate_votes_test.go +++ b/systemcontractindex/stakingindex/candidate_votes_test.go @@ -2,6 +2,7 @@ package stakingindex import ( "context" + "fmt" "math/big" "testing" @@ -60,3 +61,154 @@ func TestCandidateVotes(t *testing.T) { require.EqualValues(120, newVotes.Sub(newVotes, originCandVotes).Uint64()) }) } + +func TestCandidateVotesInterface(t *testing.T) { + r := require.New(t) + + cvs := []CandidateVotes{ + newCandidateVotesWithBuffer(newCandidateVotes()), + newCandidateVotesWrapper(newCandidateVotesWithBuffer(newCandidateVotes())), + newCandidateVotesWrapperCommitInClone(newCandidateVotesWithBuffer(newCandidateVotes())), + } + g := genesis.TestDefault() + ctx := genesis.WithGenesisContext(context.Background(), g) + ctxBeforeRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight - 1}))) + ctxAfterRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight}))) + for _, cv := range cvs { + t.Run(fmt.Sprintf("%T", cv), func(t *testing.T) { + // not exist candidate + r.Nil(cv.Votes(ctxAfterRedsea, "notexist")) + r.Nil(cv.Votes(ctxBeforeRedsea, "notexist")) + r.False(cv.IsDirty()) + + // add candidate + adds := []struct { + cand string + amount string + votes string + }{ + {"candidate1", "100", "120"}, + {"candidate2", "200", "240"}, + {"candidate1", "300", "360"}, + {"candidate3", "400", "480"}, + {"candidate2", "500", "600"}, + {"candidate2", "-200", "-240"}, + } + expects := []struct { + cand string + amount string + votes string + }{ + {"candidate1", "400", "480"}, + {"candidate2", "500", "600"}, + {"candidate3", "400", "480"}, + } + for _, add := range adds { + amount := big.NewInt(0) + votes := big.NewInt(0) + _, ok := amount.SetString(add.amount, 10) + r.True(ok) + _, ok = votes.SetString(add.votes, 10) + r.True(ok) + cv.Add(add.cand, amount, votes) + } + checkVotes := func(cv CandidateVotes) { + for _, expect := range expects { + amount := big.NewInt(0) + votes := big.NewInt(0) + _, ok := amount.SetString(expect.amount, 10) + r.True(ok) + _, ok = votes.SetString(expect.votes, 10) + r.True(ok) + r.Equal(amount, cv.Votes(ctxBeforeRedsea, expect.cand)) + r.Equal(votes, cv.Votes(ctxAfterRedsea, expect.cand)) + } + } + checkVotes(cv) + cl := cv.Clone() + checkVotes(cl) + // both cv and cl are dirty + r.True(cv.IsDirty()) + r.True(cl.IsDirty()) + // serialize dirty cv should fail + _, err := cv.Serialize() + r.ErrorIs(err, ErrCandidateVotesIsDirty) + // commit cv + reset := cv.Commit() + r.False(reset.IsDirty()) + data, err := reset.Serialize() + r.NoError(err) + // deserialize to new cv + decv := newCandidateVotes() + err = decv.Deserialize(data) + r.NoError(err) + checkVotes(newCandidateVotesWithBuffer(decv)) + // adds not affect base + cv.Add("candidate4", big.NewInt(1000), big.NewInt(1200)) + cv.Add("candidate1", big.NewInt(-100), big.NewInt(-120)) + cv.Add("candidate2", big.NewInt(100), big.NewInt(120)) + checkVotes(cv.Base()) + }) + } +} + +func TestCandidateVotesWrapper(t *testing.T) { + r := require.New(t) + baseCv := newCandidateVotesWithBuffer(newCandidateVotes()) + baseCv.Add("candidate1", big.NewInt(100), big.NewInt(120)) + baseCv.Add("candidate2", big.NewInt(200), big.NewInt(240)) + baseCv.Add("candidate3", big.NewInt(400), big.NewInt(480)) + base := baseCv.Commit() + // wrap's changes should not affect base + wrap := newCandidateVotesWrapper(base) + wrap.Add("candidate1", big.NewInt(300), big.NewInt(360)) + wrap.Add("candidate4", big.NewInt(1000), big.NewInt(1200)) + candidateVotesEqual(r, base, wrap.Base(), []string{"candidate1", "candidate2", "candidate3", "candidate4"}) + // multiple wraps return base recursively + wrap2 := newCandidateVotesWrapper(wrap) + wrap2.Add("candidate2", big.NewInt(500), big.NewInt(600)) + wrap2.Add("candidate5", big.NewInt(2000), big.NewInt(2400)) + candidateVotesEqual(r, base, wrap2.Base(), []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) + // commit wrap should apply all changes to base + wrap2.Commit() + candidateVotesEqual(r, base, wrap2, []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) +} + +func TestCandidateVotesWrapperCommitInClone(t *testing.T) { + r := require.New(t) + baseCv := newCandidateVotesWithBuffer(newCandidateVotes()) + baseCv.Add("candidate1", big.NewInt(100), big.NewInt(120)) + baseCv.Add("candidate2", big.NewInt(200), big.NewInt(240)) + baseCv.Add("candidate3", big.NewInt(400), big.NewInt(480)) + base := baseCv.Commit() + wrap := newCandidateVotesWrapper(base) + wrap.Add("candidate1", big.NewInt(300), big.NewInt(360)) + wrap.Add("candidate4", big.NewInt(1000), big.NewInt(1200)) + wrap2 := newCandidateVotesWrapper(wrap) + wrap2.Add("candidate2", big.NewInt(500), big.NewInt(600)) + wrap2.Add("candidate5", big.NewInt(2000), big.NewInt(2400)) + wrap3 := newCandidateVotesWrapper(wrap) + wrap3.Add("candidate3", big.NewInt(700), big.NewInt(840)) + wrap3.Add("candidate1", big.NewInt(-3000), big.NewInt(-3600)) + wrap3Clone := wrap3.Clone() + + // base return the first base + wrap4 := newCandidateVotesWrapperCommitInClone(wrap3) + wrap4.Add("candidate2", big.NewInt(3000), big.NewInt(3600)) + wrap4.Add("candidate3", big.NewInt(4000), big.NewInt(4800)) + candidateVotesEqual(r, wrap4.Base(), wrap3, []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) + // commit wrap4 should not apply all changes to base + wrap4.Commit() + candidateVotesEqual(r, wrap3, wrap3Clone, []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) +} + +func candidateVotesEqual(r *require.Assertions, cv1, cv2 CandidateVotes, cands []string) { + g := genesis.TestDefault() + ctx := genesis.WithGenesisContext(context.Background(), g) + ctxBeforeRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight - 1}))) + ctxAfterRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight}))) + for _, cand := range cands { + r.Equal(cv1.Votes(ctxBeforeRedsea, cand), cv2.Votes(ctxBeforeRedsea, cand)) + r.Equal(cv1.Votes(ctxAfterRedsea, cand), cv2.Votes(ctxAfterRedsea, cand)) + } +} diff --git a/systemcontractindex/stakingindex/index.go b/systemcontractindex/stakingindex/index.go index 5a706d14f1..14c73b4bf2 100644 --- a/systemcontractindex/stakingindex/index.go +++ b/systemcontractindex/stakingindex/index.go @@ -401,5 +401,5 @@ func AggregateCandidateVotes(bkts map[uint64]*Bucket, calculateUnmutedVoteWeight votes := calculateUnmutedVoteWeight(bkt) res.Add(bkt.Candidate.String(), bkt.StakedAmount, votes) } - return res + return newCandidateVotesWithBuffer(res) } diff --git a/systemcontractindex/stakingindex/voteview_test.go b/systemcontractindex/stakingindex/voteview_test.go index 4110747749..bc075cd0c1 100644 --- a/systemcontractindex/stakingindex/voteview_test.go +++ b/systemcontractindex/stakingindex/voteview_test.go @@ -22,6 +22,7 @@ func TestVoteView(t *testing.T) { defer ctrl.Finish() mockIndexer := staking.NewMockContractStakingIndexer(ctrl) + mr := NewCandidateVotesManager(identityset.Address(0)) vv := NewVoteView( mockIndexer, &VoteViewConfig{ @@ -30,6 +31,7 @@ func TestVoteView(t *testing.T) { 100, nil, nil, + mr, func(b *contractstaking.Bucket, h uint64) *big.Int { return big.NewInt(1) }, From 86835657b333eec95d6270d78a6647dfa4bbebaa Mon Sep 17 00:00:00 2001 From: envestcc Date: Tue, 30 Sep 2025 19:14:26 +0800 Subject: [PATCH 12/12] fix system contract index --- state/factory/erigonstore/systemcontracts.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/state/factory/erigonstore/systemcontracts.go b/state/factory/erigonstore/systemcontracts.go index c9a627b9bf..8b657ef95e 100644 --- a/state/factory/erigonstore/systemcontracts.go +++ b/state/factory/erigonstore/systemcontracts.go @@ -22,6 +22,9 @@ type SystemContract struct { const ( // AccountIndex is the system contract for account storage AccountIndex = -1 +) + +const ( // StakingBucketsContractIndex is the system contract for staking buckets storage StakingBucketsContractIndex int = iota // BucketPoolContractIndex is the system contract for bucket pool storage