diff --git a/action/protocol/poll/blockmeta.go b/action/protocol/poll/blockmeta.go index 13ae8fd2e4..b32e90eb6d 100644 --- a/action/protocol/poll/blockmeta.go +++ b/action/protocol/poll/blockmeta.go @@ -12,10 +12,7 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/action/protocol/poll/blockmetapb" - "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" - "github.com/iotexproject/iotex-core/v2/state/factory/erigonstore" "github.com/iotexproject/iotex-core/v2/systemcontracts" ) @@ -26,10 +23,6 @@ type BlockMeta struct { MintTime time.Time } -func init() { - assertions.MustNoError(erigonstore.GetObjectStorageRegistry().RegisterPollBlockMeta(protocol.SystemNamespace, &BlockMeta{})) -} - // NewBlockMeta constructs new blockmeta struct with given fieldss func NewBlockMeta(height uint64, producer string, mintTime time.Time) *BlockMeta { return &BlockMeta{ diff --git a/action/protocol/rewarding/admin.go b/action/protocol/rewarding/admin.go index f388f6c9ee..1d67abb3ad 100644 --- a/action/protocol/rewarding/admin.go +++ b/action/protocol/rewarding/admin.go @@ -17,9 +17,6 @@ import ( "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/action/protocol/rewarding/rewardingpb" "github.com/iotexproject/iotex-core/v2/blockchain/genesis" - "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" - "github.com/iotexproject/iotex-core/v2/state" - "github.com/iotexproject/iotex-core/v2/state/factory/erigonstore" "github.com/iotexproject/iotex-core/v2/systemcontracts" ) @@ -34,14 +31,6 @@ type admin struct { productivityThreshold uint64 } -func init() { - registry := erigonstore.GetObjectStorageRegistry() - assertions.MustNoError(registry.RegisterRewardingV1(state.AccountKVNamespace, &admin{})) - assertions.MustNoError(registry.RegisterRewardingV1(state.AccountKVNamespace, &exempt{})) - assertions.MustNoError(registry.RegisterRewardingV2(_v2RewardingNamespace, &admin{})) - assertions.MustNoError(registry.RegisterRewardingV2(_v2RewardingNamespace, &exempt{})) -} - // Serialize serializes admin state into bytes func (a admin) Serialize() ([]byte, error) { gen := rewardingpb.Admin{ diff --git a/action/protocol/rewarding/fund.go b/action/protocol/rewarding/fund.go index 95bbc49d0f..2131d447fe 100644 --- a/action/protocol/rewarding/fund.go +++ b/action/protocol/rewarding/fund.go @@ -19,9 +19,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/rewarding/rewardingpb" - "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" "github.com/iotexproject/iotex-core/v2/state" - "github.com/iotexproject/iotex-core/v2/state/factory/erigonstore" "github.com/iotexproject/iotex-core/v2/systemcontracts" ) @@ -32,12 +30,6 @@ type fund struct { unclaimedBalance *big.Int } -func init() { - registry := erigonstore.GetObjectStorageRegistry() - assertions.MustNoError(registry.RegisterRewardingV1(state.AccountKVNamespace, &fund{})) - assertions.MustNoError(registry.RegisterRewardingV2(_v2RewardingNamespace, &fund{})) -} - // Serialize serializes fund state into bytes func (f fund) Serialize() ([]byte, error) { gen := rewardingpb.Fund{ diff --git a/action/protocol/rewarding/reward.go b/action/protocol/rewarding/reward.go index 76346217b3..98ee8414ff 100644 --- a/action/protocol/rewarding/reward.go +++ b/action/protocol/rewarding/reward.go @@ -25,23 +25,13 @@ import ( "github.com/iotexproject/iotex-core/v2/action/protocol/staking" "github.com/iotexproject/iotex-core/v2/pkg/enc" "github.com/iotexproject/iotex-core/v2/pkg/log" - "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" "github.com/iotexproject/iotex-core/v2/state" - "github.com/iotexproject/iotex-core/v2/state/factory/erigonstore" "github.com/iotexproject/iotex-core/v2/systemcontracts" ) // rewardHistory is the dummy struct to record a reward. Only key matters. type rewardHistory struct{} -func init() { - registry := erigonstore.GetObjectStorageRegistry() - assertions.MustNoError(registry.RegisterRewardingV1(state.AccountKVNamespace, &rewardHistory{})) - assertions.MustNoError(registry.RegisterRewardingV1(state.AccountKVNamespace, &rewardAccount{})) - assertions.MustNoError(registry.RegisterRewardingV2(_v2RewardingNamespace, &rewardHistory{})) - assertions.MustNoError(registry.RegisterRewardingV2(_v2RewardingNamespace, &rewardAccount{})) -} - // Serialize serializes reward history state into bytes func (b rewardHistory) Serialize() ([]byte, error) { gen := rewardingpb.RewardHistory{} diff --git a/action/protocol/staking/bucket_index.go b/action/protocol/staking/bucket_index.go index 698abd6e9e..4de0f37629 100644 --- a/action/protocol/staking/bucket_index.go +++ b/action/protocol/staking/bucket_index.go @@ -12,6 +12,7 @@ import ( "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) type ( @@ -51,6 +52,20 @@ func (bis *BucketIndices) Serialize() ([]byte, error) { return proto.Marshal(bis.Proto()) } +// Encode encodes bucket indices into generic value +func (bis *BucketIndices) Encode() (systemcontracts.GenericValue, error) { + data, err := bis.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize bucket indices") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes bucket indices from generic value +func (bis *BucketIndices) Decode(gv systemcontracts.GenericValue) error { + return bis.Deserialize(gv.PrimaryData) +} + func (bis *BucketIndices) addBucketIndex(index uint64) { *bis = append(*bis, index) } diff --git a/action/protocol/staking/bucket_pool.go b/action/protocol/staking/bucket_pool.go index c5d998edcb..7616c0456d 100644 --- a/action/protocol/staking/bucket_pool.go +++ b/action/protocol/staking/bucket_pool.go @@ -9,11 +9,13 @@ import ( "math/big" "github.com/iotexproject/go-pkgs/hash" + "github.com/pkg/errors" "google.golang.org/protobuf/proto" "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/iotexproject/iotex-core/v2/state" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) // const @@ -91,6 +93,20 @@ func (t *totalAmount) SubBalance(amount *big.Int) error { return nil } +// Encode encodes total amount into generic value +func (t *totalAmount) Encode() (systemcontracts.GenericValue, error) { + data, err := t.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize total amount") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes total amount from generic value +func (t *totalAmount) Decode(gv systemcontracts.GenericValue) error { + return t.Deserialize(gv.PrimaryData) +} + // IsDirty returns true if the bucket pool is dirty func (bp *BucketPool) IsDirty() bool { return bp.dirty diff --git a/action/protocol/staking/candidate.go b/action/protocol/staking/candidate.go index 413821e49c..450ae2e83c 100644 --- a/action/protocol/staking/candidate.go +++ b/action/protocol/staking/candidate.go @@ -18,6 +18,7 @@ import ( "github.com/iotexproject/iotex-core/v2/action" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/iotexproject/iotex-core/v2/state" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) type ( @@ -194,6 +195,50 @@ func (d *Candidate) GetIdentifier() address.Address { return d.Identifier } +// Encode encodes candidate into generic value +func (d *Candidate) Encode() (systemcontracts.GenericValue, error) { + var ( + primaryData []byte + secondaryData []byte + err error + value systemcontracts.GenericValue + ) + if d.Votes.Sign() > 0 { + secondaryData, err = proto.Marshal(&stakingpb.Candidate{Votes: d.Votes.String()}) + if err != nil { + return value, errors.Wrap(err, "failed to marshal candidate votes") + } + } + clone := d.Clone() + clone.Votes = big.NewInt(0) + primaryData, err = clone.Serialize() + if err != nil { + return value, errors.Wrap(err, "failed to serialize candidate") + } + value.PrimaryData = primaryData + value.SecondaryData = secondaryData + return value, nil +} + +// Decode decodes candidate from generic value +func (d *Candidate) Decode(gv systemcontracts.GenericValue) error { + if err := d.Deserialize(gv.PrimaryData); err != nil { + return errors.Wrap(err, "failed to deserialize candidate") + } + if len(gv.SecondaryData) > 0 { + votes := &stakingpb.Candidate{} + if err := proto.Unmarshal(gv.SecondaryData, votes); err != nil { + return errors.Wrap(err, "failed to unmarshal candidate votes") + } + vote, ok := new(big.Int).SetString(votes.Votes, 10) + if !ok { + return errors.Wrapf(action.ErrInvalidAmount, "failed to parse candidate votes: %s", votes.Votes) + } + d.Votes = vote + } + return nil +} + func (d *Candidate) toProto() (*stakingpb.Candidate, error) { if d.Owner == nil || d.Operator == nil || d.Reward == nil || len(d.Name) == 0 || d.Votes == nil || d.SelfStake == nil { @@ -360,6 +405,20 @@ func (l *CandidateList) Deserialize(buf []byte) error { return nil } +// Encode encodes candidate list into generic value +func (l *CandidateList) Encode() (systemcontracts.GenericValue, error) { + data, err := l.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize candidate list") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes candidate list from generic value +func (l *CandidateList) Decode(gv systemcontracts.GenericValue) error { + return l.Deserialize(gv.PrimaryData) +} + func (l CandidateList) toStateCandidateList() (state.CandidateList, error) { list := make(state.CandidateList, 0, len(l)) for _, c := range l { diff --git a/action/protocol/staking/candidate_statemanager.go b/action/protocol/staking/candidate_statemanager.go index 580bc833e0..25fc43c58f 100644 --- a/action/protocol/staking/candidate_statemanager.go +++ b/action/protocol/staking/candidate_statemanager.go @@ -220,7 +220,9 @@ func (csm *candSM) putBucket(bucket *VoteBucket) (uint64, error) { func (csm *candSM) delBucket(index uint64) error { _, err := csm.DelState( protocol.NamespaceOption(_stakingNameSpace), - protocol.KeyOption(bucketKey(index))) + protocol.KeyOption(bucketKey(index)), + protocol.ObjectOption(&VoteBucket{}), + ) return err } @@ -295,7 +297,9 @@ func (csm *candSM) delBucketIndex(addr address.Address, prefix byte, index uint6 if len(bis) == 0 { _, err = csm.DelState( protocol.NamespaceOption(_stakingNameSpace), - protocol.KeyOption(key)) + protocol.KeyOption(key), + protocol.ObjectOption(&BucketIndices{}), + ) } else { _, err = csm.PutState( &bis, @@ -319,7 +323,7 @@ func (csm *candSM) putCandBucketIndex(addr address.Address, index uint64) error } func (csm *candSM) delCandidate(name address.Address) error { - _, err := csm.DelState(protocol.NamespaceOption(_candidateNameSpace), protocol.KeyOption(name.Bytes())) + _, err := csm.DelState(protocol.NamespaceOption(_candidateNameSpace), protocol.KeyOption(name.Bytes()), protocol.ObjectOption(&Candidate{})) return err } diff --git a/action/protocol/staking/candidate_statereader.go b/action/protocol/staking/candidate_statereader.go index 402e00c06d..8d0ad5f6b8 100644 --- a/action/protocol/staking/candidate_statereader.go +++ b/action/protocol/staking/candidate_statereader.go @@ -216,6 +216,7 @@ func (c *candSR) NativeBuckets() ([]*VoteBucket, uint64, error) { } return keys, nil }), + protocol.ObjectOption(&VoteBucket{}), ) if err != nil { return nil, height, err @@ -297,7 +298,7 @@ func (c *candSR) CandidateByAddress(name address.Address) (*Candidate, uint64, e } func (c *candSR) CreateCandidateCenter() (*CandidateCenter, uint64, error) { - height, iter, err := c.States(protocol.NamespaceOption(_candidateNameSpace)) + height, iter, err := c.States(protocol.NamespaceOption(_candidateNameSpace), protocol.ObjectOption(&Candidate{})) var cands CandidateList switch errors.Cause(err) { case nil: diff --git a/action/protocol/staking/contractstake_indexer.go b/action/protocol/staking/contractstake_indexer.go index f574ea80c8..caa9ad8d37 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,12 @@ 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) + // IndexerAt returns the contract staking indexer at a specific height + IndexerAt(protocol.StateReader) ContractStakingIndexer + // BucketReader defines the interface to read buckets + 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..d796b9999c 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() @@ -257,6 +288,20 @@ func (mr *MockContractStakingIndexerMockRecorder) Height() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Height", reflect.TypeOf((*MockContractStakingIndexer)(nil).Height)) } +// IndexerAt mocks base method. +func (m *MockContractStakingIndexer) IndexerAt(arg0 protocol.StateReader) ContractStakingIndexer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IndexerAt", arg0) + ret0, _ := ret[0].(ContractStakingIndexer) + return ret0 +} + +// IndexerAt indicates an expected call of IndexerAt. +func (mr *MockContractStakingIndexerMockRecorder) IndexerAt(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexerAt", reflect.TypeOf((*MockContractStakingIndexer)(nil).IndexerAt), arg0) +} + // LoadStakeView mocks base method. func (m *MockContractStakingIndexer) LoadStakeView(arg0 context.Context, arg1 protocol.StateReader) (ContractStakeView, error) { m.ctrl.T.Helper() @@ -441,6 +486,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 +516,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() @@ -470,6 +546,20 @@ func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) Height() *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Height", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).Height)) } +// IndexerAt mocks base method. +func (m *MockContractStakingIndexerWithBucketType) IndexerAt(arg0 protocol.StateReader) ContractStakingIndexer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IndexerAt", arg0) + ret0, _ := ret[0].(ContractStakingIndexer) + return ret0 +} + +// IndexerAt indicates an expected call of IndexerAt. +func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) IndexerAt(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexerAt", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).IndexerAt), arg0) +} + // LoadStakeView mocks base method. func (m *MockContractStakingIndexerWithBucketType) LoadStakeView(arg0 context.Context, arg1 protocol.StateReader) (ContractStakeView, error) { m.ctrl.T.Helper() 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/contractstaking/bucket.go b/action/protocol/staking/contractstaking/bucket.go index 703becee88..d68e73147f 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/endorsement.go b/action/protocol/staking/endorsement.go index 9ce2ba2a80..1b335a8088 100644 --- a/action/protocol/staking/endorsement.go +++ b/action/protocol/staking/endorsement.go @@ -7,6 +7,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) // EndorsementStatus @@ -84,6 +85,20 @@ func (e *Endorsement) Deserialize(buf []byte) error { return e.fromProto(pb) } +// Encode encodes endorsement into generic value +func (e *Endorsement) Encode() (systemcontracts.GenericValue, error) { + data, err := e.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize endorsement") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes endorsement from generic value +func (e *Endorsement) Decode(gv systemcontracts.GenericValue) error { + return e.Deserialize(gv.PrimaryData) +} + func (e *Endorsement) toProto() (*stakingpb.Endorsement, error) { return &stakingpb.Endorsement{ ExpireHeight: e.ExpireHeight, diff --git a/action/protocol/staking/endorsement_statemanager.go b/action/protocol/staking/endorsement_statemanager.go index b002512c8a..c273f8d3c1 100644 --- a/action/protocol/staking/endorsement_statemanager.go +++ b/action/protocol/staking/endorsement_statemanager.go @@ -36,7 +36,7 @@ func (esm *EndorsementStateManager) Put(bucketIndex uint64, endorse *Endorsement // Delete deletes the endorsement of a bucket func (esm *EndorsementStateManager) Delete(bucketIndex uint64) error { - _, err := esm.DelState(protocol.NamespaceOption(_stakingNameSpace), protocol.KeyOption(endorsementKey(bucketIndex))) + _, err := esm.DelState(protocol.NamespaceOption(_stakingNameSpace), protocol.KeyOption(endorsementKey(bucketIndex)), protocol.ObjectOption(&Endorsement{})) return err } diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index dc21f06773..ac7e0233fa 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -479,6 +479,28 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager return err } vd := v.(*viewData) + indexers := []ContractStakingIndexer{} + if p.contractStakingIndexer != nil { + index, err := contractStakingIndexerAt(p.contractStakingIndexer, sm, true) + if err != nil { + return err + } + indexers = append(indexers, index) + } + if p.contractStakingIndexerV2 != nil { + index, err := contractStakingIndexerAt(p.contractStakingIndexerV2, sm, true) + if err != nil { + return err + } + indexers = append(indexers, index) + } + if p.contractStakingIndexerV3 != nil { + index, err := contractStakingIndexerAt(p.contractStakingIndexerV3, sm, true) + if err != nil { + return err + } + indexers = append(indexers, index) + } if blkCtx.BlockHeight == g.ToBeEnabledBlockHeight { handler, err := newNFTBucketEventHandler(sm, func(bucket *contractstaking.Bucket, height uint64) *big.Int { vb := p.convertToVoteBucket(bucket, height) @@ -487,14 +509,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 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 { 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 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) + } } if p.candBucketsIndexer == nil { @@ -680,7 +731,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) @@ -690,19 +743,31 @@ func (p *Protocol) HandleReceipt(ctx context.Context, elp action.Envelope, sm pr return err } if p.contractStakingIndexer != nil { - processor := p.contractStakingIndexer.CreateEventProcessor(ctx, handler) + index, err := contractStakingIndexerAt(p.contractStakingIndexer, sm, true) + if err != nil { + return err + } + processor := index.CreateEventProcessor(ctx, handler) if err := processor.ProcessReceipts(ctx, receipt); err != nil { return errors.Wrap(err, "failed to process receipt for contract staking indexer") } } if p.contractStakingIndexerV2 != nil { - processor := p.contractStakingIndexerV2.CreateEventProcessor(ctx, handler) + index, err := contractStakingIndexerAt(p.contractStakingIndexerV2, sm, true) + if err != nil { + return err + } + processor := index.CreateEventProcessor(ctx, handler) if err := processor.ProcessReceipts(ctx, receipt); err != nil { return errors.Wrap(err, "failed to process receipt for contract staking indexer v2") } } if p.contractStakingIndexerV3 != nil { - processor := p.contractStakingIndexerV3.CreateEventProcessor(ctx, handler) + index, err := contractStakingIndexerAt(p.contractStakingIndexerV3, sm, true) + if err != nil { + return err + } + processor := index.CreateEventProcessor(ctx, handler) if err := processor.ProcessReceipts(ctx, receipt); err != nil { return errors.Wrap(err, "failed to process receipt for contract staking indexer v3") } @@ -779,10 +844,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 +853,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 +1138,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 } @@ -1126,3 +1175,26 @@ func readCandCenterStateFromStateDB(sr protocol.StateReader) (CandidateList, Can } return name, operator, owner, nil } + +func contractStakingIndexerAt(index ContractStakingIndexer, sr protocol.StateReader, delay bool) (ContractStakingIndexer, error) { + if index == nil { + return nil, nil + } + srHeight, err := sr.Height() + if err != nil { + return nil, err + } + if delay { + srHeight-- + } + indexHeight, err := index.Height() + if err != nil { + return nil, err + } + if index.StartHeight() > srHeight || indexHeight == srHeight { + return index, nil + } else if indexHeight < srHeight { + return nil, errors.Errorf("indexer height %d is too old for state reader height %d", indexHeight, srHeight) + } + return index.IndexerAt(sr), nil +} 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/action/protocol/staking/stakeview_builder.go b/action/protocol/staking/stakeview_builder.go index 228c275780..5bee63c34a 100644 --- a/action/protocol/staking/stakeview_builder.go +++ b/action/protocol/staking/stakeview_builder.go @@ -33,42 +33,9 @@ func NewContractStakeViewBuilder( } func (b *contractStakeViewBuilder) Build(ctx context.Context, sr protocol.StateReader, height uint64) (ContractStakeView, error) { - view, err := b.indexer.LoadStakeView(ctx, sr) + index, err := contractStakingIndexerAt(b.indexer, sr, false) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to get contract staking indexer at height") } - if b.indexer.StartHeight() > height { - return view, nil - } - indexerHeight, err := b.indexer.Height() - if err != nil { - return nil, err - } - if indexerHeight == height { - return view, nil - } - if indexerHeight > height { - return nil, errors.Errorf("indexer height %d is greater than requested height %d", indexerHeight, height) - } - if b.blockdao == nil { - return nil, errors.Errorf("blockdao is nil, cannot build view for height %d", height) - } - for h := indexerHeight + 1; h <= height; h++ { - receipts, err := b.blockdao.GetReceipts(h) - if err != nil { - return nil, errors.Wrapf(err, "failed to get receipts at height %d", h) - } - header, err := b.blockdao.HeaderByHeight(h) - if err != nil { - return nil, errors.Wrapf(err, "failed to get header at height %d", h) - } - ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ - BlockHeight: h, - BlockTimeStamp: header.Timestamp(), - }) - if err = view.AddBlockReceipts(ctx, receipts); err != nil { - return nil, errors.Wrapf(err, "failed to build view with block at height %d", h) - } - } - return view, nil + return index.LoadStakeView(ctx, sr) } diff --git a/action/protocol/staking/staking_statereader_test.go b/action/protocol/staking/staking_statereader_test.go index e65654a904..162ec27605 100644 --- a/action/protocol/staking/staking_statereader_test.go +++ b/action/protocol/staking/staking_statereader_test.go @@ -165,7 +165,7 @@ func TestStakingStateReader(t *testing.T) { } t.Run("readStateBuckets", func(t *testing.T) { sf, _, stakeSR, ctx, r := prepare(t) - sf.EXPECT().States(gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 ...protocol.StateOption) (uint64, state.Iterator, error) { + sf.EXPECT().States(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 ...protocol.StateOption) (uint64, state.Iterator, error) { iter, err := state.NewIterator(keys, states) r.NoError(err) return uint64(1), iter, nil @@ -195,7 +195,7 @@ func TestStakingStateReader(t *testing.T) { }) t.Run("readStateBucketsWithEndorsement", func(t *testing.T) { sf, _, stakeSR, ctx, r := prepare(t) - sf.EXPECT().States(gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 ...protocol.StateOption) (uint64, state.Iterator, error) { + sf.EXPECT().States(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 ...protocol.StateOption) (uint64, state.Iterator, error) { iter, err := state.NewIterator(keys, states) r.NoError(err) return uint64(1), iter, nil diff --git a/action/protocol/staking/viewdata.go b/action/protocol/staking/viewdata.go index 036d4850a5..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 @@ -99,7 +108,7 @@ func (v *viewData) Snapshot() int { wrapped := v.contractsStake.Wrap() v.snapshots = append(v.snapshots, Snapshot{ size: v.candCenter.size, - changes: v.candCenter.change.size(), + changes: len(v.candCenter.change.candidates), amount: new(big.Int).Set(v.bucketPool.total.amount), count: v.bucketPool.total.count, contractsStake: v.contractsStake, @@ -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/action/protocol/staking/vote_bucket.go b/action/protocol/staking/vote_bucket.go index 890b21cf6b..c425146d1c 100644 --- a/action/protocol/staking/vote_bucket.go +++ b/action/protocol/staking/vote_bucket.go @@ -20,6 +20,7 @@ import ( "github.com/iotexproject/iotex-core/v2/action/protocol/staking/stakingpb" "github.com/iotexproject/iotex-core/v2/blockchain/genesis" "github.com/iotexproject/iotex-core/v2/pkg/util/byteutil" + "github.com/iotexproject/iotex-core/v2/systemcontracts" ) type ( @@ -73,6 +74,20 @@ func (vb *VoteBucket) Deserialize(buf []byte) error { return vb.fromProto(pb) } +// Encode encodes VoteBucket into generic value +func (vb *VoteBucket) Encode() (systemcontracts.GenericValue, error) { + data, err := vb.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize bucket") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes VoteBucket from generic value +func (vb *VoteBucket) Decode(gv systemcontracts.GenericValue) error { + return vb.Deserialize(gv.PrimaryData) +} + func (vb *VoteBucket) fromProto(pb *stakingpb.Bucket) error { vote, ok := new(big.Int).SetString(pb.GetStakedAmount(), 10) if !ok { @@ -209,6 +224,20 @@ func (tc *totalBucketCount) Count() uint64 { return tc.count } +// Encode encodes totalBucketCount into generic value +func (tc *totalBucketCount) Encode() (systemcontracts.GenericValue, error) { + data, err := tc.Serialize() + if err != nil { + return systemcontracts.GenericValue{}, errors.Wrap(err, "failed to serialize total bucket count") + } + return systemcontracts.GenericValue{PrimaryData: data}, nil +} + +// Decode decodes totalBucketCount from generic value +func (tc *totalBucketCount) Decode(gv systemcontracts.GenericValue) error { + return tc.Deserialize(gv.PrimaryData) +} + func bucketKey(index uint64) []byte { key := []byte{_bucket} return append(key, byteutil.Uint64ToBytesBigEndian(index)...) diff --git a/action/protocol/vote/probationlist.go b/action/protocol/vote/probationlist.go index 8cd8029e0e..8d931e9c54 100644 --- a/action/protocol/vote/probationlist.go +++ b/action/protocol/vote/probationlist.go @@ -13,9 +13,6 @@ import ( "github.com/iotexproject/iotex-proto/golang/iotextypes" - "github.com/iotexproject/iotex-core/v2/action/protocol" - "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" - "github.com/iotexproject/iotex-core/v2/state/factory/erigonstore" "github.com/iotexproject/iotex-core/v2/systemcontracts" ) @@ -25,10 +22,6 @@ type ProbationList struct { IntensityRate uint32 } -func init() { - assertions.MustNoError(erigonstore.GetObjectStorageRegistry().RegisterPollProbationList(protocol.SystemNamespace, &ProbationList{})) -} - // NewProbationList returns a new probation list func NewProbationList(intensity uint32) *ProbationList { return &ProbationList{ diff --git a/action/protocol/vote/unproductivedelegate.go b/action/protocol/vote/unproductivedelegate.go index 7822b690b4..afe05612a0 100644 --- a/action/protocol/vote/unproductivedelegate.go +++ b/action/protocol/vote/unproductivedelegate.go @@ -11,10 +11,7 @@ import ( "github.com/pkg/errors" "google.golang.org/protobuf/proto" - "github.com/iotexproject/iotex-core/v2/action/protocol" updpb "github.com/iotexproject/iotex-core/v2/action/protocol/vote/unproductivedelegatepb" - "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" - "github.com/iotexproject/iotex-core/v2/state/factory/erigonstore" "github.com/iotexproject/iotex-core/v2/systemcontracts" ) @@ -25,10 +22,6 @@ type UnproductiveDelegate struct { cacheSize uint64 } -func init() { - assertions.MustNoError(erigonstore.GetObjectStorageRegistry().RegisterPollUnproductiveDelegate(protocol.SystemNamespace, &UnproductiveDelegate{})) -} - // NewUnproductiveDelegate creates new UnproductiveDelegate with probationperiod and cacheSize func NewUnproductiveDelegate(probationPeriod uint64, cacheSize uint64) (*UnproductiveDelegate, error) { if probationPeriod > cacheSize { 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..51437e85c8 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,28 @@ 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)) + if ok && !featureCtx.StoreVoteOfNFTBucketIntoView { + return nil, nil } - ids, buckets, err := contractstaking.NewStateReader(sr).Buckets(s.contractAddr) + srHeight, err := sr.Height() if err != nil { - return nil, errors.Wrapf(err, "failed to get buckets for contract %s", 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)) - } - 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 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 { - 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]) + } + cur := stakingindex.AggregateCandidateVotes(buckets, func(b *contractstaking.Bucket) *big.Int { + return s.calculateUnmutedVoteWeightAt(b, s.height) + }) + processorBuilder := newEventProcessorBuilder(s.contractAddr) + cfg := &stakingindex.VoteViewConfig{ContractAddr: s.contractAddr} + mgr := stakingindex.NewCandidateVotesManager(s.ContractAddress()) + return stakingindex.NewVoteView(cfg, s.height, cur, processorBuilder, mgr, s.calculateUnmutedVoteWeightAt), nil } // Stop stops the indexer @@ -243,6 +206,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 +351,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 { @@ -406,6 +396,12 @@ func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error { return nil } +// IndexerAt returns the contract staking indexer at a specific height +func (s *Indexer) IndexerAt(sr protocol.StateReader) staking.ContractStakingIndexer { + epb := newEventProcessorBuilder(s.contractAddr) + return stakingindex.NewHistoryIndexer(sr, s.contractAddr, s.config.ContractDeployHeight, epb, s.calculateUnmutedVoteWeightAt) +} + func (s *Indexer) commit(ctx context.Context, handler *contractStakingDirty, height uint64) error { batch, delta := handler.Finalize() cache, err := delta.Commit(ctx, s.contractAddr, nil) @@ -467,3 +463,8 @@ func (s *Indexer) validateHeight(height uint64) error { } return nil } + +func (s *Indexer) calculateUnmutedVoteWeightAt(b *contractstaking.Bucket, height uint64) *big.Int { + vb := contractBucketToVoteBucket(0, b, s.contractAddr.String(), s.genBlockDurationFn(height)) + return s.config.CalculateVoteWeight(vb) +} 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..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 @@ -368,13 +369,17 @@ 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), + stakingindex.WithCalculateUnmutedVoteWeightFn(calculateVotesWeight), ) + if err != nil { + return err + } builder.cs.contractStakingIndexerV2 = indexer builder.cs.factory.AddDependency(indexer) } @@ -384,13 +389,17 @@ 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(), + stakingindex.WithCalculateUnmutedVoteWeightFn(calculateVotesWeight), ) + if err != nil { + return err + } builder.cs.contractStakingIndexerV3 = indexer builder.cs.factory.AddDependency(indexer) } 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/state/factory/erigonstore/accountstorage.go b/state/factory/erigonstore/accountstorage.go index 68eecc1181..7a87d6af2a 100644 --- a/state/factory/erigonstore/accountstorage.go +++ b/state/factory/erigonstore/accountstorage.go @@ -74,7 +74,11 @@ func (as *accountStorage) Load(key []byte, obj any) error { case accountpb.AccountType_ZERO_NONCE: pbAcc.Nonce = nonce case accountpb.AccountType_DEFAULT: - pbAcc.Nonce = nonce - 1 + if nonce == 0 { + pbAcc.Nonce = nonce + } else { + pbAcc.Nonce = nonce - 1 + } default: return errors.Errorf("unknown account type %v for address %x", pbAcc.Type, addr.Bytes()) } diff --git a/state/factory/erigonstore/objectstorage.go b/state/factory/erigonstore/objectstorage.go index 216c10d1e1..d5a75076dc 100644 --- a/state/factory/erigonstore/objectstorage.go +++ b/state/factory/erigonstore/objectstorage.go @@ -51,6 +51,9 @@ func (cos *contractObjectStorage) Load(key []byte, obj any) error { if err != nil { return err } + if !value.KeyExists { + return errors.Wrapf(state.ErrStateNotExist, "key %x does not exist", key) + } // TODO: handle value.KeyExists return gvc.Decode(value.Value) } diff --git a/state/factory/erigonstore/registry.go b/state/factory/erigonstore/registry.go index ce50dc8f3f..08f83baa25 100644 --- a/state/factory/erigonstore/registry.go +++ b/state/factory/erigonstore/registry.go @@ -2,10 +2,14 @@ package erigonstore import ( "reflect" + "strings" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" + "github.com/iotexproject/iotex-core/v2/action/protocol/poll" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" + "github.com/iotexproject/iotex-core/v2/action/protocol/vote" "github.com/iotexproject/iotex-core/v2/pkg/util/assertions" "github.com/iotexproject/iotex-core/v2/state" "github.com/iotexproject/iotex-core/v2/systemcontracts" @@ -25,12 +29,30 @@ var ( // ObjectStorageRegistry is a registry for object storage type ObjectStorageRegistry struct { contracts map[string]map[reflect.Type]int + ns map[string]int + nsPrefix map[string]int } func init() { - assertions.MustNoError(storageRegistry.RegisterAccount(state.AccountKVNamespace, &state.Account{})) - assertions.MustNoError(storageRegistry.RegisterPollCandidateList(state.SystemNamespace, &state.CandidateList{})) - assertions.MustNoError(storageRegistry.RegisterPollLegacyCandidateList(state.AccountKVNamespace, &state.CandidateList{})) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.AccountKVNamespace, RewardingContractV1Index)) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.RewardingNamespace, RewardingContractV2Index)) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandidateNamespace, CandidatesContractIndex)) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandsMapNamespace, CandidateMapContractIndex)) + 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)) + assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.SystemNamespace, &state.CandidateList{}, PollCandidateListContractIndex)) + assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.SystemNamespace, &vote.UnproductiveDelegate{}, PollUnproductiveDelegateContractIndex)) + assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.SystemNamespace, &vote.ProbationList{}, PollProbationListContractIndex)) + assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.SystemNamespace, &poll.BlockMeta{}, PollBlockMetaContractIndex)) + assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.StakingNamespace, &staking.VoteBucket{}, StakingBucketsContractIndex)) + assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.StakingNamespace, &staking.Endorsement{}, EndorsementContractIndex)) + assertions.MustNoError(storageRegistry.RegisterObjectStorage(state.StakingNamespace, &staking.BucketIndices{}, BucketIndicesContractIndex)) } // GetObjectStorageRegistry returns the global object storage registry @@ -41,18 +63,16 @@ func GetObjectStorageRegistry() *ObjectStorageRegistry { func newObjectStorageRegistry() *ObjectStorageRegistry { return &ObjectStorageRegistry{ contracts: make(map[string]map[reflect.Type]int), + ns: make(map[string]int), + nsPrefix: make(map[string]int), } } // ObjectStorage returns the object storage for the given namespace and object type func (osr *ObjectStorageRegistry) ObjectStorage(ns string, obj any, backend *contractBackend) (ObjectStorage, error) { - types, ok := osr.contracts[ns] - if !ok { - return nil, errors.Wrapf(ErrObjectStorageNotRegistered, "namespace: %s", ns) - } - contractIndex, ok := types[reflect.TypeOf(obj)] - if !ok { - return nil, errors.Wrapf(ErrObjectStorageNotRegistered, "namespace: %s, object: %T", ns, obj) + contractIndex, exist := osr.matchContractIndex(ns, obj) + if !exist { + return nil, errors.Wrapf(ErrObjectStorageNotRegistered, "namespace: %s, type: %T", ns, obj) } // TODO: cache storage switch systemContractTypes[contractIndex] { @@ -63,7 +83,7 @@ func (osr *ObjectStorageRegistry) ObjectStorage(ns string, obj any, backend *con ) case namespaceStorageContractType: contractAddr := systemContracts[contractIndex].Address - contract, err := systemcontracts.NewGenericStorageContract(common.BytesToAddress(contractAddr.Bytes()[:]), backend, common.Address(systemContractCreatorAddr)) + contract, err := systemcontracts.NewNamespaceStorageContractWrapper(common.BytesToAddress(contractAddr.Bytes()[:]), backend, common.Address(systemContractCreatorAddr), ns) if err != nil { return nil, err } @@ -78,82 +98,46 @@ func (osr *ObjectStorageRegistry) ObjectStorage(ns string, obj any, backend *con } } -// RegisterAccount registers an account object storage -func (osr *ObjectStorageRegistry) RegisterAccount(ns string, obj any) error { - return osr.register(ns, obj, AccountIndex) -} - -// RegisterStakingBuckets registers a staking buckets object storage -func (osr *ObjectStorageRegistry) RegisterStakingBuckets(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, StakingBucketsContractIndex) -} - -// RegisterBucketPool registers a bucket pool object storage -func (osr *ObjectStorageRegistry) RegisterBucketPool(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, BucketPoolContractIndex) -} - -// RegisterBucketIndices registers a bucket indices object storage -func (osr *ObjectStorageRegistry) RegisterBucketIndices(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, BucketIndicesContractIndex) -} - -// RegisterEndorsement registers an endorsement object storage -func (osr *ObjectStorageRegistry) RegisterEndorsement(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, EndorsementContractIndex) -} - -// RegisterCandidateMap registers a candidate map object storage -func (osr *ObjectStorageRegistry) RegisterCandidateMap(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, CandidateMapContractIndex) -} - -// RegisterCandidates registers a candidates object storage -func (osr *ObjectStorageRegistry) RegisterCandidates(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, CandidatesContractIndex) -} - -// RegisterPollCandidateList registers a poll candidate list object storage -func (osr *ObjectStorageRegistry) RegisterPollCandidateList(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, PollCandidateListContractIndex) -} - -// RegisterPollLegacyCandidateList registers a poll legacy candidate list object storage -func (osr *ObjectStorageRegistry) RegisterPollLegacyCandidateList(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, PollLegacyCandidateListContractIndex) -} - -// RegisterPollProbationList registers a poll probation list object storage -func (osr *ObjectStorageRegistry) RegisterPollProbationList(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, PollProbationListContractIndex) -} - -// RegisterPollUnproductiveDelegate registers a poll unproductive delegate object storage -func (osr *ObjectStorageRegistry) RegisterPollUnproductiveDelegate(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, PollUnproductiveDelegateContractIndex) -} - -// RegisterPollBlockMeta registers a poll block meta object storage -func (osr *ObjectStorageRegistry) RegisterPollBlockMeta(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, PollBlockMetaContractIndex) +// RegisterObjectStorage registers a generic object storage +func (osr *ObjectStorageRegistry) RegisterObjectStorage(ns string, obj any, index int) error { + if index < AccountIndex || index >= SystemContractCount { + return errors.Errorf("invalid system contract index %d", index) + } + return osr.register(ns, obj, index) } -// RegisterRewardingV1 registers a rewarding v1 object storage -func (osr *ObjectStorageRegistry) RegisterRewardingV1(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, RewardingContractV1Index) +// RegisterNamespace registers a namespace object storage +func (osr *ObjectStorageRegistry) RegisterNamespace(ns string, index int) error { + if index < AccountIndex || index >= SystemContractCount { + return errors.Errorf("invalid system contract index %d", index) + } + return osr.register(ns, nil, index) } -// RegisterRewardingV2 registers a rewarding v2 object storage -func (osr *ObjectStorageRegistry) RegisterRewardingV2(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, RewardingContractV2Index) +// 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) } -// RegisterStakingView registers a staking view object storage -func (osr *ObjectStorageRegistry) RegisterStakingView(ns string, obj systemcontracts.GenericValueContainer) error { - return osr.register(ns, obj, StakingViewContractIndex) +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.ns[ns]; exists { + return errors.Wrapf(ErrObjectStorageAlreadyRegistered, "registered: %v", osr.ns[ns]) + } + osr.ns[ns] = index + return nil + } types, ok := osr.contracts[ns] if !ok { osr.contracts[ns] = make(map[reflect.Type]int) @@ -165,3 +149,28 @@ func (osr *ObjectStorageRegistry) register(ns string, obj any, index int) error types[reflect.TypeOf(obj)] = index return nil } + +func (osr *ObjectStorageRegistry) matchContractIndex(ns string, obj any) (int, bool) { + // object specific storage + if obj != nil { + types, ok := osr.contracts[ns] + if ok { + index, exist := types[reflect.TypeOf(obj)] + if exist { + return index, true + } + } + } + // 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/erigonstore/workingsetstore_erigon.go b/state/factory/erigonstore/workingsetstore_erigon.go index 62f482444c..6d03c0fc20 100644 --- a/state/factory/erigonstore/workingsetstore_erigon.go +++ b/state/factory/erigonstore/workingsetstore_erigon.go @@ -428,15 +428,7 @@ func (store *ErigonWorkingSetStore) StateReader() erigonstate.StateReader { return store.backend.org } +// NewObjectStorage creates a new ObjectStorage for the given namespace and object type func (store *ErigonWorkingSetStore) NewObjectStorage(ns string, obj any) (ObjectStorage, error) { - cs, err := storageRegistry.ObjectStorage(ns, obj, store.backend) - switch errors.Cause(err) { - case nil: - return cs, nil - case ErrObjectStorageNotRegistered: - // TODO: fail unknown namespace - return nil, nil - default: - return nil, err - } + return storageRegistry.ObjectStorage(ns, obj, store.backend) } diff --git a/state/factory/statedb.go b/state/factory/statedb.go index 0db3aa5811..86d6dcb4a0 100644 --- a/state/factory/statedb.go +++ b/state/factory/statedb.go @@ -8,7 +8,9 @@ package factory import ( "context" "fmt" + "slices" "strconv" + "strings" "sync" "time" @@ -237,11 +239,20 @@ func (sdb *stateDB) AddDependency(indexer blockdao.BlockIndexer) { } func (sdb *stateDB) newReadOnlyWorkingSet(ctx context.Context, height uint64) (*workingSet, error) { - return sdb.newWorkingSetWithKVStore(ctx, height, &readOnlyKV{sdb.dao.atHeight(height)}) + ws, err := sdb.newWorkingSetWithKVStore(ctx, height, &readOnlyKV{sdb.dao.atHeight(height)}, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to create new read-only working set") + } + views, err := sdb.registry.StartAll(ctx, ws) + if err != nil { + return nil, errors.Wrap(err, "failed to create new read-only working set") + } + ws.views = views + return ws, nil } func (sdb *stateDB) newWorkingSet(ctx context.Context, height uint64) (*workingSet, error) { - ws, err := sdb.newWorkingSetWithKVStore(ctx, height, sdb.dao.atHeight(height)) + ws, err := sdb.newWorkingSetWithKVStore(ctx, height, sdb.dao.atHeight(height), sdb.protocolViews.Fork()) if err != nil { return nil, errors.Wrap(err, "failed to create new working set") } @@ -259,7 +270,7 @@ func (sdb *stateDB) newWorkingSet(ctx context.Context, height uint64) (*workingS return ws, nil } -func (sdb *stateDB) newWorkingSetWithKVStore(ctx context.Context, height uint64, kvstore db.KVStore) (*workingSet, error) { +func (sdb *stateDB) newWorkingSetWithKVStore(ctx context.Context, height uint64, kvstore db.KVStore, views *protocol.Views) (*workingSet, error) { store, err := sdb.createWorkingSetStore(ctx, height, kvstore) if err != nil { return nil, err @@ -267,7 +278,7 @@ func (sdb *stateDB) newWorkingSetWithKVStore(ctx context.Context, height uint64, if err := store.Start(ctx); err != nil { return nil, err } - return newWorkingSet(height, sdb.protocolViews.Fork(), store, sdb), nil + return newWorkingSet(height, views, store, sdb), nil } func (sdb *stateDB) CreateWorkingSetStore(ctx context.Context, height uint64, kvstore db.KVStore) (workingSetStore, error) { @@ -282,7 +293,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 @@ -558,7 +569,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 { @@ -567,15 +578,46 @@ func (sdb *stateDB) flusherOptions(preEaster bool) []db.KVStoreFlusherOption { return wi.Serialize() }), } - if !preEaster { - return opts + 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, + ) } - return append( - opts, + opts = append(opts, + db.FlushTranslateOption(func(wi *batch.WriteInfo) *batch.WriteInfo { + 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 wi.Namespace() == evm.CodeKVNameSpace || wi.Namespace() == staking.CandsMapNS + return slices.Contains(serializeFilterNs, wi.Namespace()) || + slices.ContainsFunc(serializeFilterNsPrefixes, func(prefix string) bool { + return strings.HasPrefix(wi.Namespace(), prefix) + }) }), ) + return opts } func (sdb *stateDB) state(h uint64, ns string, addr []byte, s interface{}) error { diff --git a/state/factory/workingsetstore_test.go b/state/factory/workingsetstore_test.go index de104e6e5b..098cea7884 100644 --- a/state/factory/workingsetstore_test.go +++ b/state/factory/workingsetstore_test.go @@ -52,7 +52,7 @@ func TestStateDBWorkingSetStore(t *testing.T) { var value valueBytes err := store.GetObject(namespace, key1, &value) require.Error(err) - require.NoError(store.DeleteObject(namespace, key1, &value)) + require.NoError(store.DeleteObject(namespace, key1, nil)) require.NoError(store.PutObject(namespace, key1, &value1)) var valueInStore valueBytes err = store.GetObject(namespace, key1, &valueInStore) @@ -88,7 +88,7 @@ func TestStateDBWorkingSetStore(t *testing.T) { require.Equal("e1f83be0a44ae601061724990036b8a40edbf81cffc639657c9bb2c5d384defa", hex.EncodeToString(h[:])) }) sn3 := store.Snapshot() - require.NoError(store.DeleteObject(namespace, key1, &valueInStore)) + require.NoError(store.DeleteObject(namespace, key1, nil)) err = store.GetObject(namespace, key1, &valueInStore) require.Error(err) iter, err = store.States(namespace, &valueInStore, [][]byte{key1, key2, key3}) diff --git a/state/tables.go b/state/tables.go index 97e9486d84..ab5e6ddf88 100644 --- a/state/tables.go +++ b/state/tables.go @@ -38,6 +38,19 @@ const ( // - "4" + --> Endorsement StakingNamespace = "Staking" + // StakingViewNamespace is the namespace to store staking view information + // - "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" diff --git a/systemcontractindex/stakingindex/candidate_votes.go b/systemcontractindex/stakingindex/candidate_votes.go new file mode 100644 index 0000000000..8fc8b8dc91 --- /dev/null +++ b/systemcontractindex/stakingindex/candidate_votes.go @@ -0,0 +1,231 @@ +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" + "github.com/iotexproject/iotex-core/v2/systemcontracts" +) + +// 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) 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 +} + +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/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/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/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/history.go b/systemcontractindex/stakingindex/history.go new file mode 100644 index 0000000000..c301de4e99 --- /dev/null +++ b/systemcontractindex/stakingindex/history.go @@ -0,0 +1,120 @@ +package stakingindex + +import ( + "context" + "errors" + + "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/blockchain/block" +) + +// historyIndexer implements historical staking indexer +type historyIndexer struct { + sr protocol.StateReader + startHeight uint64 + contractAddr address.Address + epb EventProcessorBuilder + cuvwFn CalculateUnmutedVoteWeightAtFn +} + +// NewHistoryIndexer creates a new instance of historyIndexer +func NewHistoryIndexer(sr protocol.StateReader, contract address.Address, startHeight uint64, epb EventProcessorBuilder, cuvwFn CalculateUnmutedVoteWeightAtFn) staking.ContractStakingIndexer { + return &historyIndexer{ + sr: sr, + contractAddr: contract, + startHeight: startHeight, + epb: epb, + cuvwFn: cuvwFn, + } +} + +func (h *historyIndexer) Start(ctx context.Context) error { + return nil +} + +func (h *historyIndexer) Stop(ctx context.Context) error { + return nil +} + +func (h *historyIndexer) PutBlock(ctx context.Context, blk *block.Block) error { + return errors.New("not implemented") +} + +// StartHeight returns the start height of the indexer +func (h *historyIndexer) StartHeight() uint64 { + return h.startHeight +} + +// Height returns the latest indexed height +func (h *historyIndexer) Height() (uint64, error) { + return h.sr.Height() +} + +func (h *historyIndexer) Buckets(height uint64) ([]*VoteBucket, error) { + return nil, errors.New("not implemented") +} + +// BucketsByIndices returns active buckets by indices +func (h *historyIndexer) BucketsByIndices([]uint64, uint64) ([]*VoteBucket, error) { + return nil, errors.New("not implemented") +} + +// BucketsByCandidate returns active buckets by candidate +func (h *historyIndexer) BucketsByCandidate(ownerAddr address.Address, height uint64) ([]*VoteBucket, error) { + return nil, errors.New("not implemented") +} + +func (h *historyIndexer) TotalBucketCount(height uint64) (uint64, error) { + return 0, errors.New("not implemented") +} + +func (h *historyIndexer) ContractAddress() address.Address { + return h.contractAddr +} + +func (h *historyIndexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (staking.ContractStakeView, error) { + cvm := NewCandidateVotesManager(h.contractAddr) + cur, err := cvm.Load(ctx, sr) + if err != nil { + return nil, err + } + height, err := sr.Height() + if err != nil { + return nil, err + } + return NewVoteView(&VoteViewConfig{ContractAddr: h.contractAddr}, height, cur, h.epb, cvm, h.cuvwFn), nil +} + +func (h *historyIndexer) CreateEventProcessor(ctx context.Context, handler staking.EventHandler) staking.EventProcessor { + return h.epb.Build(ctx, handler) +} + +func (h *historyIndexer) ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) { + cssr := contractstaking.NewStateReader(h.sr) + idxs, btks, err := cssr.Buckets(h.contractAddr) + if err != nil { + return 0, nil, err + } + buckets := make(map[uint64]*contractstaking.Bucket) + for i, id := range idxs { + buckets[id] = btks[i] + } + height, err := h.sr.Height() + if err != nil { + return 0, nil, err + } + return height, buckets, nil +} + +func (h *historyIndexer) DeductBucket(addr address.Address, id uint64) (*contractstaking.Bucket, error) { + cssr := contractstaking.NewStateReader(h.sr) + return cssr.Bucket(addr, id) +} + +func (h *historyIndexer) IndexerAt(sr protocol.StateReader) staking.ContractStakingIndexer { + return NewHistoryIndexer(sr, h.contractAddr, h.startHeight, h.epb, h.cuvwFn) +} diff --git a/systemcontractindex/stakingindex/index.go b/systemcontractindex/stakingindex/index.go index f8e0fb77ed..e9f26f4e4d 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,28 @@ 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 + IndexerAt(protocol.StateReader) staking.ContractStakingIndexer } // 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 +81,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 +102,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 +143,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 +164,33 @@ 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) + 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 s.common.StartHeight() <= srHeight && 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(), + } + mgr := NewCandidateVotesManager(s.ContractAddress()) + processorBuilder := newEventProcessorBuilder(s.common.ContractAddress(), s.timestamped, s.muteHeight) + return NewVoteView(cfg, s.common.Height(), s.createCandidateVotes(s.cache.buckets), processorBuilder, mgr, 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 @@ -319,6 +338,12 @@ func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error { return s.commit(ctx, handler, blk.Height()) } +// IndexerAt returns the staking indexer at the given state reader +func (s *Indexer) IndexerAt(sr protocol.StateReader) staking.ContractStakingIndexer { + epb := newEventProcessorBuilder(s.common.ContractAddress(), s.timestamped, s.muteHeight) + return NewHistoryIndexer(sr, s.common.ContractAddress(), s.common.StartHeight(), epb, s.calculateContractVoteWeight) +} + func (s *Indexer) commit(ctx context.Context, handler *eventHandler, height uint64) error { delta, dirty := handler.Finalize() // update db @@ -358,3 +383,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..d9212d75da --- /dev/null +++ b/systemcontractindex/stakingindex/voteview.go @@ -0,0 +1,145 @@ +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 + cvm CandidateVotesManager + processorBuilder EventProcessorBuilder + calculateVoteWeightFn CalculateUnmutedVoteWeightAtFn + } +) + +// NewVoteView creates a new vote view +func NewVoteView(cfg *VoteViewConfig, + height uint64, + cur CandidateVotes, + processorBuilder EventProcessorBuilder, + cvm CandidateVotesManager, + fn CalculateUnmutedVoteWeightAtFn, +) staking.ContractStakeView { + return &voteView{ + config: cfg, + height: height, + cur: cur, + processorBuilder: processorBuilder, + cvm: cvm, + 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, + cvm: s.cvm, + 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, + cvm: s.cvm, + 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 s.cvm.Store(ctx, sm, s.cur) +} diff --git a/systemcontracts/namespace_storage_wrapper.go b/systemcontracts/namespace_storage_wrapper.go index 2b957cfd27..c994ced1cc 100644 --- a/systemcontracts/namespace_storage_wrapper.go +++ b/systemcontracts/namespace_storage_wrapper.go @@ -65,13 +65,13 @@ func (ns *NamespaceStorageContractWrapper) BatchPut(keys [][]byte, values []Name } // List retrieves all stored data in a namespace with pagination -func (ns *NamespaceStorageContractWrapper) List(offset, limit *big.Int) (*NamespaceListResult, error) { - return ns.contract.List(ns.ns, offset, limit) +func (ns *NamespaceStorageContractWrapper) List(offset, limit uint64) (*NamespaceListResult, error) { + return ns.contract.List(ns.ns, big.NewInt(int64(offset)), big.NewInt(int64(limit))) } // ListKeys retrieves all keys in a namespace with pagination -func (ns *NamespaceStorageContractWrapper) ListKeys(offset, limit *big.Int) (*NamespaceListKeysResult, error) { - return ns.contract.ListKeys(ns.ns, offset, limit) +func (ns *NamespaceStorageContractWrapper) ListKeys(offset, limit uint64) (*NamespaceListKeysResult, error) { + return ns.contract.ListKeys(ns.ns, big.NewInt(int64(offset)), big.NewInt(int64(limit))) } // Count returns the number of items in the namespace