diff --git a/Makefile b/Makefile index 24e4e3f2d2..56098b1868 100644 --- a/Makefile +++ b/Makefile @@ -256,7 +256,7 @@ recover: .PHONY: ioctl ioctl: - $(GOBUILD) -ldflags "$(PackageFlags)" -o ./bin/$(BUILD_TARGET_IOCTL) -v ./tools/ioctl + $(GOBUILD) -tags $(BUILD_TAGS) -ldflags "$(PackageFlags)" -o ./bin/$(BUILD_TARGET_IOCTL) -v ./tools/ioctl .PHONY: newioctl newioctl: diff --git a/action/protocol/context.go b/action/protocol/context.go index 6ef68e1ffe..8dff16ad1b 100644 --- a/action/protocol/context.go +++ b/action/protocol/context.go @@ -325,10 +325,10 @@ func WithFeatureCtx(ctx context.Context) context.Context { TimestampedStakingContract: g.IsWake(height), PreStateSystemAction: !g.IsWake(height), CreatePostActionStates: g.IsWake(height), - NotSlashUnproductiveDelegates: !g.IsToBeEnabled(height), - CandidateBLSPublicKey: g.IsToBeEnabled(height), - NotUseMinSelfStakeToBeActive: !g.IsToBeEnabled(height), - StoreVoteOfNFTBucketIntoView: !g.IsToBeEnabled(height), + NotSlashUnproductiveDelegates: !g.IsXingu(height), + CandidateBLSPublicKey: g.IsXingu(height), + NotUseMinSelfStakeToBeActive: !g.IsXingu(height), + StoreVoteOfNFTBucketIntoView: !g.IsXingu(height), }, ) } @@ -351,7 +351,7 @@ func GetFeatureCtx(ctx context.Context) (FeatureCtx, bool) { func MustGetFeatureCtx(ctx context.Context) FeatureCtx { fc, ok := ctx.Value(featureContextKey{}).(FeatureCtx) if !ok { - log.S().Panic("Miss feature context") + log.L().Panic("Miss feature context") } return fc } diff --git a/action/protocol/execution/evm/evmstatedbadapter.go b/action/protocol/execution/evm/evmstatedbadapter.go index f9a8b360b2..d19897a9f5 100644 --- a/action/protocol/execution/evm/evmstatedbadapter.go +++ b/action/protocol/execution/evm/evmstatedbadapter.go @@ -1092,7 +1092,7 @@ func (stateDB *StateDBAdapter) CommitContracts() error { sort.Slice(contractAddrs, func(i, j int) bool { return bytes.Compare(contractAddrs[i][:], contractAddrs[j][:]) < 0 }) for _, addr := range contractAddrs { - _, err := stateDB.sm.DelState(protocol.KeyOption(addr[:])) + _, err := stateDB.sm.DelState(protocol.KeyOption(addr[:]), protocol.ObjectOption(&state.Account{})) if stateDB.assertError(err, "failed to delete SelfDestruct account/contract", zap.Error(err), zap.String("address", addr.Hex())) { return errors.Wrapf(err, "failed to delete SelfDestruct account/contract %x", addr[:]) } diff --git a/action/protocol/managers.go b/action/protocol/managers.go index 7b24c616c6..e633e22e35 100644 --- a/action/protocol/managers.go +++ b/action/protocol/managers.go @@ -62,13 +62,22 @@ func ObjectOption(obj any) StateOption { } } +// ErigonStoreOnlyOption sets the option to only read/write from/to erigon store +func ErigonStoreOnlyOption() StateOption { + return func(cfg *StateConfig) error { + cfg.ErigonStoreOnly = true + return nil + } +} + type ( // StateConfig is the config for accessing stateDB StateConfig struct { - Namespace string // namespace used by state's storage - Key []byte - Keys [][]byte - Object any // object used by state's storage + Namespace string // namespace used by state's storage + Key []byte + Keys [][]byte + Object any // object used by state's storage + ErigonStoreOnly bool // whether only read/write from/to erigon store } // StateOption sets parameter for access state diff --git a/action/protocol/rewarding/protocol_test.go b/action/protocol/rewarding/protocol_test.go index 9ed2e52a12..84668f2e21 100644 --- a/action/protocol/rewarding/protocol_test.go +++ b/action/protocol/rewarding/protocol_test.go @@ -73,7 +73,7 @@ func testProtocol(t *testing.T, test func(*testing.T, context.Context, protocol. g.Rewarding.NumDelegatesForFoundationBonus = 5 g.Rewarding.FoundationBonusLastEpoch = 365 g.Rewarding.ProductivityThreshold = 50 - g.ToBeEnabledBlockHeight = slashHeight + g.XinguBlockHeight = slashHeight // Initialize the protocol if withExempt { g.Rewarding.ExemptAddrStrsFromEpochReward = []string{ diff --git a/action/protocol/staking/contractstake_indexer.go b/action/protocol/staking/contractstake_indexer.go index f574ea80c8..7497f27a44 100644 --- a/action/protocol/staking/contractstake_indexer.go +++ b/action/protocol/staking/contractstake_indexer.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/iotexproject/iotex-address/address" + "github.com/iotexproject/iotex-core/v2/action" "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" @@ -64,6 +65,10 @@ type ( LoadStakeView(context.Context, protocol.StateReader) (ContractStakeView, error) // CreateEventProcessor creates a new event processor CreateEventProcessor(context.Context, EventHandler) EventProcessor + // ContractStakingBuckets returns all the contract staking buckets + ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) + + BucketReader } // ContractStakingIndexerWithBucketType defines the interface of contract staking reader with bucket type ContractStakingIndexerWithBucketType interface { diff --git a/action/protocol/staking/contractstake_indexer_mock.go b/action/protocol/staking/contractstake_indexer_mock.go index 7b6047db46..e1da4905a9 100644 --- a/action/protocol/staking/contractstake_indexer_mock.go +++ b/action/protocol/staking/contractstake_indexer_mock.go @@ -228,6 +228,22 @@ func (mr *MockContractStakingIndexerMockRecorder) ContractAddress() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractAddress", reflect.TypeOf((*MockContractStakingIndexer)(nil).ContractAddress)) } +// ContractStakingBuckets mocks base method. +func (m *MockContractStakingIndexer) ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ContractStakingBuckets") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(map[uint64]*contractstaking.Bucket) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ContractStakingBuckets indicates an expected call of ContractStakingBuckets. +func (mr *MockContractStakingIndexerMockRecorder) ContractStakingBuckets() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractStakingBuckets", reflect.TypeOf((*MockContractStakingIndexer)(nil).ContractStakingBuckets)) +} + // CreateEventProcessor mocks base method. func (m *MockContractStakingIndexer) CreateEventProcessor(arg0 context.Context, arg1 EventHandler) EventProcessor { m.ctrl.T.Helper() @@ -242,6 +258,21 @@ func (mr *MockContractStakingIndexerMockRecorder) CreateEventProcessor(arg0, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEventProcessor", reflect.TypeOf((*MockContractStakingIndexer)(nil).CreateEventProcessor), arg0, arg1) } +// DeductBucket mocks base method. +func (m *MockContractStakingIndexer) DeductBucket(arg0 address.Address, arg1 uint64) (*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeductBucket", arg0, arg1) + ret0, _ := ret[0].(*contractstaking.Bucket) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeductBucket indicates an expected call of DeductBucket. +func (mr *MockContractStakingIndexerMockRecorder) DeductBucket(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeductBucket", reflect.TypeOf((*MockContractStakingIndexer)(nil).DeductBucket), arg0, arg1) +} + // Height mocks base method. func (m *MockContractStakingIndexer) Height() (uint64, error) { m.ctrl.T.Helper() @@ -441,6 +472,22 @@ func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) ContractAddress( return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractAddress", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).ContractAddress)) } +// ContractStakingBuckets mocks base method. +func (m *MockContractStakingIndexerWithBucketType) ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ContractStakingBuckets") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(map[uint64]*contractstaking.Bucket) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ContractStakingBuckets indicates an expected call of ContractStakingBuckets. +func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) ContractStakingBuckets() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContractStakingBuckets", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).ContractStakingBuckets)) +} + // CreateEventProcessor mocks base method. func (m *MockContractStakingIndexerWithBucketType) CreateEventProcessor(arg0 context.Context, arg1 EventHandler) EventProcessor { m.ctrl.T.Helper() @@ -455,6 +502,21 @@ func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) CreateEventProce return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEventProcessor", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).CreateEventProcessor), arg0, arg1) } +// DeductBucket mocks base method. +func (m *MockContractStakingIndexerWithBucketType) DeductBucket(arg0 address.Address, arg1 uint64) (*contractstaking.Bucket, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeductBucket", arg0, arg1) + ret0, _ := ret[0].(*contractstaking.Bucket) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeductBucket indicates an expected call of DeductBucket. +func (mr *MockContractStakingIndexerWithBucketTypeMockRecorder) DeductBucket(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeductBucket", reflect.TypeOf((*MockContractStakingIndexerWithBucketType)(nil).DeductBucket), arg0, arg1) +} + // Height mocks base method. func (m *MockContractStakingIndexerWithBucketType) Height() (uint64, error) { m.ctrl.T.Helper() diff --git a/action/protocol/staking/contractstakeview_mock.go b/action/protocol/staking/contractstakeview_mock.go new file mode 100644 index 0000000000..b50bf487ce --- /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) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePreStates", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreatePreStates indicates an expected call of CreatePreStates. +func (mr *MockContractStakeViewMockRecorder) CreatePreStates(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePreStates", reflect.TypeOf((*MockContractStakeView)(nil).CreatePreStates), ctx) +} + +// 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 context.Context, arg1 EventHandler) 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 context.Context) { + 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..dbd41f4406 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 ( @@ -23,7 +25,7 @@ type ( StakedDuration uint64 // in seconds if timestamped, in block number if not // CreatedAt is the time when the bucket was created. CreatedAt uint64 // in unix timestamp if timestamped, in block height if not - // UnlockedAt is the time when the bucket can be unlocked. + // UnlockedAt is the time when the bucket was unlocked. UnlockedAt uint64 // in unix timestamp if timestamped, in block height if not // UnstakedAt is the time when the bucket was unstaked. UnstakedAt uint64 // in unix timestamp if timestamped, in block height if not @@ -43,14 +45,15 @@ func (b *Bucket) toProto() *stakingpb.SystemStakingBucket { return nil } return &stakingpb.SystemStakingBucket{ - Owner: b.Owner.Bytes(), - Candidate: b.Candidate.Bytes(), - Amount: b.StakedAmount.Bytes(), - Duration: b.StakedDuration, - CreatedAt: b.CreatedAt, - UnlockedAt: b.UnlockedAt, - UnstakedAt: b.UnstakedAt, - Muted: b.Muted, + Owner: b.Owner.String(), + Candidate: b.Candidate.String(), + Amount: b.StakedAmount.String(), + Duration: b.StakedDuration, + CreatedAt: b.CreatedAt, + UnlockedAt: b.UnlockedAt, + UnstakedAt: b.UnstakedAt, + Muted: b.Muted, + Timestamped: b.IsTimestampBased, } } @@ -60,22 +63,27 @@ func LoadBucketFromProto(pb *stakingpb.SystemStakingBucket) (*Bucket, error) { return nil, nil } b := &Bucket{} - owner, err := address.FromBytes(pb.Owner) + owner, err := address.FromString(pb.Owner) if err != nil { return nil, errors.Wrap(err, "failed to convert owner bytes to address") } b.Owner = owner - cand, err := address.FromBytes(pb.Candidate) + cand, err := address.FromString(pb.Candidate) if err != nil { return nil, errors.Wrap(err, "failed to convert candidate bytes to address") } + amount, ok := new(big.Int).SetString(pb.Amount, 10) + if !ok { + return nil, errors.Errorf("invalid staked amount %s", pb.Amount) + } b.Candidate = cand - b.StakedAmount = new(big.Int).SetBytes(pb.Amount) + b.StakedAmount = amount b.StakedDuration = pb.Duration b.CreatedAt = pb.CreatedAt b.UnlockedAt = pb.UnlockedAt b.UnstakedAt = pb.UnstakedAt b.Muted = pb.Muted + b.IsTimestampBased = pb.Timestamped return b, nil } @@ -113,3 +121,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/statemanager.go b/action/protocol/staking/contractstaking/statemanager.go index b376a2f84f..c8cf89e1b2 100644 --- a/action/protocol/staking/contractstaking/statemanager.go +++ b/action/protocol/staking/contractstaking/statemanager.go @@ -12,9 +12,9 @@ type ContractStakingStateManager struct { } // NewContractStakingStateManager creates a new ContractStakingStateManager -func NewContractStakingStateManager(sm protocol.StateManager) *ContractStakingStateManager { +func NewContractStakingStateManager(sm protocol.StateManager, opts ...protocol.StateOption) *ContractStakingStateManager { return &ContractStakingStateManager{ - ContractStakingStateReader: ContractStakingStateReader{sr: sm}, + ContractStakingStateReader: *NewStateReader(sm, opts...), sm: sm, } } @@ -23,8 +23,10 @@ func NewContractStakingStateManager(sm protocol.StateManager) *ContractStakingSt func (cs *ContractStakingStateManager) UpsertBucketType(contractAddr address.Address, bucketID uint64, bucketType *BucketType) error { _, err := cs.sm.PutState( bucketType, - bucketTypeNamespaceOption(contractAddr), - bucketIDKeyOption(bucketID), + cs.makeOpts( + bucketTypeNamespaceOption(contractAddr), + bucketIDKeyOption(bucketID), + )..., ) return err @@ -33,8 +35,11 @@ func (cs *ContractStakingStateManager) UpsertBucketType(contractAddr address.Add // DeleteBucket removes a bucket for a given contract and bucket ID. func (cs *ContractStakingStateManager) DeleteBucket(contractAddr address.Address, bucketID uint64) error { _, err := cs.sm.DelState( - contractNamespaceOption(contractAddr), - bucketIDKeyOption(bucketID), + cs.makeOpts( + contractNamespaceOption(contractAddr), + bucketIDKeyOption(bucketID), + protocol.ObjectOption(&Bucket{}), + )..., ) return err @@ -44,8 +49,10 @@ func (cs *ContractStakingStateManager) DeleteBucket(contractAddr address.Address func (cs *ContractStakingStateManager) UpsertBucket(contractAddr address.Address, bid uint64, bucket *Bucket) error { _, err := cs.sm.PutState( bucket, - contractNamespaceOption(contractAddr), - bucketIDKeyOption(bid), + cs.makeOpts( + contractNamespaceOption(contractAddr), + bucketIDKeyOption(bid), + )..., ) return err @@ -57,8 +64,10 @@ func (cs *ContractStakingStateManager) UpdateNumOfBuckets(contractAddr address.A &StakingContract{ NumOfBuckets: uint64(numOfBuckets), }, - metaNamespaceOption(), - contractKeyOption(contractAddr), + cs.makeOpts( + metaNamespaceOption(), + contractKeyOption(contractAddr), + )..., ) return err diff --git a/action/protocol/staking/contractstaking/statemanager_test.go b/action/protocol/staking/contractstaking/statemanager_test.go index c7714a6fcd..a0e16b9766 100644 --- a/action/protocol/staking/contractstaking/statemanager_test.go +++ b/action/protocol/staking/contractstaking/statemanager_test.go @@ -69,7 +69,7 @@ func TestDeleteBucket_Error(t *testing.T) { defer ctrl.Finish() mockSM := mock_chainmanager.NewMockStateManager(ctrl) - mockSM.EXPECT().DelState(gomock.Any(), gomock.Any()).Return(uint64(0), errors.New("delstate error")) + mockSM.EXPECT().DelState(gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(0), errors.New("delstate error")) csm := NewContractStakingStateManager(mockSM) contractAddr := identityset.Address(1) @@ -86,7 +86,7 @@ func TestDeleteBucket_Success(t *testing.T) { defer ctrl.Finish() mockSM := mock_chainmanager.NewMockStateManager(ctrl) - mockSM.EXPECT().DelState(gomock.Any(), gomock.Any()).Return(uint64(0), nil) + mockSM.EXPECT().DelState(gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(0), nil) csm := NewContractStakingStateManager(mockSM) contractAddr := identityset.Address(1) diff --git a/action/protocol/staking/contractstaking/statereader.go b/action/protocol/staking/contractstaking/statereader.go index 98b0d542d2..e591bcc68e 100644 --- a/action/protocol/staking/contractstaking/statereader.go +++ b/action/protocol/staking/contractstaking/statereader.go @@ -3,33 +3,35 @@ 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" ) // ContractStakingStateReader wraps a state reader to provide staking contract-specific reads. type ContractStakingStateReader struct { - sr protocol.StateReader + sr protocol.StateReader + globalOpts []protocol.StateOption } // NewStateReader creates a new ContractStakingStateReader. -func NewStateReader(sr protocol.StateReader) *ContractStakingStateReader { +func NewStateReader(sr protocol.StateReader, opts ...protocol.StateOption) *ContractStakingStateReader { return &ContractStakingStateReader{ - sr: sr, + sr: sr, + globalOpts: opts, } } 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,15 +44,17 @@ 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) { var contract StakingContract _, err := r.sr.State( &contract, - metaNamespaceOption(), - contractKeyOption(contractAddr), + r.makeOpts( + metaNamespaceOption(), + contractKeyOption(contractAddr), + )..., ) if err != nil { return nil, err @@ -69,15 +73,17 @@ func (r *ContractStakingStateReader) NumOfBuckets(contractAddr address.Address) // BucketType returns the BucketType for a given contract and bucket id. func (r *ContractStakingStateReader) BucketType(contractAddr address.Address, tID uint64) (*BucketType, error) { - var bktType stakingpb.BucketType + var bktType BucketType if _, err := r.sr.State( &bktType, - bucketTypeNamespaceOption(contractAddr), - bucketIDKeyOption(tID), + r.makeOpts( + bucketTypeNamespaceOption(contractAddr), + bucketIDKeyOption(tID), + )..., ); err != nil { - return nil, fmt.Errorf("failed to get bucket type %d for contract %s: %w", tID, contractAddr.String(), err) + return nil, errors.Wrapf(err, "failed to get bucket type %d for contract %s", tID, contractAddr.String()) } - return LoadBucketTypeFromProto(&bktType) + return &bktType, nil } // Bucket returns the Bucket for a given contract and bucket id. @@ -85,8 +91,10 @@ func (r *ContractStakingStateReader) Bucket(contractAddr address.Address, bucket var ssb Bucket if _, err := r.sr.State( &ssb, - contractNamespaceOption(contractAddr), - bucketIDKeyOption(bucketID), + r.makeOpts( + contractNamespaceOption(contractAddr), + bucketIDKeyOption(bucketID), + )..., ); err != nil { switch errors.Cause(err) { case state.ErrStateNotExist: @@ -100,25 +108,28 @@ func (r *ContractStakingStateReader) Bucket(contractAddr address.Address, bucket // BucketTypes returns all BucketType for a given contract and bucket id. func (r *ContractStakingStateReader) BucketTypes(contractAddr address.Address) ([]uint64, []*BucketType, error) { - _, iter, err := r.sr.States(bucketTypeNamespaceOption(contractAddr)) - if err != nil { - return nil, nil, fmt.Errorf("failed to get bucket types for contract %s: %w", contractAddr.String(), err) + _, iter, err := r.sr.States(r.makeOpts( + bucketTypeNamespaceOption(contractAddr), + protocol.ObjectOption(&BucketType{}), + )...) + switch errors.Cause(err) { + case nil: + case state.ErrStateNotExist: + return nil, nil, nil + default: + return nil, nil, errors.Wrapf(err, "failed to get bucket types for contract %s", contractAddr.String()) } ids := make([]uint64, 0, iter.Size()) types := make([]*BucketType, 0, iter.Size()) for i := 0; i < iter.Size(); i++ { - var bktType stakingpb.BucketType + var bktType BucketType switch key, err := iter.Next(&bktType); err { case nil: - bt, err := LoadBucketTypeFromProto(&bktType) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to load bucket type from proto") - } ids = append(ids, byteutil.BytesToUint64(key)) - types = append(types, bt) + types = append(types, &bktType) case state.ErrNilValue: default: - return nil, nil, fmt.Errorf("failed to read bucket type %d for contract %s: %w", byteutil.BytesToUint64(key), contractAddr.String(), err) + return nil, nil, errors.Wrapf(err, "failed to read bucket type %x for contract %s", key, contractAddr.String()) } } return ids, types, nil @@ -126,28 +137,33 @@ func (r *ContractStakingStateReader) BucketTypes(contractAddr address.Address) ( // Buckets returns all BucketInfo for a given contract. func (r *ContractStakingStateReader) Buckets(contractAddr address.Address) ([]uint64, []*Bucket, error) { - _, iter, err := r.sr.States(contractNamespaceOption(contractAddr)) - if err != nil { - return nil, nil, fmt.Errorf("failed to get buckets for contract %s: %w", contractAddr.String(), err) + _, iter, err := r.sr.States(r.makeOpts( + contractNamespaceOption(contractAddr), + protocol.ObjectOption(&Bucket{}), + )...) + switch errors.Cause(err) { + case nil: + case state.ErrStateNotExist: + return nil, nil, nil + default: + return nil, nil, errors.Wrapf(err, "failed to get buckets for contract %s", contractAddr.String()) } ids := make([]uint64, 0, iter.Size()) buckets := make([]*Bucket, 0, iter.Size()) for i := 0; i < iter.Size(); i++ { - var ssb stakingpb.SystemStakingBucket + var ssb Bucket switch key, err := iter.Next(&ssb); err { case nil: - bucket, err := LoadBucketFromProto(&ssb) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to load bucket from proto") - } - if bucket != nil { - ids = append(ids, byteutil.BytesToUint64(key)) - buckets = append(buckets, bucket) - } + ids = append(ids, byteutil.BytesToUint64(key)) + buckets = append(buckets, &ssb) case state.ErrNilValue: default: - return nil, nil, fmt.Errorf("failed to read bucket %d for contract %s: %w", byteutil.BytesToUint64(key), contractAddr.String(), err) + return nil, nil, errors.Wrapf(err, "failed to read bucket %d for contract %s", byteutil.BytesToUint64(key), contractAddr.String()) } } return ids, buckets, nil } + +func (cs *ContractStakingStateReader) makeOpts(opts ...protocol.StateOption) []protocol.StateOption { + return append(cs.globalOpts, opts...) +} diff --git a/action/protocol/staking/nfteventhandler.go b/action/protocol/staking/nfteventhandler.go index 009fe90594..68dac6ffeb 100644 --- a/action/protocol/staking/nfteventhandler.go +++ b/action/protocol/staking/nfteventhandler.go @@ -4,9 +4,10 @@ import ( "math/big" "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" + "github.com/iotexproject/iotex-core/v2/action/protocol" "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" - "github.com/pkg/errors" ) type ( @@ -36,6 +37,15 @@ func newNFTBucketEventHandler(sm protocol.StateManager, calculateVoteWeight Calc }, nil } +func newNFTBucketEventHandlerErigonOnly(sm protocol.StateManager, calculateVoteWeight CalculateVoteWeightFunc) *nftEventHandler { + return &nftEventHandler{ + calculateVoteWeight: calculateVoteWeight, + cssm: contractstaking.NewContractStakingStateManager(sm, protocol.ErigonStoreOnlyOption()), + bucketTypes: make(map[address.Address]map[uint64]*contractstaking.BucketType), + bucketTypesLookup: make(map[address.Address]map[int64]map[uint64]uint64), + } +} + func (handler *nftEventHandler) matchBucketType(contractAddr address.Address, amount *big.Int, duration uint64) (uint64, error) { cmap, ok := handler.bucketTypesLookup[contractAddr] if !ok { @@ -85,11 +95,17 @@ func (handler *nftEventHandler) DeductBucket(contractAddr address.Address, id ui if err != nil { return nil, errors.Wrap(err, "failed to get bucket") } + if handler.csm == nil { + return bucket, nil + } height, err := handler.csm.SR().Height() if err != nil { return nil, errors.Wrap(err, "failed to get height") } candidate := handler.csm.GetByIdentifier(bucket.Candidate) + if candidate == nil { + return bucket, nil + } if err := candidate.SubVote(handler.calculateVoteWeight(bucket, height)); err != nil { return nil, errors.Wrap(err, "failed to subtract vote") } @@ -100,17 +116,23 @@ func (handler *nftEventHandler) DeductBucket(contractAddr address.Address, id ui } func (handler *nftEventHandler) PutBucket(contractAddr address.Address, id uint64, bkt *contractstaking.Bucket) error { + if err := handler.cssm.UpsertBucket(contractAddr, id, bkt); err != nil { + return errors.Wrap(err, "failed to put bucket") + } + if handler.csm == nil { + return nil + } height, err := handler.csm.SR().Height() if err != nil { return errors.Wrap(err, "failed to get height") } candidate := handler.csm.GetByIdentifier(bkt.Candidate) + if candidate == nil { + return nil + } if err := candidate.AddVote(handler.calculateVoteWeight(bkt, height)); err != nil { return errors.Wrap(err, "failed to add vote") } - if err := handler.cssm.UpsertBucket(contractAddr, id, bkt); err != nil { - return errors.Wrap(err, "failed to put bucket") - } return handler.csm.Upsert(candidate) } @@ -119,16 +141,22 @@ func (handler *nftEventHandler) DeleteBucket(contractAddr address.Address, id ui if err != nil { return errors.Wrap(err, "failed to get bucket") } + if err := handler.cssm.DeleteBucket(contractAddr, id); err != nil { + return errors.Wrap(err, "failed to delete bucket") + } + if handler.csm == nil { + return nil + } height, err := handler.csm.SR().Height() if err != nil { return errors.Wrap(err, "failed to get height") } candidate := handler.csm.GetByIdentifier(bucket.Candidate) + if candidate == nil { + return nil + } if err := candidate.SubVote(handler.calculateVoteWeight(bucket, height)); err != nil { return errors.Wrap(err, "failed to subtract vote") } - if err := handler.cssm.DeleteBucket(contractAddr, id); err != nil { - return errors.Wrap(err, "failed to delete bucket") - } return handler.csm.Upsert(candidate) } diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index a192ce935d..c39636674a 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -275,15 +275,30 @@ func (p *Protocol) Start(ctx context.Context, sr protocol.StateReader) (protocol if err := indexer.Start(ctx); err != nil { return errors.Wrap(err, "failed to start contract staking indexer") } + if indexer.StartHeight() > height { + return nil + } + indexerHeight, err := indexer.Height() + if err != nil { + return errors.Wrap(err, "failed to get contract staking indexer height") + } + if indexerHeight > height { + return errors.Errorf("contract staking indexer height %d > current height %d", indexerHeight, height) + } + if height == 0 { + return nil + } checkerHeight, err := checker.Height() if err != nil { return errors.Wrap(err, "failed to get checker height") } - if indexer.StartHeight() > checkerHeight { - return nil + if checkerHeight < height { + return errors.Errorf("checker height %d < target height %d", checkerHeight, height) } return checker.CheckIndexer(ctx, indexer, height, func(h uint64) { - log.L().Info("Checking contract staking indexer", zap.Uint64("height", h)) + if h%5000 == 0 || h == height { + log.L().Info("Checking contract staking indexer", zap.Uint64("height", h)) + } }) } buildView := func(indexer ContractStakingIndexer, callback func(ContractStakeView)) { @@ -299,7 +314,7 @@ func (p *Protocol) Start(ctx context.Context, sr protocol.StateReader) (protocol } view, err := NewContractStakeViewBuilder(indexer, p.blockStore).Build(ctx, sr, height) if err != nil { - errChan <- errors.Wrapf(err, "failed to create stake view for contract %s", p.contractStakingIndexer.ContractAddress()) + errChan <- errors.Wrapf(err, "failed to create stake view for contract %s", indexer.ContractAddress()) return } callback(view) @@ -338,7 +353,7 @@ func (p *Protocol) CreateGenesisStates( if err != nil { return err } - + blkCtx := protocol.MustGetBlockCtx(ctx) for _, bc := range p.config.BootstrapCandidates { owner, err := address.FromString(bc.OwnerAddress) if err != nil { @@ -359,7 +374,7 @@ func (p *Protocol) CreateGenesisStates( if !ok { return action.ErrInvalidAmount } - bucket := NewVoteBucket(owner, owner, selfStake, 7, time.Now(), true) + bucket := NewVoteBucket(owner, owner, selfStake, 7, blkCtx.BlockTimeStamp, true) bucketIdx, err := csm.putBucketAndIndex(bucket) if err != nil { return err @@ -479,15 +494,12 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager return err } vd := v.(*viewData) - if blkCtx.BlockHeight == g.ToBeEnabledBlockHeight { - handler, err := newNFTBucketEventHandler(sm, func(bucket *contractstaking.Bucket, height uint64) *big.Int { - vb := p.convertToVoteBucket(bucket, height) - return p.calculateVoteWeight(vb, false) - }) + if blkCtx.BlockHeight == g.XinguBlockHeight { + handler, err := newNFTBucketEventHandler(sm, p.calculateContractBucketVoteWeight) if err != nil { return err } - if err := vd.contractsStake.Migrate(handler); err != nil { + if err := vd.contractsStake.Migrate(ctx, handler); err != nil { return errors.Wrap(err, "failed to flush buckets for contract staking") } } @@ -495,6 +507,9 @@ func (p *Protocol) CreatePreStates(ctx context.Context, sm protocol.StateManager if err := vd.contractsStake.CreatePreStates(ctx); err != nil { return err } + if blkCtx.BlockHeight == g.WakeBlockHeight { + vd.contractsStake.Revise(ctx) + } } if p.candBucketsIndexer == nil { @@ -675,36 +690,51 @@ func (p *Protocol) HandleReceipt(ctx context.Context, elp action.Envelope, sm pr if !ok { return errors.New("failed to get feature context from action context") } + var ( + handler *nftEventHandler + err error + ) + ccvw := p.calculateContractBucketVoteWeight if featureCtx.StoreVoteOfNFTBucketIntoView { v, err := sm.ReadView(_protocolID) 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 = newNFTBucketEventHandlerErigonOnly(sm, ccvw) + } else { + handler, err = newNFTBucketEventHandler(sm, ccvw) } - handler, err := newNFTBucketEventHandler(sm, func(bucket *contractstaking.Bucket, height uint64) *big.Int { - vb := p.convertToVoteBucket(bucket, height) - return p.calculateVoteWeight(vb, false) - }) if err != nil { return err } if p.contractStakingIndexer != nil { processor := p.contractStakingIndexer.CreateEventProcessor(ctx, handler) if err := processor.ProcessReceipts(ctx, receipt); err != nil { - return errors.Wrap(err, "failed to process receipt for contract staking indexer") + if !errors.Is(err, state.ErrErigonStoreNotSupported) { + return errors.Wrap(err, "failed to process receipt for contract staking indexer") + } + log.L().Debug("skip processing receipt for contract staking indexer due to erigon store not supported") } } if p.contractStakingIndexerV2 != nil { processor := p.contractStakingIndexerV2.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 !errors.Is(err, state.ErrErigonStoreNotSupported) { + return errors.Wrap(err, "failed to process receipt for contract staking indexer v2") + } + log.L().Debug("skip processing receipt for contract staking indexer v2 due to erigon store not supported") } } if p.contractStakingIndexerV3 != nil { processor := p.contractStakingIndexerV3.CreateEventProcessor(ctx, handler) if err := processor.ProcessReceipts(ctx, receipt); err != nil { - return errors.Wrap(err, "failed to process receipt for contract staking indexer v3") + if !errors.Is(err, state.ErrErigonStoreNotSupported) { + return errors.Wrap(err, "failed to process receipt for contract staking indexer v3") + } + log.L().Debug("skip processing receipt for contract staking indexer v3 due to erigon store not supported") } } return nil @@ -779,10 +809,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 +818,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]) @@ -939,7 +955,7 @@ func (p *Protocol) convertToVoteBucket(bkt *contractstaking.Bucket, height uint6 AutoStake: bkt.UnlockedAt == MaxDurationNumber, Candidate: bkt.Candidate, Owner: bkt.Owner, - ContractAddress: "", + ContractAddress: address.ZeroAddress, Timestamped: bkt.IsTimestampBased, } if bkt.IsTimestampBased { @@ -974,6 +990,14 @@ func (p *Protocol) calculateVoteWeight(v *VoteBucket, selfStake bool) *big.Int { return CalculateVoteWeight(p.config.VoteWeightCalConsts, v, selfStake) } +func (p *Protocol) calculateContractBucketVoteWeight(bucket *contractstaking.Bucket, height uint64) *big.Int { + vb := p.convertToVoteBucket(bucket, height) + if bucket.Muted || vb.isUnstaked() { + return big.NewInt(0) + } + return p.calculateVoteWeight(vb, false) +} + type nonceUpdateType bool const ( @@ -1047,32 +1071,6 @@ func (p *Protocol) needToWriteCandsMap(ctx context.Context, height uint64) bool return height >= p.config.PersistStakingPatchBlock && fCtx.CandCenterHasAlias(height) } -func (p *Protocol) contractStakingVotesFromIndexer(ctx context.Context, candidate address.Address, height uint64) (*big.Int, error) { - featureCtx := protocol.MustGetFeatureCtx(ctx) - votes := big.NewInt(0) - indexers := []ContractStakingIndexer{} - if p.contractStakingIndexer != nil && featureCtx.AddContractStakingVotes { - indexers = append(indexers, p.contractStakingIndexer) - } - if p.contractStakingIndexerV2 != nil && !featureCtx.LimitedStakingContract { - indexers = append(indexers, p.contractStakingIndexerV2) - } - if p.contractStakingIndexerV3 != nil && featureCtx.TimestampedStakingContract { - indexers = append(indexers, p.contractStakingIndexerV3) - } - - for _, indexer := range indexers { - btks, err := indexer.BucketsByCandidate(candidate, height) - if err != nil { - return nil, errors.Wrap(err, "failed to get BucketsByCandidate from contractStakingIndexer") - } - for _, b := range btks { - votes.Add(votes, p.contractBucketVotes(featureCtx, b)) - } - } - return votes, nil -} - func (p *Protocol) contractStakingVotesFromView(ctx context.Context, candidate address.Address, view *viewData) (*big.Int, error) { featureCtx := protocol.MustGetFeatureCtx(ctx) votes := big.NewInt(0) @@ -1087,30 +1085,15 @@ 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 } -func (p *Protocol) contractBucketVotes(fCtx protocol.FeatureCtx, bkt *VoteBucket) *big.Int { - votes := big.NewInt(0) - if bkt.isUnstaked() { - return votes - } - if fCtx.FixContractStakingWeightedVotes { - votes.Add(votes, p.calculateVoteWeight(bkt, false)) - } else { - votes.Add(votes, bkt.StakedAmount) - } - return votes -} - func readCandCenterStateFromStateDB(sr protocol.StateReader) (CandidateList, CandidateList, CandidateList, error) { var ( name, operator, owner CandidateList diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index 5f373b48a0..3666b52bcc 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -314,6 +314,59 @@ func Test_CreatePreStatesWithRegisterProtocol(t *testing.T) { require.NoError(p.CreatePreStates(ctx, sm)) } +func TestCreatePreStatesMigration(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + sm := testdb.NewMockStateManager(ctrl) + g := genesis.TestDefault() + mockView := NewMockContractStakeView(ctrl) + mockContractStaking := NewMockContractStakingIndexer(ctrl) + mockContractStaking.EXPECT().ContractAddress().Return(identityset.Address(1)).Times(1) + mockContractStaking.EXPECT().LoadStakeView(gomock.Any(), gomock.Any()).Return(mockView, nil).Times(1) + mockContractStaking.EXPECT().StartHeight().Return(uint64(0)).Times(1) + mockContractStaking.EXPECT().Height().Return(uint64(0), nil).Times(1) + p, err := NewProtocol(HelperCtx{ + DepositGas: nil, + BlockInterval: getBlockInterval, + }, &BuilderConfig{ + Staking: g.Staking, + PersistStakingPatchBlock: math.MaxUint64, + SkipContractStakingViewHeight: math.MaxUint64, + Revise: ReviseConfig{ + VoteWeight: g.Staking.VoteWeightCalConsts, + ReviseHeights: []uint64{g.GreenlandBlockHeight}}, + }, nil, nil, nil, mockContractStaking) + require.NoError(err) + ctx := genesis.WithGenesisContext(context.Background(), g) + ctx = protocol.WithBlockCtx( + ctx, + protocol.BlockCtx{ + BlockHeight: g.XinguBlockHeight, + }, + ) + ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) + v, err := p.Start(ctx, sm) + require.NoError(err) + require.NoError(sm.WriteView(_protocolID, v)) + mockView.EXPECT().Migrate(gomock.Any(), gomock.Any()).Return(errors.New("migration error")).Times(1) + require.ErrorContains(p.CreatePreStates(ctx, sm), "migration error") + mockView.EXPECT().Migrate(gomock.Any(), gomock.Any()).Return(nil).Times(1) + require.NoError(p.CreatePreStates(ctx, sm)) + require.NoError(p.CreatePreStates(protocol.WithBlockCtx( + ctx, + protocol.BlockCtx{ + BlockHeight: g.XinguBlockHeight - 1, + }, + ), sm)) + require.NoError(p.CreatePreStates(protocol.WithBlockCtx( + ctx, + protocol.BlockCtx{ + BlockHeight: g.XinguBlockHeight + 1, + }, + ), sm)) +} + func Test_CreateGenesisStates(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) @@ -428,113 +481,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) @@ -672,3 +618,128 @@ func TestIsSelfStakeBucket(t *testing.T) { r.False(selfStake) }) } + +func TestSlashCandidate(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + sm := testdb.NewMockStateManager(ctrl) + + owner := identityset.Address(1) + operator := identityset.Address(2) + reward := identityset.Address(3) + selfStake := big.NewInt(1000) + bucket := NewVoteBucket(owner, owner, new(big.Int).Set(selfStake), 10, time.Now(), true) + bucketIdx := uint64(0) + bucket.Index = bucketIdx + + cand := &Candidate{ + Owner: owner, + Operator: operator, + Reward: reward, + Name: "cand1", + Votes: big.NewInt(1000), + SelfStakeBucketIdx: bucketIdx, + SelfStake: new(big.Int).Set(selfStake), + } + cc, err := NewCandidateCenter(CandidateList{cand}) + require.NoError(err) + require.NoError(sm.WriteView(_protocolID, &viewData{ + candCenter: cc, + bucketPool: &BucketPool{ + enableSMStorage: true, + total: &totalAmount{ + amount: big.NewInt(0), + }, + }, + })) + csm, err := NewCandidateStateManager(sm) + require.NoError(err) + + p := &Protocol{ + config: Configuration{ + RegistrationConsts: RegistrationConsts{ + MinSelfStake: big.NewInt(1000), + }, + MinSelfStakeToBeActive: big.NewInt(590), + }, + } + ctx := context.Background() + ctx = genesis.WithGenesisContext(ctx, genesis.TestDefault()) + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: 100, + }) + ctx = protocol.WithFeatureCtx(ctx) + + t.Run("nil amount", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, nil) + require.ErrorContains(err, "nil or non-positive amount") + }) + + t.Run("zero amount", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, big.NewInt(0)) + require.ErrorContains(err, "nil or non-positive amount") + }) + + t.Run("candidate not exist", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, identityset.Address(9), big.NewInt(1)) + require.ErrorContains(err, "does not exist") + }) + + t.Run("bucket not exist", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, big.NewInt(1)) + require.ErrorContains(err, "failed to fetch bucket") + }) + + _, err = csm.putBucket(bucket) + require.NoError(err) + require.NoError(csm.DebitBucketPool(bucket.StakedAmount, true)) + cl, err := p.ActiveCandidates(ctx, sm, 0) + require.NoError(err) + require.Equal(1, len(cl)) + + t.Run("amount greater than staked", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, big.NewInt(2000)) + require.ErrorContains(err, "is greater than staked amount") + }) + + t.Run("success", func(t *testing.T) { + amount := big.NewInt(400) + remaining := bucket.StakedAmount.Sub(bucket.StakedAmount, amount) + require.NoError(p.SlashCandidate(ctx, sm, owner, amount)) + cl, err = p.ActiveCandidates(ctx, sm, 0) + require.NoError(err) + require.Equal(0, len(cl)) + ctx = protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: genesis.Default.XinguBlockHeight, + })) + cl, err = p.ActiveCandidates( + ctx, + sm, + 0, + ) + require.NoError(err) + require.Equal(1, len(cl)) + bucket, err := csm.NativeBucket(bucketIdx) + require.NoError(err) + require.Equal(remaining.String(), bucket.StakedAmount.String()) + cand := csm.GetByIdentifier(owner) + require.Equal(remaining.String(), cand.SelfStake.String()) + require.NoError(p.SlashCandidate(ctx, sm, owner, big.NewInt(11))) + cl, err = p.ActiveCandidates( + ctx, + sm, + 0, + ) + require.NoError(err) + require.Equal(0, len(cl)) + require.NoError(cand.AddSelfStake(big.NewInt(21))) + require.NoError(csm.Upsert(cand)) + cl, err = p.ActiveCandidates( + ctx, + sm, + 0, + ) + require.NoError(err) + require.Equal(1, len(cl)) + }) +} diff --git a/action/protocol/staking/stakeview_builder.go b/action/protocol/staking/stakeview_builder.go index 228c275780..be750e9a4e 100644 --- a/action/protocol/staking/stakeview_builder.go +++ b/action/protocol/staking/stakeview_builder.go @@ -33,6 +33,10 @@ func NewContractStakeViewBuilder( } func (b *contractStakeViewBuilder) Build(ctx context.Context, sr protocol.StateReader, height uint64) (ContractStakeView, error) { + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: height, + }) + ctx = protocol.WithFeatureCtx(ctx) view, err := b.indexer.LoadStakeView(ctx, sr) if err != nil { return nil, err diff --git a/action/protocol/staking/staking_statereader.go b/action/protocol/staking/staking_statereader.go index 25f1edcad5..617f0ae95e 100644 --- a/action/protocol/staking/staking_statereader.go +++ b/action/protocol/staking/staking_statereader.go @@ -241,7 +241,8 @@ func (c *compositeStakingStateReader) readStateCandidates(ctx context.Context, r return nil, 0, err } } - if !protocol.MustGetFeatureCtx(ctx).AddContractStakingVotes { + fCtx := protocol.MustGetFeatureCtx(ctx) + if !fCtx.AddContractStakingVotes || !fCtx.StoreVoteOfNFTBucketIntoView { return candidates, height, nil } if !c.isContractStakingEnabled() { @@ -265,7 +266,8 @@ func (c *compositeStakingStateReader) readStateCandidateByName(ctx context.Conte if !c.isContractStakingEnabled() { return candidate, height, nil } - if !protocol.MustGetFeatureCtx(ctx).AddContractStakingVotes { + fCtx := protocol.MustGetFeatureCtx(ctx) + if !fCtx.AddContractStakingVotes || !fCtx.StoreVoteOfNFTBucketIntoView { return candidate, height, nil } for _, indexer := range c.contractIndexers { @@ -284,7 +286,8 @@ func (c *compositeStakingStateReader) readStateCandidateByAddress(ctx context.Co if !c.isContractStakingEnabled() { return candidate, height, nil } - if !protocol.MustGetFeatureCtx(ctx).AddContractStakingVotes { + fCtx := protocol.MustGetFeatureCtx(ctx) + if !fCtx.AddContractStakingVotes || !fCtx.StoreVoteOfNFTBucketIntoView { return candidate, height, nil } for _, indexer := range c.contractIndexers { diff --git a/action/protocol/staking/stakingpb/staking.pb.go b/action/protocol/staking/stakingpb/staking.pb.go index 665c795614..a84f18711b 100644 --- a/action/protocol/staking/stakingpb/staking.pb.go +++ b/action/protocol/staking/stakingpb/staking.pb.go @@ -8,9 +8,9 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.29.3 -// source: staking.proto +// protoc-gen-go v1.26.0 +// protoc v4.23.3 +// source: action/protocol/staking/stakingpb/staking.proto package stakingpb @@ -53,7 +53,7 @@ type Bucket struct { func (x *Bucket) Reset() { *x = Bucket{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[0] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -66,7 +66,7 @@ func (x *Bucket) String() string { func (*Bucket) ProtoMessage() {} func (x *Bucket) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[0] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -79,7 +79,7 @@ func (x *Bucket) ProtoReflect() protoreflect.Message { // Deprecated: Use Bucket.ProtoReflect.Descriptor instead. func (*Bucket) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{0} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{0} } func (x *Bucket) GetIndex() uint64 { @@ -191,7 +191,7 @@ type BucketIndices struct { func (x *BucketIndices) Reset() { *x = BucketIndices{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[1] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -204,7 +204,7 @@ func (x *BucketIndices) String() string { func (*BucketIndices) ProtoMessage() {} func (x *BucketIndices) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[1] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -217,7 +217,7 @@ func (x *BucketIndices) ProtoReflect() protoreflect.Message { // Deprecated: Use BucketIndices.ProtoReflect.Descriptor instead. func (*BucketIndices) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{1} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{1} } func (x *BucketIndices) GetIndices() []uint64 { @@ -246,7 +246,7 @@ type Candidate struct { func (x *Candidate) Reset() { *x = Candidate{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[2] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -259,7 +259,7 @@ func (x *Candidate) String() string { func (*Candidate) ProtoMessage() {} func (x *Candidate) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[2] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -272,7 +272,7 @@ func (x *Candidate) ProtoReflect() protoreflect.Message { // Deprecated: Use Candidate.ProtoReflect.Descriptor instead. func (*Candidate) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{2} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{2} } func (x *Candidate) GetOwnerAddress() string { @@ -349,7 +349,7 @@ type Candidates struct { func (x *Candidates) Reset() { *x = Candidates{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[3] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -362,7 +362,7 @@ func (x *Candidates) String() string { func (*Candidates) ProtoMessage() {} func (x *Candidates) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[3] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -375,7 +375,7 @@ func (x *Candidates) ProtoReflect() protoreflect.Message { // Deprecated: Use Candidates.ProtoReflect.Descriptor instead. func (*Candidates) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{3} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{3} } func (x *Candidates) GetCandidates() []*Candidate { @@ -397,7 +397,7 @@ type TotalAmount struct { func (x *TotalAmount) Reset() { *x = TotalAmount{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[4] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -410,7 +410,7 @@ func (x *TotalAmount) String() string { func (*TotalAmount) ProtoMessage() {} func (x *TotalAmount) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[4] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -423,7 +423,7 @@ func (x *TotalAmount) ProtoReflect() protoreflect.Message { // Deprecated: Use TotalAmount.ProtoReflect.Descriptor instead. func (*TotalAmount) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{4} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{4} } func (x *TotalAmount) GetAmount() string { @@ -453,7 +453,7 @@ type BucketType struct { func (x *BucketType) Reset() { *x = BucketType{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[5] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -466,7 +466,7 @@ func (x *BucketType) String() string { func (*BucketType) ProtoMessage() {} func (x *BucketType) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[5] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -479,7 +479,7 @@ func (x *BucketType) ProtoReflect() protoreflect.Message { // Deprecated: Use BucketType.ProtoReflect.Descriptor instead. func (*BucketType) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{5} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{5} } func (x *BucketType) GetAmount() string { @@ -514,7 +514,7 @@ type Endorsement struct { func (x *Endorsement) Reset() { *x = Endorsement{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[6] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -527,7 +527,7 @@ func (x *Endorsement) String() string { func (*Endorsement) ProtoMessage() {} func (x *Endorsement) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[6] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -540,7 +540,7 @@ func (x *Endorsement) ProtoReflect() protoreflect.Message { // Deprecated: Use Endorsement.ProtoReflect.Descriptor instead. func (*Endorsement) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{6} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{6} } func (x *Endorsement) GetExpireHeight() uint64 { @@ -561,7 +561,7 @@ type SystemStakingContract struct { func (x *SystemStakingContract) Reset() { *x = SystemStakingContract{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[7] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -574,7 +574,7 @@ func (x *SystemStakingContract) String() string { func (*SystemStakingContract) ProtoMessage() {} func (x *SystemStakingContract) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[7] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -587,7 +587,7 @@ func (x *SystemStakingContract) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemStakingContract.ProtoReflect.Descriptor instead. func (*SystemStakingContract) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{7} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{7} } func (x *SystemStakingContract) GetNumOfBuckets() uint64 { @@ -602,20 +602,21 @@ type SystemStakingBucket struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Owner []byte `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"` - Candidate []byte `protobuf:"bytes,2,opt,name=candidate,proto3" json:"candidate,omitempty"` - Amount []byte `protobuf:"bytes,3,opt,name=amount,proto3" json:"amount,omitempty"` - Duration uint64 `protobuf:"varint,4,opt,name=duration,proto3" json:"duration,omitempty"` - CreatedAt uint64 `protobuf:"varint,5,opt,name=createdAt,proto3" json:"createdAt,omitempty"` - UnlockedAt uint64 `protobuf:"varint,6,opt,name=unlockedAt,proto3" json:"unlockedAt,omitempty"` - UnstakedAt uint64 `protobuf:"varint,7,opt,name=unstakedAt,proto3" json:"unstakedAt,omitempty"` - Muted bool `protobuf:"varint,8,opt,name=muted,proto3" json:"muted,omitempty"` + Candidate string `protobuf:"bytes,1,opt,name=candidate,proto3" json:"candidate,omitempty"` + Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + Amount string `protobuf:"bytes,3,opt,name=amount,proto3" json:"amount,omitempty"` + Duration uint64 `protobuf:"varint,4,opt,name=duration,proto3" json:"duration,omitempty"` + CreatedAt uint64 `protobuf:"varint,5,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + UnlockedAt uint64 `protobuf:"varint,6,opt,name=unlockedAt,proto3" json:"unlockedAt,omitempty"` + UnstakedAt uint64 `protobuf:"varint,7,opt,name=unstakedAt,proto3" json:"unstakedAt,omitempty"` + Muted bool `protobuf:"varint,8,opt,name=muted,proto3" json:"muted,omitempty"` + Timestamped bool `protobuf:"varint,9,opt,name=timestamped,proto3" json:"timestamped,omitempty"` } func (x *SystemStakingBucket) Reset() { *x = SystemStakingBucket{} if protoimpl.UnsafeEnabled { - mi := &file_staking_proto_msgTypes[8] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -628,7 +629,7 @@ func (x *SystemStakingBucket) String() string { func (*SystemStakingBucket) ProtoMessage() {} func (x *SystemStakingBucket) ProtoReflect() protoreflect.Message { - mi := &file_staking_proto_msgTypes[8] + mi := &file_action_protocol_staking_stakingpb_staking_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -641,28 +642,28 @@ func (x *SystemStakingBucket) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemStakingBucket.ProtoReflect.Descriptor instead. func (*SystemStakingBucket) Descriptor() ([]byte, []int) { - return file_staking_proto_rawDescGZIP(), []int{8} + return file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP(), []int{8} } -func (x *SystemStakingBucket) GetOwner() []byte { +func (x *SystemStakingBucket) GetCandidate() string { if x != nil { - return x.Owner + return x.Candidate } - return nil + return "" } -func (x *SystemStakingBucket) GetCandidate() []byte { +func (x *SystemStakingBucket) GetOwner() string { if x != nil { - return x.Candidate + return x.Owner } - return nil + return "" } -func (x *SystemStakingBucket) GetAmount() []byte { +func (x *SystemStakingBucket) GetAmount() string { if x != nil { return x.Amount } - return nil + return "" } func (x *SystemStakingBucket) GetDuration() uint64 { @@ -700,135 +701,146 @@ func (x *SystemStakingBucket) GetMuted() bool { return false } -var File_staking_proto protoreflect.FileDescriptor - -var file_staking_proto_rawDesc = []byte{ - 0x0a, 0x0d, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, - 0x09, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x98, 0x05, 0x0a, 0x06, - 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2a, 0x0a, 0x10, - 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x6b, - 0x65, 0x64, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0e, - 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, - 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, - 0x12, 0x42, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, - 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x75, 0x6e, 0x73, 0x74, - 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, - 0x61, 0x75, 0x74, 0x6f, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x61, 0x75, 0x74, 0x6f, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, - 0x6e, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, - 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x19, 0x73, 0x74, - 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, - 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x19, 0x73, - 0x74, 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, - 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x34, 0x0a, 0x15, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, - 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, - 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x38, 0x0a, 0x17, - 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, - 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x75, - 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, - 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x29, 0x0a, 0x0d, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, - 0x49, 0x6e, 0x64, 0x69, 0x63, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x64, 0x69, 0x63, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x07, 0x69, 0x6e, 0x64, 0x69, 0x63, 0x65, - 0x73, 0x22, 0xbd, 0x02, 0x0a, 0x09, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x22, 0x0a, 0x0c, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x41, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x24, 0x0a, - 0x0d, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x6f, 0x74, 0x65, 0x73, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x2e, 0x0a, - 0x12, 0x73, 0x65, 0x6c, 0x66, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, - 0x49, 0x64, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x73, 0x65, 0x6c, 0x66, 0x53, - 0x74, 0x61, 0x6b, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x78, 0x12, 0x1c, 0x0a, - 0x09, 0x73, 0x65, 0x6c, 0x66, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x65, 0x6c, 0x66, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x12, 0x2c, 0x0a, 0x11, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, - 0x6b, 0x65, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x22, 0x42, 0x0a, 0x0a, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, - 0x34, 0x0a, 0x0a, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 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, 0x22, 0x3b, 0x0a, 0x0b, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0x62, 0x0a, 0x0a, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x31, 0x0a, 0x0b, 0x45, 0x6e, 0x64, 0x6f, 0x72, 0x73, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x48, - 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x65, 0x78, 0x70, - 0x69, 0x72, 0x65, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x3b, 0x0a, 0x15, 0x53, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x61, - 0x63, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x4f, 0x66, 0x42, 0x75, 0x63, 0x6b, 0x65, - 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6e, 0x75, 0x6d, 0x4f, 0x66, 0x42, - 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x22, 0xf1, 0x01, 0x0a, 0x13, 0x53, 0x79, 0x73, 0x74, 0x65, - 0x6d, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x14, - 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6f, - 0x77, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x64, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x41, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, - 0x41, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, - 0x65, 0x64, 0x41, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, - 0x41, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, - 0x65, 0x64, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x42, 0x46, 0x5a, 0x44, 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, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +func (x *SystemStakingBucket) GetTimestamped() bool { + if x != nil { + return x.Timestamped + } + return false +} + +var File_action_protocol_staking_stakingpb_staking_proto protoreflect.FileDescriptor + +var file_action_protocol_staking_stakingpb_staking_proto_rawDesc = []byte{ + 0x0a, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, + 0x67, 0x70, 0x62, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x09, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x1a, 0x1f, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x98, 0x05, + 0x0a, 0x06, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, + 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2a, + 0x0a, 0x10, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x74, + 0x61, 0x6b, 0x65, 0x64, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, + 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x42, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, + 0x54, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, + 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x75, 0x6e, + 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1c, + 0x0a, 0x09, 0x61, 0x75, 0x74, 0x6f, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x61, 0x75, 0x74, 0x6f, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, + 0x65, 0x72, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x61, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x3c, 0x0a, 0x19, + 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x19, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x64, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, + 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x34, 0x0a, 0x15, 0x73, 0x74, 0x61, 0x6b, + 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, + 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, + 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x38, + 0x0a, 0x17, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x17, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x29, 0x0a, 0x0d, 0x42, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x49, 0x6e, 0x64, 0x69, 0x63, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x64, + 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x07, 0x69, 0x6e, 0x64, 0x69, + 0x63, 0x65, 0x73, 0x22, 0xbd, 0x02, 0x0a, 0x09, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, + 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, + 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x6f, 0x74, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x6f, 0x74, 0x65, 0x73, 0x12, + 0x2e, 0x0a, 0x12, 0x73, 0x65, 0x6c, 0x66, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x42, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x49, 0x64, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x73, 0x65, 0x6c, + 0x66, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x78, 0x12, + 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6c, 0x66, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x6c, 0x66, 0x53, 0x74, 0x61, 0x6b, 0x65, 0x12, 0x2c, 0x0a, + 0x11, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, + 0x6b, 0x65, 0x79, 0x22, 0x42, 0x0a, 0x0a, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x73, 0x12, 0x34, 0x0a, 0x0a, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 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, 0x22, 0x3b, 0x0a, 0x0b, 0x54, 0x6f, 0x74, 0x61, 0x6c, + 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, + 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x62, 0x0a, 0x0a, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x31, 0x0a, 0x0b, 0x45, 0x6e, 0x64, 0x6f, + 0x72, 0x73, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x3b, 0x0a, 0x15, 0x53, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x74, + 0x72, 0x61, 0x63, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x4f, 0x66, 0x42, 0x75, 0x63, + 0x6b, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6e, 0x75, 0x6d, 0x4f, + 0x66, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x22, 0x93, 0x02, 0x0a, 0x13, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x53, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, + 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, + 0x77, 0x6e, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, + 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, + 0x65, 0x64, 0x41, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x75, 0x6e, 0x6c, 0x6f, + 0x63, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x6e, 0x73, 0x74, 0x61, 0x6b, + 0x65, 0x64, 0x41, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x75, 0x6e, 0x73, 0x74, + 0x61, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x14, 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, 0x46, + 0x5a, 0x44, 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, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x73, 0x74, 0x61, + 0x6b, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( - file_staking_proto_rawDescOnce sync.Once - file_staking_proto_rawDescData = file_staking_proto_rawDesc + file_action_protocol_staking_stakingpb_staking_proto_rawDescOnce sync.Once + file_action_protocol_staking_stakingpb_staking_proto_rawDescData = file_action_protocol_staking_stakingpb_staking_proto_rawDesc ) -func file_staking_proto_rawDescGZIP() []byte { - file_staking_proto_rawDescOnce.Do(func() { - file_staking_proto_rawDescData = protoimpl.X.CompressGZIP(file_staking_proto_rawDescData) +func file_action_protocol_staking_stakingpb_staking_proto_rawDescGZIP() []byte { + file_action_protocol_staking_stakingpb_staking_proto_rawDescOnce.Do(func() { + file_action_protocol_staking_stakingpb_staking_proto_rawDescData = protoimpl.X.CompressGZIP(file_action_protocol_staking_stakingpb_staking_proto_rawDescData) }) - return file_staking_proto_rawDescData + return file_action_protocol_staking_stakingpb_staking_proto_rawDescData } -var file_staking_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_staking_proto_goTypes = []any{ +var file_action_protocol_staking_stakingpb_staking_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_action_protocol_staking_stakingpb_staking_proto_goTypes = []interface{}{ (*Bucket)(nil), // 0: stakingpb.Bucket (*BucketIndices)(nil), // 1: stakingpb.BucketIndices (*Candidate)(nil), // 2: stakingpb.Candidate @@ -840,7 +852,7 @@ var file_staking_proto_goTypes = []any{ (*SystemStakingBucket)(nil), // 8: stakingpb.SystemStakingBucket (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp } -var file_staking_proto_depIdxs = []int32{ +var file_action_protocol_staking_stakingpb_staking_proto_depIdxs = []int32{ 9, // 0: stakingpb.Bucket.createTime:type_name -> google.protobuf.Timestamp 9, // 1: stakingpb.Bucket.stakeStartTime:type_name -> google.protobuf.Timestamp 9, // 2: stakingpb.Bucket.unstakeStartTime:type_name -> google.protobuf.Timestamp @@ -852,13 +864,13 @@ var file_staking_proto_depIdxs = []int32{ 0, // [0:4] is the sub-list for field type_name } -func init() { file_staking_proto_init() } -func file_staking_proto_init() { - if File_staking_proto != nil { +func init() { file_action_protocol_staking_stakingpb_staking_proto_init() } +func file_action_protocol_staking_stakingpb_staking_proto_init() { + if File_action_protocol_staking_stakingpb_staking_proto != nil { return } if !protoimpl.UnsafeEnabled { - file_staking_proto_msgTypes[0].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Bucket); i { case 0: return &v.state @@ -870,7 +882,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[1].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BucketIndices); i { case 0: return &v.state @@ -882,7 +894,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[2].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Candidate); i { case 0: return &v.state @@ -894,7 +906,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[3].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Candidates); i { case 0: return &v.state @@ -906,7 +918,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[4].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TotalAmount); i { case 0: return &v.state @@ -918,7 +930,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[5].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BucketType); i { case 0: return &v.state @@ -930,7 +942,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[6].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Endorsement); i { case 0: return &v.state @@ -942,7 +954,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[7].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SystemStakingContract); i { case 0: return &v.state @@ -954,7 +966,7 @@ func file_staking_proto_init() { return nil } } - file_staking_proto_msgTypes[8].Exporter = func(v any, i int) any { + file_action_protocol_staking_stakingpb_staking_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SystemStakingBucket); i { case 0: return &v.state @@ -971,18 +983,18 @@ func file_staking_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_staking_proto_rawDesc, + RawDescriptor: file_action_protocol_staking_stakingpb_staking_proto_rawDesc, NumEnums: 0, NumMessages: 9, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_staking_proto_goTypes, - DependencyIndexes: file_staking_proto_depIdxs, - MessageInfos: file_staking_proto_msgTypes, + GoTypes: file_action_protocol_staking_stakingpb_staking_proto_goTypes, + DependencyIndexes: file_action_protocol_staking_stakingpb_staking_proto_depIdxs, + MessageInfos: file_action_protocol_staking_stakingpb_staking_proto_msgTypes, }.Build() - File_staking_proto = out.File - file_staking_proto_rawDesc = nil - file_staking_proto_goTypes = nil - file_staking_proto_depIdxs = nil + File_action_protocol_staking_stakingpb_staking_proto = out.File + file_action_protocol_staking_stakingpb_staking_proto_rawDesc = nil + file_action_protocol_staking_stakingpb_staking_proto_goTypes = nil + file_action_protocol_staking_stakingpb_staking_proto_depIdxs = nil } diff --git a/action/protocol/staking/stakingpb/staking.proto b/action/protocol/staking/stakingpb/staking.proto index 236279f1c9..7cf720d3e5 100644 --- a/action/protocol/staking/stakingpb/staking.proto +++ b/action/protocol/staking/stakingpb/staking.proto @@ -68,12 +68,13 @@ message SystemStakingContract { } message SystemStakingBucket { - bytes owner = 1; - bytes candidate = 2; - bytes amount = 3; + string candidate = 1; + string owner = 2; + string amount = 3; uint64 duration = 4; uint64 createdAt = 5; uint64 unlockedAt = 6; uint64 unstakedAt = 7; bool muted = 8; + bool timestamped = 9; } diff --git a/action/protocol/staking/viewdata.go b/action/protocol/staking/viewdata.go index ff541fd6dd..1cb7393b3f 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 @@ -31,9 +38,11 @@ type ( // 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(context.Context, EventHandler) error + // Revise updates the contract stake view with the latest bucket data + Revise(context.Context) // BucketsByCandidate returns the buckets by candidate address - BucketsByCandidate(ownerAddr address.Address) ([]*VoteBucket, error) + CandidateStakeVotes(ctx context.Context, id address.Address) *big.Int AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error } // viewData is the data that need to be stored in protocol's view @@ -126,19 +135,31 @@ func (v *viewData) Revert(snapshot int) error { return nil } -func (csv *contractStakeView) Migrate(nftHandler EventHandler) error { +func (csv *contractStakeView) Revise(ctx context.Context) { + if csv.v1 != nil { + csv.v1.Revise(ctx) + } + if csv.v2 != nil { + csv.v2.Revise(ctx) + } + if csv.v3 != nil { + csv.v3.Revise(ctx) + } +} + +func (csv *contractStakeView) Migrate(ctx context.Context, nftHandler EventHandler) error { if csv.v1 != nil { - if err := csv.v1.Migrate(nftHandler); err != nil { + if err := csv.v1.Migrate(ctx, nftHandler); err != nil { return err } } if csv.v2 != nil { - if err := csv.v2.Migrate(nftHandler); err != nil { + if err := csv.v2.Migrate(ctx, nftHandler); err != nil { return err } } if csv.v3 != nil { - if err := csv.v3.Migrate(nftHandler); err != nil { + if err := csv.v3.Migrate(ctx, nftHandler); err != nil { return err } } diff --git a/blockchain/genesis/genesis.go b/blockchain/genesis/genesis.go index 620b8f64e5..72c40d4b2e 100644 --- a/blockchain/genesis/genesis.go +++ b/blockchain/genesis/genesis.go @@ -78,6 +78,7 @@ func defaultConfig() Genesis { UpernavikBlockHeight: 31174201, VanuatuBlockHeight: 33730921, WakeBlockHeight: 36893881, + XinguBlockHeight: 96893881, ToBeEnabledBlockHeight: math.MaxUint64, }, Account: Account{ @@ -369,6 +370,11 @@ type ( // WakeBlockHeight is the start height to // 1. enable 3s block interval WakeBlockHeight uint64 `yaml:"wakeHeight"` + // XinguBlockHeight is the start height to + // 1. enable IIP-50 slash delegates + // 2. enable candidate BLS pubkey registration and update + // 3. enable contract staking buckets storage in trie + XinguBlockHeight uint64 `yaml:"xinguHeight"` // ToBeEnabledBlockHeight is a fake height that acts as a gating factor for WIP features // upon next release, change IsToBeEnabled() to IsNextHeight() for features to be released ToBeEnabledBlockHeight uint64 `yaml:"toBeEnabledHeight"` @@ -749,6 +755,11 @@ func (g *Blockchain) IsWake(height uint64) bool { return g.isPost(g.WakeBlockHeight, height) } +// IsXingu checks whether height is equal to or larger than xingu height +func (g *Blockchain) IsXingu(height uint64) bool { + return g.isPost(g.XinguBlockHeight, height) +} + // IsToBeEnabled checks whether height is equal to or larger than toBeEnabled height func (g *Blockchain) IsToBeEnabled(height uint64) bool { return g.isPost(g.ToBeEnabledBlockHeight, height) diff --git a/blockchain/genesis/heightupgrade_test.go b/blockchain/genesis/heightupgrade_test.go index 0b31891028..c9cd8434c6 100644 --- a/blockchain/genesis/heightupgrade_test.go +++ b/blockchain/genesis/heightupgrade_test.go @@ -69,6 +69,8 @@ func TestNewHeightChange(t *testing.T) { require.True(cfg.IsVanuatu(uint64(33730921))) require.False(cfg.IsWake(uint64(36893880))) require.True(cfg.IsWake(uint64(36893881))) + require.False(cfg.IsXingu(uint64(96893880))) + require.True(cfg.IsXingu(uint64(96893881))) require.Equal(cfg.PacificBlockHeight, uint64(432001)) require.Equal(cfg.AleutianBlockHeight, uint64(864001)) @@ -96,4 +98,5 @@ func TestNewHeightChange(t *testing.T) { require.Equal(cfg.UpernavikBlockHeight, uint64(31174201)) require.Equal(cfg.VanuatuBlockHeight, uint64(33730921)) require.Equal(cfg.WakeBlockHeight, uint64(36893881)) + require.Equal(cfg.XinguBlockHeight, uint64(96893881)) } 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/cache.go b/blockindex/contractstaking/cache.go index 6f20bb0f4b..d1edaaa4ad 100644 --- a/blockindex/contractstaking/cache.go +++ b/blockindex/contractstaking/cache.go @@ -55,8 +55,6 @@ type ( ) var ( - // ErrBucketNotExist is the error when bucket does not exist - ErrBucketNotExist = errors.New("bucket does not exist") // ErrInvalidHeight is the error when height is invalid ErrInvalidHeight = errors.New("invalid height") ) diff --git a/blockindex/contractstaking/eventprocessor.go b/blockindex/contractstaking/eventprocessor.go index 6d5981ec14..790cada4d6 100644 --- a/blockindex/contractstaking/eventprocessor.go +++ b/blockindex/contractstaking/eventprocessor.go @@ -651,7 +651,7 @@ func (processor *contractStakingEventProcessor) handleDelegateChangedEvent(event bucket, err := processor.dirty.DeductBucket(processor.contractAddr, tokenIDParam.Uint64()) if err != nil { - return errors.Wrapf(ErrBucketNotExist, "token id %d", tokenIDParam.Uint64()) + return errors.Wrapf(err, "token id %d", tokenIDParam.Uint64()) } bucket.Candidate = delegateParam 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..51653f43e2 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,32 @@ 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) - } - if len(ids) != len(buckets) { - return nil, errors.Errorf("length of ids (%d) does not match length of buckets (%d)", len(ids), len(buckets)) + return nil, errors.Wrap(err, "failed to get state reader height") } - 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]) + } + calculateUnmutedVoteWeightAt := func(b *contractstaking.Bucket, height uint64) *big.Int { + vb := contractBucketToVoteBucket(0, b, s.contractAddr.String(), s.genBlockDurationFn(height)) + return s.config.CalculateVoteWeight(vb) + } + cur := stakingindex.AggregateCandidateVotes(buckets, func(b *contractstaking.Bucket) *big.Int { + return calculateUnmutedVoteWeightAt(b, s.height) + }) + processorBuilder := newEventProcessorBuilder(s.contractAddr) + cfg := &stakingindex.VoteViewConfig{ContractAddr: s.contractAddr} + mgr := stakingindex.NewCandidateVotesManager(s.ContractAddress()) + return stakingindex.NewVoteView(s, cfg, s.height, cur, processorBuilder, mgr, calculateUnmutedVoteWeightAt), nil } // Stop stops the indexer @@ -243,6 +210,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 +355,19 @@ func (s *Indexer) BucketTypes(height uint64) ([]*BucketType, error) { return bts, nil } +// ContractStakingBuckets returns all contract staking buckets +func (s *Indexer) ContractStakingBuckets() (uint64, map[uint64]*contractstaking.Bucket, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + ids, typs, infos := s.cache.Buckets() + res := make(map[uint64]*contractstaking.Bucket) + for i, id := range ids { + res[id] = assembleContractBucket(infos[i], typs[i]) + } + return s.height, res, nil +} + // PutBlock puts a block into indexer func (s *Indexer) PutBlock(ctx context.Context, blk *block.Block) error { if blk.Height() < s.config.ContractDeployHeight { diff --git a/blockindex/contractstaking/stakeview.go b/blockindex/contractstaking/stakeview.go deleted file mode 100644 index 1515c076d4..0000000000 --- a/blockindex/contractstaking/stakeview.go +++ /dev/null @@ -1,162 +0,0 @@ -package contractstaking - -import ( - "context" - "slices" - - "github.com/iotexproject/iotex-address/address" - "github.com/pkg/errors" - - "github.com/iotexproject/iotex-core/v2/action" - "github.com/iotexproject/iotex-core/v2/action/protocol" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking" - "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" -) - -type stakeView struct { - contractAddr address.Address - config Config - cache stakingCache - genBlockDurationFn func(view uint64) blocksDurationFn - height uint64 -} - -func (s *stakeView) Wrap() staking.ContractStakeView { - return &stakeView{ - contractAddr: s.contractAddr, - config: s.config, - cache: newWrappedCache(s.cache), - height: s.height, - genBlockDurationFn: s.genBlockDurationFn, - } -} - -func (s *stakeView) Fork() staking.ContractStakeView { - return &stakeView{ - contractAddr: s.contractAddr, - cache: newWrappedCacheWithCloneInCommit(s.cache), - height: s.height, - genBlockDurationFn: s.genBlockDurationFn, - } -} - -func (s *stakeView) assembleBuckets(ids []uint64, types []*BucketType, infos []*bucketInfo) []*Bucket { - vbs := make([]*Bucket, 0, len(ids)) - for i, id := range ids { - bt := types[i] - info := infos[i] - if bt != nil && info != nil { - vbs = append(vbs, s.assembleBucket(id, info, bt)) - } - } - return vbs -} - -func (s *stakeView) IsDirty() bool { - return s.cache.IsDirty() -} - -func (s *stakeView) Migrate(handler staking.EventHandler) error { - bts := s.cache.BucketTypes() - tids := make([]uint64, 0, len(bts)) - for id := range bts { - tids = append(tids, id) - } - slices.Sort(tids) - for _, id := range tids { - if err := handler.PutBucketType(s.contractAddr, bts[id]); err != nil { - return err - } - } - ids, types, infos := s.cache.Buckets() - bucketMap := make(map[uint64]*bucketInfo, len(ids)) - typeMap := make(map[uint64]*BucketType, len(ids)) - for i, id := range ids { - bucketMap[id] = infos[i] - typeMap[id] = types[i] - } - slices.Sort(ids) - for _, id := range ids { - info, ok := bucketMap[id] - if !ok { - continue - } - bt := typeMap[id] - if err := handler.PutBucket(s.contractAddr, id, &contractstaking.Bucket{ - Candidate: info.Delegate, - Owner: info.Owner, - StakedAmount: bt.Amount, - StakedDuration: bt.Duration, - CreatedAt: info.CreatedAt, - UnstakedAt: info.UnstakedAt, - UnlockedAt: info.UnlockedAt, - Muted: false, - IsTimestampBased: false, - }); err != nil { - return err - } - } - return nil -} - -func (s *stakeView) BucketsByCandidate(candidate address.Address) ([]*Bucket, error) { - ids, types, infos := s.cache.BucketsByCandidate(candidate) - return s.assembleBuckets(ids, types, infos), nil -} - -func (s *stakeView) assembleBucket(token uint64, bi *bucketInfo, bt *BucketType) *Bucket { - return assembleBucket(token, bi, bt, s.contractAddr.String(), s.genBlockDurationFn(s.height)) -} - -func (s *stakeView) CreatePreStates(ctx context.Context) error { - blkCtx := protocol.MustGetBlockCtx(ctx) - s.height = blkCtx.BlockHeight - return nil -} - -func (s *stakeView) Handle(ctx context.Context, receipt *action.Receipt) error { - // new event handler for this receipt - handler := newContractStakingDirty(newWrappedCache(s.cache)) - processor := newContractStakingEventProcessor(s.contractAddr, handler) - if err := processor.ProcessReceipts(ctx, receipt); err != nil { - return err - } - _, delta := handler.Finalize() - s.cache = delta - - return nil -} - -func (s *stakeView) Commit(ctx context.Context, sm protocol.StateManager) error { - cache, err := s.cache.Commit(ctx, s.contractAddr, sm) - if err != nil { - return err - } - s.cache = cache - return nil -} - -func (s *stakeView) AddBlockReceipts(ctx context.Context, receipts []*action.Receipt) error { - blkCtx := protocol.MustGetBlockCtx(ctx) - height := blkCtx.BlockHeight - expectHeight := s.height + 1 - if expectHeight < s.config.ContractDeployHeight { - expectHeight = s.config.ContractDeployHeight - } - if height < expectHeight { - return nil - } - if height > expectHeight { - return errors.Errorf("invalid block height %d, expect %d", height, expectHeight) - } - - handler := newContractStakingDirty(newWrappedCache(s.cache)) - processor := newContractStakingEventProcessor(s.contractAddr, handler) - if err := processor.ProcessReceipts(ctx, receipts...); err != nil { - return err - } - _, delta := handler.Finalize() - s.cache = delta - s.height = height - return nil -} diff --git a/chainservice/builder.go b/chainservice/builder.go index 83be715310..9bd18f1dc8 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) } @@ -687,7 +696,7 @@ func (builder *Builder) registerStakingProtocol() error { Staking: builder.cfg.Genesis.Staking, PersistStakingPatchBlock: builder.cfg.Chain.PersistStakingPatchBlock, FixAliasForNonStopHeight: builder.cfg.Chain.FixAliasForNonStopHeight, - SkipContractStakingViewHeight: builder.cfg.Genesis.ToBeEnabledBlockHeight, + SkipContractStakingViewHeight: builder.cfg.Genesis.XinguBlockHeight, StakingPatchDir: builder.cfg.Chain.StakingPatchDir, Revise: staking.ReviseConfig{ VoteWeight: builder.cfg.Genesis.VoteWeightCalConsts, 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/config/config.go b/config/config.go index 1d15607cd3..8727584b0d 100644 --- a/config/config.go +++ b/config/config.go @@ -342,6 +342,8 @@ func ValidateForkHeights(cfg Config) error { return errors.Wrap(ErrInvalidCfg, "Upernavik is heigher than Vanuatu") case hu.VanuatuBlockHeight > hu.WakeBlockHeight: return errors.Wrap(ErrInvalidCfg, "Vanuatu is heigher than Wake") + case hu.WakeBlockHeight > hu.XinguBlockHeight: + return errors.Wrap(ErrInvalidCfg, "Wake is heigher than Xingu") } return nil } diff --git a/config/config_test.go b/config/config_test.go index 3b5e79068e..73cf53801a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -381,6 +381,9 @@ func TestValidateForkHeights(t *testing.T) { { "Vanuatu", ErrInvalidCfg, "Vanuatu is heigher than Wake", }, + { + "Wake", ErrInvalidCfg, "Wake is heigher than Xingu", + }, { "", nil, "", }, @@ -447,6 +450,8 @@ func newTestCfg(fork string) Config { cfg.Genesis.UpernavikBlockHeight = cfg.Genesis.VanuatuBlockHeight + 1 case "Vanuatu": cfg.Genesis.VanuatuBlockHeight = cfg.Genesis.WakeBlockHeight + 1 + case "Wake": + cfg.Genesis.WakeBlockHeight = cfg.Genesis.XinguBlockHeight + 1 } return cfg } diff --git a/e2etest/contract_staking_v2_test.go b/e2etest/contract_staking_v2_test.go index c4ba1e8559..c23dccd89c 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" @@ -14,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" + "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" "github.com/iotexproject/iotex-address/address" @@ -26,9 +28,11 @@ import ( "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/log" "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,15 +52,432 @@ 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.XinguBlockHeight = 200 // store buckets in trie + cfg.Genesis.SystemStakingContractAddress = contractAddress + cfg.Genesis.SystemStakingContractHeight = 1 + cfg.Genesis.SystemStakingContractV2Address = "" + cfg.Genesis.SystemStakingContractV3Address = "" + cfg.DardanellesUpgrade.BlockInterval = time.Second * 8640 + cfg.WakeUpgrade.BlockInterval = cfg.DardanellesUpgrade.BlockInterval / 2 + 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") + }}, + }, + }, + }) + + tipHeight, err := test.cs.BlockDAO().Height() + require.NoError(err) + var blocksPerDayWake = 24 * time.Hour / cfg.WakeUpgrade.BlockInterval + test.run([]*testcase{ + { + name: "wake", + preFunc: func(e *e2etest) { + candidate, err := e.getCandidateByName("cand2") + require.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + require.True(ok) + }, + preActs: genTransferActionsWithPrice(int(cfg.Genesis.WakeBlockHeight-tipHeight), gasPrice1559), + acts: genTransferActionsWithPrice(1, gasPrice1559), + blockExpect: func(test *e2etest, blk *block.Block, err error) { + require.NoError(err) + }, + }, + }) + + tipHeight, err = test.cs.BlockDAO().Height() + require.NoError(err) + var bktIdx uint64 + test.run([]*testcase{ + { + name: "migrate to trie", + preFunc: func(e *e2etest) { + candidate, err := e.getCandidateByName("cand2") + require.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + require.True(ok) + }, + preActs: genTransferActionsWithPrice(int(cfg.Genesis.XinguBlockHeight-tipHeight), gasPrice1559), + acts: genTransferActionsWithPrice(1, gasPrice1559), + blockExpect: func(test *e2etest, blk *block.Block, err error) { + require.NoError(err) + candidate, err := test.getCandidateByName("cand2") + require.NoError(err) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }, + }, + { + 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, gasPrice1559, 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(contractAddress, receipt) + require.NoError(err) + require.Equal(1, len(bktIdxs)) + bktIdx = bktIdxs[0] + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()*24/uint64(blocksPerDayWake)) * (time.Hour), StakedAmount: stakeAmount}, false) + require.Equal(tmpVotes.Add(tmpVotes, deltaVotes).String(), candidate.TotalWeightedVotes) + + checkStakingVoteView(test, require, "cand1") + }}, + }, + }, + }) + test.run([]*testcase{ + { + name: "unlock", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("unlock(uint256)", big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), unlockTime}, + expect: []actionExpect{successExpect, + &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()*24/uint64(blocksPerDay)) * (time.Hour), StakedAmount: stakeAmount}, false) + unlockedVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: false, StakedDuration: time.Duration(stakeDurationBlocks.Uint64()*24/uint64(blocksPerDay)) * (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, gasPrice1559, mustCallData("lock(uint256,uint256)", big.NewInt(int64(bktIdx)), big.NewInt(0).Mul(big.NewInt(2), stakeDurationBlocks)), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &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()*24/uint64(blocksPerDayWake)) * (time.Hour), StakedAmount: stakeAmount}, false) + postVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(2*stakeDurationBlocks.Uint64()*24/uint64(blocksPerDayWake)) * (time.Hour), StakedAmount: stakeAmount}, false) + log.L().Info("original Votes:", zap.String("votes", tmpVotes.String())) + log.L().Info("preStake Votes:", zap.String("votes", preStakeVotes.String())) + log.L().Info("post Votes:", zap.String("votes", postVotes.String())) + 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, gasPrice1559, mustCallData("unlock(uint256)", big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), unlockTime}, + }, genTransferActionsWithPrice(20, gasPrice1559)...), + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("unstake(uint256)", big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + log.L().Info("original Votes:", zap.String("votes", tmpVotes.String())) + preStakeVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(2*stakeDurationBlocks.Uint64()*24/uint64(blocksPerDayWake)) * (time.Hour), StakedAmount: stakeAmount}, false) + log.L().Info("preStake Votes:", zap.String("votes", preStakeVotes.String())) + tmpVotes.Sub(tmpVotes, preStakeVotes) + log.L().Info("new Votes:", zap.String("votes", tmpVotes.String())) + 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: genTransferActionsWithPrice(30, gasPrice1559), + act: &actionWithTime{mustNoErr(action.SignedExecution(contractAddress, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("withdraw(uint256,address)", big.NewInt(int64(bktIdx)), common.BytesToAddress(identityset.Address(beneficiaryID).Bytes())), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &noBucketExpect{bktIdx, 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") + }}, + }, + }, + }) + + checkStakingViewInit(test, require) +} + func TestContractStakingV2(t *testing.T) { require := require.New(t) contractAddress := stakingContractV2Address cfg := initCfg(require) cfg.Genesis.UpernavikBlockHeight = 1 cfg.Genesis.VanuatuBlockHeight = 100 - cfg.Genesis.WakeBlockHeight = 120 // mute staking v2 + cfg.Genesis.WakeBlockHeight = 120 // mute staking v2 + cfg.Genesis.XinguBlockHeight = 200 // store contract staking in trie + 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 +548,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 +566,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 +584,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 +603,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 +625,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 +643,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 +667,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 +686,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 +696,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 +717,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 +755,8 @@ func TestContractStakingV2(t *testing.T) { _, err := test.getBucket(idx, contractAddress) require.NoError(err) } + checkStakingVoteView(test, require, "cand1") + checkStakingVoteView(test, require, "cand2") }, }, }) @@ -327,6 +771,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 +794,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 +828,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 +856,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 +884,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 +918,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 +947,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 +971,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 +1006,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 +1036,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 +1066,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 +1088,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 +1107,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,10 +1132,35 @@ func TestContractStakingV2(t *testing.T) { cand1, err := test.getCandidateByName("cand1") require.NoError(err) require.Equal(tmpVotes.String(), cand1.TotalWeightedVotes) + checkStakingVoteView(test, require, "cand1") }, }, }) + // case: after xingu + tipHeight, err = test.cs.BlockDAO().Height() + require.NoError(err) + test.run([]*testcase{ + { + name: "migrate to trie", + preFunc: func(e *e2etest) { + candidate, err := e.getCandidateByName("cand1") + require.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + require.True(ok) + tmpBkt, err = e.getBucket(legacyBucketIdxs[5], contractAddress) + require.NoError(err) + }, + preActs: genTransferActionsWithPrice(int(cfg.Genesis.XinguBlockHeight-tipHeight), gasPrice1559), + acts: genTransferActionsWithPrice(1, gasPrice1559), + blockExpect: func(test *e2etest, blk *block.Block, err error) { + require.NoError(err) + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }, + }, + }) checkStakingViewInit(test, require) } @@ -688,7 +1173,9 @@ func TestContractStakingV3(t *testing.T) { cfg := initCfg(require) cfg.Genesis.UpernavikBlockHeight = 1 cfg.Genesis.VanuatuBlockHeight = 100 - cfg.Genesis.WakeBlockHeight = 120 // mute staking v2 & enable staking v3 + cfg.Genesis.WakeBlockHeight = 120 // mute staking v2 & enable staking v3 + cfg.Genesis.XinguBlockHeight = 200 // store contract staking in trie + cfg.Genesis.SystemStakingContractAddress = "" cfg.Genesis.SystemStakingContractV2Address = contractV2Address cfg.Genesis.SystemStakingContractV2Height = 1 cfg.Genesis.SystemStakingContractV3Address = contractV3Address @@ -716,6 +1203,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 +1444,284 @@ 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") + }}, + }, + }, + }) + + // case: after xingu + tipHeight, err := test.cs.BlockDAO().Height() + require.NoError(err) + test.run([]*testcase{ + { + name: "migrate to trie", + preFunc: func(e *e2etest) { + candidate, err := e.getCandidateByName("cand1") + require.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + require.True(ok) + }, + preActs: genTransferActionsWithPrice(int(cfg.Genesis.XinguBlockHeight-tipHeight), gasPrice1559), + acts: genTransferActionsWithPrice(1, gasPrice1559), + blockExpect: func(test *e2etest, blk *block.Block, err error) { + require.NoError(err) + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }, + }, + { + 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(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), stakeAmount, gasLimit, gasPrice1559, mustCallData("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) + bkts, err := parseV3StakedBucketIdx(contractV3Address, receipt) + require.NoError(err) + require.Len(bkts, 1) + bktIdx = bkts[0] + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationSeconds.Int64()) * (time.Second), StakedAmount: stakeAmount, Timestamped: true}, false) + require.Equal(tmpVotes.Add(tmpVotes, deltaVotes).String(), candidate.TotalWeightedVotes) + }}, + }, + }, + }) + test.run([]*testcase{ + { + name: "unlock", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallDataV3("unlock(uint256)", big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), unlockTime}, + expect: []actionExpect{successExpect, + &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(stakeDurationSeconds.Int64()) * (time.Second), StakedAmount: stakeAmount}, false) + unlockedVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: false, StakedDuration: time.Duration(stakeDurationSeconds.Int64()) * (time.Second), StakedAmount: stakeAmount}, false) + tmpVotes.Sub(tmpVotes, lockedStakeVotes) + tmpVotes.Add(tmpVotes, unlockedVotes) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }}, + }, + }, + { + name: "lock", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallDataV3("lock(uint256,uint256)", big.NewInt(int64(bktIdx)), big.NewInt(0).Mul(big.NewInt(2), stakeDurationSeconds)), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &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(stakeDurationSeconds.Uint64()) * (time.Second), StakedAmount: stakeAmount}, false) + postVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(2*stakeDurationSeconds.Uint64()) * (time.Second), StakedAmount: stakeAmount}, false) + tmpVotes.Sub(tmpVotes, preStakeVotes) + tmpVotes.Add(tmpVotes, postVotes) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }}, + }, + }, + { + name: "unstake", + preActs: []*actionWithTime{ + {mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("unlock(uint256)", big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), unlockTime}, + }, + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("unstake(uint256)", big.NewInt(int64(bktIdx))), action.WithChainID(chainID))), unstakeTime}, + expect: []actionExpect{successExpect}, + blockExpect: func(test *e2etest, blk *block.Block, err error) { + require.NoError(err) + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + preStakeVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(2*stakeDurationSeconds.Uint64()) * (time.Second), StakedAmount: stakeAmount}, false) + tmpVotes.Sub(tmpVotes, preStakeVotes) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }, + }, + { + 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) + }, + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallDataV3("withdraw(uint256,address)", big.NewInt(int64(bktIdx)), common.BytesToAddress(identityset.Address(beneficiaryID).Bytes())), action.WithChainID(chainID))), withdrawTime}, + expect: []actionExpect{successExpect, + &noBucketExpect{bktIdx, contractV3Address}, + &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) + }}, + }, + }, + }) + test.run([]*testcase{ + { + name: "stake", + preFunc: func(*e2etest) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + _, ok := tmpVotes.SetString(candidate.TotalWeightedVotes, 10) + require.True(ok) + }, + 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) { + bktIdxs, err := parseV3StakedBucketIdx(contractV3Address, receipt) + require.NoError(err) + require.Len(bktIdxs, 1) + bktIdx = bktIdxs[0] + }}, + }, + }, + }) + var tmpBktIdxs []uint64 + test.run([]*testcase{ + { + name: "change candidate", + act: &actionWithTime{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) { + candidate, err := test.getCandidateByName("cand1") + require.NoError(err) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }}, + }, + }, + { + 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(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0).Mul(big.NewInt(10), stakeAmount), gasLimit, gasPrice1559, mustCallDataV3("stake(uint256,uint256,address,uint256)", stakeAmount, stakeDurationSeconds, common.BytesToAddress(identityset.Address(candOwnerID2).Bytes()), big.NewInt(10)), action.WithChainID(chainID))), stakeTime}, + expect: []actionExpect{successExpect, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + bktIdxs, err := parseV3StakedBucketIdx(contractV3Address, receipt) + require.NoError(err) + require.Equal(10, len(bktIdxs)) + tmpBktIdxs = bktIdxs + candidate, err := test.getCandidateByName("cand2") + require.NoError(err) + deltaVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationSeconds.Uint64()) * (time.Second), StakedAmount: stakeAmount}, false) + tmpVotes.Add(tmpVotes, deltaVotes.Mul(deltaVotes, big.NewInt(10))) + require.Equal(tmpVotes.String(), candidate.TotalWeightedVotes) + }}, + }, + }, + }) + test.run([]*testcase{ + { + name: "merge", + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("merge(uint256[],uint256)", []*big.Int{big.NewInt(int64(tmpBktIdxs[0])), big.NewInt(int64(tmpBktIdxs[1])), big.NewInt(int64(tmpBktIdxs[2]))}, stakeDurationSeconds), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &noBucketExpect{tmpBktIdxs[1], contractV3Address}, &noBucketExpect{tmpBktIdxs[2], contractV3Address}, + &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(stakeDurationSeconds.Uint64()) * (time.Second), StakedAmount: stakeAmount}, false) + addVotes := staking.CalculateVoteWeight(test.cfg.Genesis.VoteWeightCalConsts, &staking.VoteBucket{AutoStake: true, StakedDuration: time.Duration(stakeDurationSeconds.Uint64()) * (time.Second), 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) + }}, + }, + }, + { + name: "expand", + act: &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(tmpBktIdxs[0])), big.NewInt(0).Mul(stakeDurationSeconds, big.NewInt(2))), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect}, + }, + { + name: "donate", + preFunc: func(e *e2etest) { + resp, err := test.api.GetAccount(context.Background(), &iotexapi.GetAccountRequest{Address: identityset.Address(beneficiaryID).String()}) + require.NoError(err) + _, ok := tmpBalance.SetString(resp.AccountMeta.Balance, 10) + require.True(ok) + }, + act: &actionWithTime{mustNoErr(action.SignedExecution(contractV3Address, identityset.PrivateKey(stakerID), test.nonceMgr.pop(identityset.Address(stakerID).String()), big.NewInt(0), gasLimit, gasPrice1559, mustCallData("donate(uint256,uint256)", big.NewInt(int64(tmpBktIdxs[0])), stakeAmount), action.WithChainID(chainID))), time.Now()}, + expect: []actionExpect{successExpect, + &functionExpect{func(test *e2etest, act *action.SealedEnvelope, receipt *action.Receipt, err error) { + resp, err := test.api.GetAccount(context.Background(), &iotexapi.GetAccountRequest{Address: identityset.Address(beneficiaryID).String()}) + require.NoError(err) + tmpBalance.Add(tmpBalance, stakeAmount) + require.Equal(tmpBalance.String(), resp.AccountMeta.Balance) + }}, + }, + }, + }) checkStakingViewInit(test, require) } @@ -1324,6 +2083,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/e2etest.go b/e2etest/e2etest.go index 8a6cf3c275..a91ff19f7f 100644 --- a/e2etest/e2etest.go +++ b/e2etest/e2etest.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/ethclient" @@ -33,6 +34,7 @@ import ( "github.com/iotexproject/iotex-core/v2/config" "github.com/iotexproject/iotex-core/v2/pkg/util/abiutil" "github.com/iotexproject/iotex-core/v2/server/itx" + "github.com/iotexproject/iotex-core/v2/systemcontractindex/stakingindex" "github.com/iotexproject/iotex-core/v2/testutil" ) @@ -411,7 +413,15 @@ func clearDBPaths(cfg *config.Config) { testutil.CleanupPath(cfg.Chain.HistoryIndexPath) } +func parseV3StakedBucketIdx(contract string, receipt *action.Receipt) ([]uint64, error) { + return parseStakedBucketIdx(contract, stakingindex.StakingContractABI, receipt) +} + func parseV2StakedBucketIdx(contract string, receipt *action.Receipt) ([]uint64, error) { + return parseStakedBucketIdx(contract, staking.StakingContractABI, receipt) +} + +func parseStakedBucketIdx(contract string, _abi abi.ABI, receipt *action.Receipt) ([]uint64, error) { if uint64(iotextypes.ReceiptStatus_Success) != receipt.Status { return nil, nil } @@ -420,7 +430,7 @@ func parseV2StakedBucketIdx(contract string, receipt *action.Receipt) ([]uint64, if log.Address != contract { continue } - abiEvent, err := staking.StakingContractABI.EventByID(common.Hash(log.Topics[0])) + abiEvent, err := _abi.EventByID(common.Hash(log.Topics[0])) if err != nil { return nil, errors.Wrapf(err, "get event abi from topic %v failed", log.Topics[0]) } 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/e2etest/rewarding_test.go b/e2etest/rewarding_test.go index 4a7e34aba4..2cf28842ca 100644 --- a/e2etest/rewarding_test.go +++ b/e2etest/rewarding_test.go @@ -69,6 +69,8 @@ func TestBlockReward(t *testing.T) { cfg.Genesis = genesis.TestDefault() initDBPaths(r, &cfg) defer func() { clearDBPaths(&cfg) }() + cfg.API.GRPCPort = 0 + cfg.API.HTTPPort = 0 cfg.Consensus.Scheme = config.RollDPoSScheme cfg.Genesis.NumDelegates = 1 cfg.Genesis.NumSubEpochs = 10 @@ -97,8 +99,8 @@ func TestBlockReward(t *testing.T) { cfg.Genesis.DardanellesBlockHeight = 1 // enable block reward cfg.Genesis.GreenlandBlockHeight = 6 cfg.Genesis.KamchatkaBlockHeight = 7 - cfg.Genesis.VanuatuBlockHeight = 8 // enable dynamic fee - cfg.Genesis.ToBeEnabledBlockHeight = 10 // enable wake block reward + cfg.Genesis.VanuatuBlockHeight = 8 // enable dynamic fee + cfg.Genesis.WakeBlockHeight = 10 // enable wake block reward testutil.NormalizeGenesisHeights(&cfg.Genesis.Blockchain) block.LoadGenesisHash(&cfg.Genesis) @@ -204,7 +206,7 @@ func TestBlockReward(t *testing.T) { // fixed block reward assert.Equal(t, cfg.Genesis.DardanellesBlockReward().String(), rewards[rewardingpb.RewardLog_BLOCK_REWARD].String()) assert.Equal(t, big.NewInt(0).String(), rewards[rewardingpb.RewardLog_PRIORITY_BONUS].String()) - case blockHeight < cfg.Genesis.ToBeEnabledBlockHeight: + case blockHeight < cfg.Genesis.WakeBlockHeight: // fixed block reward + priority bonus require.True(t, rewards[rewardingpb.RewardLog_PRIORITY_BONUS].Sign() > 0, "blockHeight %d", blockHeight) assert.Equal(t, cfg.Genesis.DardanellesBlockReward().String(), rewards[rewardingpb.RewardLog_BLOCK_REWARD].String()) diff --git a/e2etest/staking_test.go b/e2etest/staking_test.go index 11e218c76c..23e83ed8d3 100644 --- a/e2etest/staking_test.go +++ b/e2etest/staking_test.go @@ -196,6 +196,8 @@ func TestStakingContract(t *testing.T) { delete(cfg.Plugins, config.GatewayPlugin) }() + cfg.API.GRPCPort = 0 + cfg.API.HTTPPort = 0 cfg.ActPool.MinGasPriceStr = "0" cfg.Chain.TrieDBPatchFile = "" cfg.Chain.ProducerPrivKey = "a000000000000000000000000000000000000000000000000000000000000000" diff --git a/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..594b210208 100644 --- a/state/factory/erigonstore/accountstorage.go +++ b/state/factory/erigonstore/accountstorage.go @@ -33,8 +33,15 @@ func newAccountStorage(addr common.Address, backend *contractBackend) (*accountS }, nil } -func (as *accountStorage) Delete([]byte) error { - return errors.New("not implemented") +func (as *accountStorage) Delete(key []byte) error { + exist, err := as.contract.Remove(key) + if err != nil { + return errors.Wrapf(err, "failed to remove account data for key %x", key) + } + if !exist { + return errors.Wrapf(state.ErrStateNotExist, "key: %x", key) + } + return nil } func (as *accountStorage) Batch([][]byte) (state.Iterator, error) { diff --git a/state/factory/erigonstore/objectstorage.go b/state/factory/erigonstore/objectstorage.go index 216c10d1e1..11c51d792b 100644 --- a/state/factory/erigonstore/objectstorage.go +++ b/state/factory/erigonstore/objectstorage.go @@ -56,7 +56,14 @@ func (cos *contractObjectStorage) Load(key []byte, obj any) error { } func (cos *contractObjectStorage) Delete(key []byte) error { - return cos.contract.Remove(key) + exist, err := cos.contract.Remove(key) + if err != nil { + return errors.Wrapf(err, "failed to remove data for key %x", key) + } + if !exist { + return errors.Wrapf(state.ErrStateNotExist, "key: %x", key) + } + return nil } func (cos *contractObjectStorage) List() (state.Iterator, error) { diff --git a/state/factory/erigonstore/registry.go b/state/factory/erigonstore/registry.go index b7df0b71c1..468b171149 100644 --- a/state/factory/erigonstore/registry.go +++ b/state/factory/erigonstore/registry.go @@ -2,6 +2,7 @@ package erigonstore import ( "reflect" + "strings" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" @@ -28,7 +29,8 @@ var ( // ObjectStorageRegistry is a registry for object storage type ObjectStorageRegistry struct { contracts map[string]map[reflect.Type]int - fallback map[string]int + ns map[string]int + nsPrefix map[string]int } func init() { @@ -37,6 +39,10 @@ func init() { assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandidateNamespace, CandidatesContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.CandsMapNamespace, CandidateMapContractIndex)) assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingNamespace, BucketPoolContractIndex)) + assertions.MustNoError(storageRegistry.RegisterNamespace(state.StakingViewNamespace, StakingViewContractIndex)) + assertions.MustNoError(storageRegistry.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)) @@ -57,7 +63,8 @@ func GetObjectStorageRegistry() *ObjectStorageRegistry { func newObjectStorageRegistry() *ObjectStorageRegistry { return &ObjectStorageRegistry{ contracts: make(map[string]map[reflect.Type]int), - fallback: make(map[string]int), + ns: make(map[string]int), + nsPrefix: make(map[string]int), } } @@ -107,12 +114,28 @@ func (osr *ObjectStorageRegistry) RegisterNamespace(ns string, index int) error return osr.register(ns, nil, index) } +// RegisterNamespacePrefix registers a namespace prefix object storage +func (osr *ObjectStorageRegistry) RegisterNamespacePrefix(prefix string, index int) error { + if index < AccountIndex || index >= SystemContractCount { + return errors.Errorf("invalid system contract index %d", index) + } + return osr.registerPrefix(prefix, index) +} + +func (osr *ObjectStorageRegistry) registerPrefix(ns string, index int) error { + if _, exists := osr.nsPrefix[ns]; exists { + return errors.Wrapf(ErrObjectStorageAlreadyRegistered, "registered: %v", osr.nsPrefix[ns]) + } + osr.nsPrefix[ns] = index + return nil +} + func (osr *ObjectStorageRegistry) register(ns string, obj any, index int) error { if obj == nil { - if _, exists := osr.fallback[ns]; exists { - return errors.Wrapf(ErrObjectStorageAlreadyRegistered, "registered: %v", osr.fallback[ns]) + if _, exists := osr.ns[ns]; exists { + return errors.Wrapf(ErrObjectStorageAlreadyRegistered, "registered: %v", osr.ns[ns]) } - osr.fallback[ns] = index + osr.ns[ns] = index return nil } types, ok := osr.contracts[ns] @@ -128,6 +151,7 @@ func (osr *ObjectStorageRegistry) register(ns string, obj any, index int) error } func (osr *ObjectStorageRegistry) matchContractIndex(ns string, obj any) (int, bool) { + // object specific storage if obj != nil { types, ok := osr.contracts[ns] if ok { @@ -137,6 +161,16 @@ func (osr *ObjectStorageRegistry) matchContractIndex(ns string, obj any) (int, b } } } - index, exist := osr.fallback[ns] - return index, exist + // namespace specific storage + index, exist := osr.ns[ns] + if exist { + return index, true + } + // namespace prefix specific storage + for prefix, index := range osr.nsPrefix { + if strings.HasPrefix(ns, prefix) { + return index, true + } + } + return 0, false } diff --git a/state/factory/erigonstore/systemcontracts.go b/state/factory/erigonstore/systemcontracts.go index c9a627b9bf..8b657ef95e 100644 --- a/state/factory/erigonstore/systemcontracts.go +++ b/state/factory/erigonstore/systemcontracts.go @@ -22,6 +22,9 @@ type SystemContract struct { const ( // AccountIndex is the system contract for account storage AccountIndex = -1 +) + +const ( // StakingBucketsContractIndex is the system contract for staking buckets storage StakingBucketsContractIndex int = iota // BucketPoolContractIndex is the system contract for bucket pool storage diff --git a/state/factory/erigonstore/workingsetstore_erigon.go b/state/factory/erigonstore/workingsetstore_erigon.go index 62f482444c..b852267195 100644 --- a/state/factory/erigonstore/workingsetstore_erigon.go +++ b/state/factory/erigonstore/workingsetstore_erigon.go @@ -440,3 +440,7 @@ func (store *ErigonWorkingSetStore) NewObjectStorage(ns string, obj any) (Object return nil, err } } + +func (store *ErigonWorkingSetStore) ErigonStore() (any, error) { + return store, nil +} diff --git a/state/factory/statedb.go b/state/factory/statedb.go index 0db3aa5811..03477f449b 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" @@ -282,7 +284,7 @@ func (sdb *stateDB) createWorkingSetStore(ctx context.Context, height uint64, kv flusher, err := db.NewKVStoreFlusher( kvstore, batch.NewCachedBatch(), - sdb.flusherOptions(!g.IsEaster(height))..., + sdb.flusherOptions(!g.IsEaster(height), g.IsXingu(height))..., ) if err != nil { return nil, err @@ -558,7 +560,7 @@ func (sdb *stateDB) StateReaderAt(blkHeight uint64, blkHash hash.Hash256) (proto // private trie constructor functions //====================================== -func (sdb *stateDB) flusherOptions(preEaster bool) []db.KVStoreFlusherOption { +func (sdb *stateDB) flusherOptions(preEaster, storeContractStaking bool) []db.KVStoreFlusherOption { opts := []db.KVStoreFlusherOption{ db.SerializeOption(func(wi *batch.WriteInfo) []byte { if preEaster { @@ -567,15 +569,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/workingset.go b/state/factory/workingset.go index 2d4799cb8c..ccd87834dc 100644 --- a/state/factory/workingset.go +++ b/state/factory/workingset.go @@ -361,7 +361,11 @@ func (ws *workingSet) State(s interface{}, opts ...protocol.StateOption) (uint64 if cfg.Keys != nil { return 0, errors.Wrap(ErrNotSupported, "Read state with keys option has not been implemented yet") } - return ws.height, ws.store.GetObject(cfg.Namespace, cfg.Key, s) + store, err := ws.matchStore(cfg) + if err != nil { + return 0, err + } + return ws.height, store.GetObject(cfg.Namespace, cfg.Key, s) } func (ws *workingSet) States(opts ...protocol.StateOption) (uint64, state.Iterator, error) { @@ -372,7 +376,11 @@ func (ws *workingSet) States(opts ...protocol.StateOption) (uint64, state.Iterat if cfg.Key != nil { return 0, nil, errors.Wrap(ErrNotSupported, "Read states with key option has not been implemented yet") } - iter, err := ws.store.States(cfg.Namespace, cfg.Object, cfg.Keys) + store, err := ws.matchStore(cfg) + if err != nil { + return 0, nil, err + } + iter, err := store.States(cfg.Namespace, cfg.Object, cfg.Keys) if err != nil { return 0, nil, err } @@ -387,7 +395,11 @@ func (ws *workingSet) PutState(s interface{}, opts ...protocol.StateOption) (uin if err != nil { return ws.height, err } - return ws.height, ws.store.PutObject(cfg.Namespace, cfg.Key, s) + store, err := ws.matchStore(cfg) + if err != nil { + return ws.height, err + } + return ws.height, store.PutObject(cfg.Namespace, cfg.Key, s) } // DelState deletes a state from DB @@ -397,7 +409,11 @@ func (ws *workingSet) DelState(opts ...protocol.StateOption) (uint64, error) { if err != nil { return ws.height, err } - return ws.height, ws.store.DeleteObject(cfg.Namespace, cfg.Key, cfg.Object) + store, err := ws.matchStore(cfg) + if err != nil { + return ws.height, err + } + return ws.height, store.DeleteObject(cfg.Namespace, cfg.Key, cfg.Object) } // ReadView reads the view @@ -1060,3 +1076,15 @@ func (ws *workingSet) Erigon() (*erigonstate.IntraBlockState, bool) { return nil, false } } + +func (ws *workingSet) matchStore(cfg *protocol.StateConfig) (workingSetStore, error) { + store := ws.store + if cfg.ErigonStoreOnly { + erigonStore, err := store.ErigonStore() + if err != nil { + return nil, err + } + store = erigonStore.(workingSetStore) + } + return store, nil +} diff --git a/state/factory/workingsetstore.go b/state/factory/workingsetstore.go index cf5919fcf6..be60455c1b 100644 --- a/state/factory/workingsetstore.go +++ b/state/factory/workingsetstore.go @@ -37,6 +37,7 @@ type ( ResetSnapshots() Close() CreateGenesisStates(context.Context) error + ErigonStore() (any, error) } stateDBWorkingSetStore struct { @@ -213,3 +214,7 @@ func (store *stateDBWorkingSetStore) CreateGenesisStates(ctx context.Context) er func (store *stateDBWorkingSetStore) KVStore() db.KVStore { return store } + +func (store *stateDBWorkingSetStore) ErigonStore() (any, error) { + return nil, errors.Wrap(state.ErrErigonStoreNotSupported, "failed to get erigon store") +} diff --git a/state/factory/workingsetstore_erigon_simulate.go b/state/factory/workingsetstore_erigon_simulate.go index d31d604275..3b5b8e8539 100644 --- a/state/factory/workingsetstore_erigon_simulate.go +++ b/state/factory/workingsetstore_erigon_simulate.go @@ -86,3 +86,7 @@ func (store *erigonWorkingSetStoreForSimulate) CreateGenesisStates(ctx context.C func (store *erigonWorkingSetStoreForSimulate) KVStore() db.KVStore { return nil } + +func (store *erigonWorkingSetStoreForSimulate) ErigonStore() (any, error) { + return store, nil +} diff --git a/state/factory/workingsetstore_with_secondary.go b/state/factory/workingsetstore_with_secondary.go index 7ca49dc934..0bebb8271d 100644 --- a/state/factory/workingsetstore_with_secondary.go +++ b/state/factory/workingsetstore_with_secondary.go @@ -159,3 +159,7 @@ func (store *workingSetStoreWithSecondary) States(ns string, obj any, keys [][]b func (store *workingSetStoreWithSecondary) KVStore() db.KVStore { return nil } + +func (store *workingSetStoreWithSecondary) ErigonStore() (any, error) { + return store.writerSecondary, nil +} diff --git a/state/state.go b/state/state.go index 0751fedb68..fed2ac9591 100644 --- a/state/state.go +++ b/state/state.go @@ -18,6 +18,9 @@ var ( // ErrStateNotExist is the error that the state does not exist ErrStateNotExist = errors.New("state does not exist") + + // ErrErigonStoreNotSupported is the error that the erigon store is not supported + ErrErigonStoreNotSupported = errors.New("erigon store not supported") ) // State is the interface, which defines the common methods for state struct to be handled by state factory 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/cache.go b/systemcontractindex/stakingindex/cache.go index 8882f7c53e..254348c941 100644 --- a/systemcontractindex/stakingindex/cache.go +++ b/systemcontractindex/stakingindex/cache.go @@ -70,7 +70,7 @@ func (s *base) Load(kvstore db.KVStore, ns, bucketNS string) error { if err := b.Deserialize(vs[i]); err != nil { return err } - s.PutBucket(byteutil.BytesToUint64BigEndian(ks[i]), &b) + s.putBucket(byteutil.BytesToUint64BigEndian(ks[i]), &b) } return nil } @@ -104,6 +104,10 @@ func (s *base) Clone() indexerCache { func (s *base) PutBucket(id uint64, bkt *Bucket) { s.mu.Lock() defer s.mu.Unlock() + s.putBucket(id, bkt) +} + +func (s *base) putBucket(id uint64, bkt *Bucket) { cand := bkt.Candidate.String() if s.buckets[id] != nil { prevCand := s.buckets[id].Candidate.String() diff --git a/systemcontractindex/stakingindex/cache_test.go b/systemcontractindex/stakingindex/cache_test.go index 7b6a579a58..b982cd155c 100644 --- a/systemcontractindex/stakingindex/cache_test.go +++ b/systemcontractindex/stakingindex/cache_test.go @@ -8,9 +8,10 @@ import ( "go.uber.org/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/iotexproject/iotex-core/v2/test/identityset" "github.com/iotexproject/iotex-core/v2/test/mock/mock_chainmanager" - "github.com/stretchr/testify/require" ) func TestBase(t *testing.T) { @@ -64,7 +65,7 @@ func TestBase(t *testing.T) { require.Equal(1, len(cache.BucketIdxs())) t.Run("commit", func(t *testing.T) { sm := mock_chainmanager.NewMockStateManager(ctrl) - sm.EXPECT().DelState(gomock.Any(), gomock.Any()).Return(uint64(0), nil).Times(1) + sm.EXPECT().DelState(gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(0), nil).Times(1) sm.EXPECT().PutState(gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(0), nil).Times(1) _, err := cache.Commit(context.Background(), identityset.Address(10), false, sm) require.NoError(err) diff --git a/systemcontractindex/stakingindex/candidate_votes.go b/systemcontractindex/stakingindex/candidate_votes.go new file mode 100644 index 0000000000..e25b131aa7 --- /dev/null +++ b/systemcontractindex/stakingindex/candidate_votes.go @@ -0,0 +1,298 @@ +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" +) + +var ( + // ErrCandidateVotesIsDirty is returned when candidate votes are dirty + ErrCandidateVotesIsDirty = errors.New("candidate votes is dirty") +) + +// CandidateVotes is the interface to manage candidate votes +type CandidateVotes interface { + Clone() CandidateVotes + Votes(fCtx protocol.FeatureCtx, cand string) *big.Int + Add(cand string, amount *big.Int, votes *big.Int) + 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 candidateVotesWithBuffer struct { + base *candidateVotes + change *candidateVotes +} + +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) 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) 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 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(), + } +} + +func (cv *candidateVotesWraper) IsDirty() bool { + return len(cv.change.cands) > 0 || 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) 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) { + if cv.IsDirty() { + return nil, errors.Wrap(ErrCandidateVotesIsDirty, "cannot serialize dirty candidate votes") + } + return cv.base.Serialize() +} + +func (cv *candidateVotesWraper) Deserialize(data []byte) error { + cv.change = newCandidateVotes() + return cv.base.Deserialize(data) +} + +func (cv *candidateVotesWraper) Base() CandidateVotes { + return cv.base.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() +} + +func (cv *candidateVotesWraperCommitInClone) Base() CandidateVotes { + return cv.base +} + +func newCandidateVotesWithBuffer(base *candidateVotes) *candidateVotesWithBuffer { + return &candidateVotesWithBuffer{ + base: base, + change: newCandidateVotes(), + } +} + +func (cv *candidateVotesWithBuffer) Clone() CandidateVotes { + return &candidateVotesWithBuffer{ + base: cv.base.Clone(), + change: cv.change.Clone(), + } +} + +func (cv *candidateVotesWithBuffer) IsDirty() bool { + return len(cv.change.cands) > 0 +} + +func (cv *candidateVotesWithBuffer) Votes(fCtx protocol.FeatureCtx, cand string) *big.Int { + base := cv.base.Votes(fCtx, cand) + change := cv.change.Votes(fCtx, cand) + if change == nil { + return base + } + if base == nil { + return change + } + return new(big.Int).Add(base, change) +} + +func (cv *candidateVotesWithBuffer) Add(cand string, amount *big.Int, votes *big.Int) { + cv.change.Add(cand, amount, votes) +} + +func (cv *candidateVotesWithBuffer) Commit() CandidateVotes { + // Commit the changes to the base + for cand, change := range cv.change.cands { + cv.base.Add(cand, change.amount, change.votes) + } + cv.change = newCandidateVotes() + return cv +} + +func (cv *candidateVotesWithBuffer) Serialize() ([]byte, error) { + if cv.IsDirty() { + return nil, errors.Wrap(ErrCandidateVotesIsDirty, "cannot serialize dirty candidate votes") + } + return cv.base.Serialize() +} + +func (cv *candidateVotesWithBuffer) Deserialize(data []byte) error { + cv.change = newCandidateVotes() + return cv.base.Deserialize(data) +} + +func (cv *candidateVotesWithBuffer) Base() CandidateVotes { + return newCandidateVotesWithBuffer(cv.base) +} + +func (cv *candidateVotesWithBuffer) Encode() (systemcontracts.GenericValue, error) { + if cv.IsDirty() { + return systemcontracts.GenericValue{}, errors.Wrap(ErrCandidateVotesIsDirty, "cannot encode dirty candidate votes") + } + return cv.base.Encode() +} + +func (cv *candidateVotesWithBuffer) Decode(data systemcontracts.GenericValue) error { + cv.change = newCandidateVotes() + return cv.base.Decode(data) +} diff --git a/systemcontractindex/stakingindex/candidate_votes_manager.go b/systemcontractindex/stakingindex/candidate_votes_manager.go new file mode 100644 index 0000000000..0abe85364a --- /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 newCandidateVotesWithBuffer(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..217519cc1c --- /dev/null +++ b/systemcontractindex/stakingindex/candidate_votes_test.go @@ -0,0 +1,214 @@ +package stakingindex + +import ( + "context" + "fmt" + "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()) + }) +} + +func TestCandidateVotesInterface(t *testing.T) { + r := require.New(t) + + cvs := []CandidateVotes{ + newCandidateVotesWithBuffer(newCandidateVotes()), + newCandidateVotesWrapper(newCandidateVotesWithBuffer(newCandidateVotes())), + newCandidateVotesWrapperCommitInClone(newCandidateVotesWithBuffer(newCandidateVotes())), + } + g := genesis.TestDefault() + ctx := genesis.WithGenesisContext(context.Background(), g) + ctxBeforeRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight - 1}))) + ctxAfterRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight}))) + for _, cv := range cvs { + t.Run(fmt.Sprintf("%T", cv), func(t *testing.T) { + // not exist candidate + r.Nil(cv.Votes(ctxAfterRedsea, "notexist")) + r.Nil(cv.Votes(ctxBeforeRedsea, "notexist")) + r.False(cv.IsDirty()) + + // add candidate + adds := []struct { + cand string + amount string + votes string + }{ + {"candidate1", "100", "120"}, + {"candidate2", "200", "240"}, + {"candidate1", "300", "360"}, + {"candidate3", "400", "480"}, + {"candidate2", "500", "600"}, + {"candidate2", "-200", "-240"}, + } + expects := []struct { + cand string + amount string + votes string + }{ + {"candidate1", "400", "480"}, + {"candidate2", "500", "600"}, + {"candidate3", "400", "480"}, + } + for _, add := range adds { + amount := big.NewInt(0) + votes := big.NewInt(0) + _, ok := amount.SetString(add.amount, 10) + r.True(ok) + _, ok = votes.SetString(add.votes, 10) + r.True(ok) + cv.Add(add.cand, amount, votes) + } + checkVotes := func(cv CandidateVotes) { + for _, expect := range expects { + amount := big.NewInt(0) + votes := big.NewInt(0) + _, ok := amount.SetString(expect.amount, 10) + r.True(ok) + _, ok = votes.SetString(expect.votes, 10) + r.True(ok) + r.Equal(amount, cv.Votes(ctxBeforeRedsea, expect.cand)) + r.Equal(votes, cv.Votes(ctxAfterRedsea, expect.cand)) + } + } + checkVotes(cv) + cl := cv.Clone() + checkVotes(cl) + // both cv and cl are dirty + r.True(cv.IsDirty()) + r.True(cl.IsDirty()) + // serialize dirty cv should fail + _, err := cv.Serialize() + r.ErrorIs(err, ErrCandidateVotesIsDirty) + // commit cv + reset := cv.Commit() + r.False(reset.IsDirty()) + data, err := reset.Serialize() + r.NoError(err) + // deserialize to new cv + decv := newCandidateVotes() + err = decv.Deserialize(data) + r.NoError(err) + checkVotes(newCandidateVotesWithBuffer(decv)) + // adds not affect base + cv.Add("candidate4", big.NewInt(1000), big.NewInt(1200)) + cv.Add("candidate1", big.NewInt(-100), big.NewInt(-120)) + cv.Add("candidate2", big.NewInt(100), big.NewInt(120)) + checkVotes(cv.Base()) + }) + } +} + +func TestCandidateVotesWrapper(t *testing.T) { + r := require.New(t) + baseCv := newCandidateVotesWithBuffer(newCandidateVotes()) + baseCv.Add("candidate1", big.NewInt(100), big.NewInt(120)) + baseCv.Add("candidate2", big.NewInt(200), big.NewInt(240)) + baseCv.Add("candidate3", big.NewInt(400), big.NewInt(480)) + base := baseCv.Commit() + // wrap's changes should not affect base + wrap := newCandidateVotesWrapper(base) + wrap.Add("candidate1", big.NewInt(300), big.NewInt(360)) + wrap.Add("candidate4", big.NewInt(1000), big.NewInt(1200)) + candidateVotesEqual(r, base, wrap.Base(), []string{"candidate1", "candidate2", "candidate3", "candidate4"}) + // multiple wraps return base recursively + wrap2 := newCandidateVotesWrapper(wrap) + wrap2.Add("candidate2", big.NewInt(500), big.NewInt(600)) + wrap2.Add("candidate5", big.NewInt(2000), big.NewInt(2400)) + candidateVotesEqual(r, base, wrap2.Base(), []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) + // commit wrap should apply all changes to base + wrap2.Commit() + candidateVotesEqual(r, base, wrap2, []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) +} + +func TestCandidateVotesWrapperCommitInClone(t *testing.T) { + r := require.New(t) + baseCv := newCandidateVotesWithBuffer(newCandidateVotes()) + baseCv.Add("candidate1", big.NewInt(100), big.NewInt(120)) + baseCv.Add("candidate2", big.NewInt(200), big.NewInt(240)) + baseCv.Add("candidate3", big.NewInt(400), big.NewInt(480)) + base := baseCv.Commit() + wrap := newCandidateVotesWrapper(base) + wrap.Add("candidate1", big.NewInt(300), big.NewInt(360)) + wrap.Add("candidate4", big.NewInt(1000), big.NewInt(1200)) + wrap2 := newCandidateVotesWrapper(wrap) + wrap2.Add("candidate2", big.NewInt(500), big.NewInt(600)) + wrap2.Add("candidate5", big.NewInt(2000), big.NewInt(2400)) + wrap3 := newCandidateVotesWrapper(wrap) + wrap3.Add("candidate3", big.NewInt(700), big.NewInt(840)) + wrap3.Add("candidate1", big.NewInt(-3000), big.NewInt(-3600)) + wrap3Clone := wrap3.Clone() + + // base return the first base + wrap4 := newCandidateVotesWrapperCommitInClone(wrap3) + wrap4.Add("candidate2", big.NewInt(3000), big.NewInt(3600)) + wrap4.Add("candidate3", big.NewInt(4000), big.NewInt(4800)) + candidateVotesEqual(r, wrap4.Base(), wrap3, []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) + // commit wrap4 should not apply all changes to base + wrap4.Commit() + candidateVotesEqual(r, wrap3, wrap3Clone, []string{"candidate1", "candidate2", "candidate3", "candidate4", "candidate5"}) +} + +func candidateVotesEqual(r *require.Assertions, cv1, cv2 CandidateVotes, cands []string) { + g := genesis.TestDefault() + ctx := genesis.WithGenesisContext(context.Background(), g) + ctxBeforeRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight - 1}))) + ctxAfterRedsea := protocol.MustGetFeatureCtx(protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: g.RedseaBlockHeight}))) + for _, cand := range cands { + r.Equal(cv1.Votes(ctxBeforeRedsea, cand), cv2.Votes(ctxBeforeRedsea, cand)) + r.Equal(cv1.Votes(ctxAfterRedsea, cand), cv2.Votes(ctxAfterRedsea, cand)) + } +} diff --git a/systemcontractindex/stakingindex/eventprocessor_builder.go b/systemcontractindex/stakingindex/eventprocessor_builder.go new file mode 100644 index 0000000000..b81ba25df6 --- /dev/null +++ b/systemcontractindex/stakingindex/eventprocessor_builder.go @@ -0,0 +1,30 @@ +package stakingindex + +import ( + "context" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" +) + +type eventProcessorBuilder struct { + contractAddr address.Address + timestamped bool + muteHeight uint64 +} + +func newEventProcessorBuilder(contractAddr address.Address, timestamped bool, muteHeight uint64) *eventProcessorBuilder { + return &eventProcessorBuilder{ + contractAddr: contractAddr, + timestamped: timestamped, + muteHeight: muteHeight, + } +} + +func (b *eventProcessorBuilder) Build(ctx context.Context, handler staking.EventHandler) staking.EventProcessor { + blkCtx := protocol.MustGetBlockCtx(ctx) + muted := b.muteHeight > 0 && blkCtx.BlockHeight >= b.muteHeight + return newEventProcessor(b.contractAddr, blkCtx, handler, b.timestamped, muted) +} diff --git a/systemcontractindex/stakingindex/index.go b/systemcontractindex/stakingindex/index.go index f8e0fb77ed..14c73b4bf2 100644 --- a/systemcontractindex/stakingindex/index.go +++ b/systemcontractindex/stakingindex/index.go @@ -2,6 +2,7 @@ package stakingindex import ( "context" + "math/big" "sync" "time" @@ -42,23 +43,27 @@ type ( PutBlock(ctx context.Context, blk *block.Block) error LoadStakeView(context.Context, protocol.StateReader) (staking.ContractStakeView, error) CreateEventProcessor(context.Context, staking.EventHandler) staking.EventProcessor + ContractStakingBuckets() (uint64, map[uint64]*Bucket, error) + staking.BucketReader } // Indexer is the staking indexer Indexer struct { - common *systemcontractindex.IndexerCommon - cache *base // in-memory cache, used to query index data - mutex sync.RWMutex - blocksToDuration blocksDurationAtFn // function to calculate duration from block range - bucketNS string - ns string - muteHeight uint64 - timestamped bool + common *systemcontractindex.IndexerCommon + cache *base // in-memory cache, used to query index data + mutex sync.RWMutex + blocksToDuration blocksDurationAtFn // function to calculate duration from block range + bucketNS string + ns string + muteHeight uint64 + timestamped bool + calculateVoteWeight CalculateVoteWeightFunc } // IndexerOption is the option to create an indexer IndexerOption func(*Indexer) - blocksDurationFn func(start uint64, end uint64) time.Duration - blocksDurationAtFn func(start uint64, end uint64, viewAt uint64) time.Duration + blocksDurationFn func(start uint64, end uint64) time.Duration + blocksDurationAtFn func(start uint64, end uint64, viewAt uint64) time.Duration + CalculateVoteWeightFunc func(v *VoteBucket) *big.Int ) // WithMuteHeight sets the mute height @@ -75,8 +80,15 @@ func EnableTimestamped() IndexerOption { } } +// WithCalculateUnmutedVoteWeightFn sets the function to calculate unmuted vote weight +func WithCalculateUnmutedVoteWeightFn(f CalculateVoteWeightFunc) IndexerOption { + return func(s *Indexer) { + s.calculateVoteWeight = f + } +} + // NewIndexer creates a new staking indexer -func NewIndexer(kvstore db.KVStore, contractAddr address.Address, startHeight uint64, blocksToDurationFn blocksDurationAtFn, opts ...IndexerOption) *Indexer { +func NewIndexer(kvstore db.KVStore, contractAddr address.Address, startHeight uint64, blocksToDurationFn blocksDurationAtFn, opts ...IndexerOption) (*Indexer, error) { bucketNS := contractAddr.String() + "#" + stakingBucketNS ns := contractAddr.String() + "#" + stakingNS idx := &Indexer{ @@ -89,7 +101,10 @@ func NewIndexer(kvstore db.KVStore, contractAddr address.Address, startHeight ui for _, opt := range opts { opt(idx) } - return idx + if idx.calculateVoteWeight == nil { + return nil, errors.New("calculateVoteWeight function is not set") + } + return idx, nil } // Start starts the indexer @@ -127,6 +142,20 @@ func (s *Indexer) CreateEventProcessor(ctx context.Context, handler staking.Even ) } +// DeductBucket deducts the bucket from the indexer +func (s *Indexer) DeductBucket(addr address.Address, id uint64) (*contractstaking.Bucket, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + if s.ContractAddress().String() != addr.String() { + return nil, errors.Wrap(contractstaking.ErrBucketNotExist, "contract address not match") + } + bkt := s.cache.Bucket(id) + if bkt == nil { + return nil, errors.Wrap(contractstaking.ErrBucketNotExist, "bucket not exist") + } + return bkt, nil +} + // LoadStakeView loads the contract stake view from state reader func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (staking.ContractStakeView, error) { s.mutex.RLock() @@ -134,44 +163,36 @@ func (s *Indexer) LoadStakeView(ctx context.Context, sr protocol.StateReader) (s if !s.common.Started() { return nil, errors.New("indexer not started") } - if protocol.MustGetFeatureCtx(ctx).StoreVoteOfNFTBucketIntoView { - return &stakeView{ - cache: s.cache.Clone(), - height: s.common.Height(), - contractAddr: s.common.ContractAddress(), - muteHeight: s.muteHeight, - timestamped: s.timestamped, - startHeight: s.common.StartHeight(), - bucketNS: s.bucketNS, - genBlockDurationFn: s.genBlockDurationFn, - }, nil - } - contractAddr := s.common.ContractAddress() - ids, buckets, err := contractstaking.NewStateReader(sr).Buckets(contractAddr) + if !protocol.MustGetFeatureCtx(ctx).StoreVoteOfNFTBucketIntoView { + return nil, nil + } + srHeight, err := sr.Height() if err != nil { - return nil, errors.Wrapf(err, "failed to get buckets for contract %s", contractAddr) + return nil, errors.Wrap(err, "failed to get state reader height") } - if len(ids) != len(buckets) { - return nil, errors.Errorf("length of ids (%d) does not match length of buckets (%d)", len(ids), len(buckets)) + if 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(s, 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 @@ -358,3 +379,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 newCandidateVotesWithBuffer(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..d6e852f535 --- /dev/null +++ b/systemcontractindex/stakingindex/voteview.go @@ -0,0 +1,184 @@ +package stakingindex + +import ( + "context" + "math/big" + "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 ( + // 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 { + indexer staking.ContractStakingIndexer + config *VoteViewConfig + height uint64 + cur CandidateVotes + store BucketStore + cvm CandidateVotesManager + processorBuilder EventProcessorBuilder + calculateVoteWeightFn CalculateUnmutedVoteWeightAtFn + } +) + +// NewVoteView creates a new vote view +func NewVoteView( + indexer staking.ContractStakingIndexer, + cfg *VoteViewConfig, + height uint64, + cur CandidateVotes, + processorBuilder EventProcessorBuilder, + cvm CandidateVotesManager, + fn CalculateUnmutedVoteWeightAtFn, +) staking.ContractStakeView { + return &voteView{ + indexer: indexer, + 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{ + indexer: s.indexer, + 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{ + indexer: s.indexer, + 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) buckets(ctx context.Context) (map[uint64]*contractstaking.Bucket, error) { + h, buckets, err := s.indexer.ContractStakingBuckets() + if err != nil { + return nil, err + } + blkCtx := protocol.MustGetBlockCtx(ctx) + if s.indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { + return nil, errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) + } + return buckets, nil +} + +func (s *voteView) Migrate(ctx context.Context, handler staking.EventHandler) error { + h, buckets, err := s.indexer.ContractStakingBuckets() + if err != nil { + return err + } + blkCtx := protocol.MustGetBlockCtx(ctx) + if s.indexer.StartHeight() <= blkCtx.BlockHeight && h != blkCtx.BlockHeight-1 { + return errors.Errorf("bucket cache height %d does not match current height %d", h, blkCtx.BlockHeight-1) + } + sortedIDs := make([]uint64, 0, len(buckets)) + for id := range buckets { + sortedIDs = append(sortedIDs, id) + } + slices.Sort(sortedIDs) + for _, id := range sortedIDs { + if err := handler.PutBucket(s.config.ContractAddr, id, buckets[id]); err != nil { + return err + } + } + return nil +} + +func (s *voteView) Revise(ctx context.Context) { + buckets, err := s.buckets(ctx) + if err != nil { + return + } + s.cur = AggregateCandidateVotes(buckets, func(b *contractstaking.Bucket) *big.Int { + return s.calculateVoteWeightFn(b, s.height) + }) +} + +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) error { + blkCtx := protocol.MustGetBlockCtx(ctx) + s.height = blkCtx.BlockHeight + s.store = newBucketStore(s.indexer) + 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() + if sm == nil { + return nil + } + return s.cvm.Store(ctx, sm, s.cur) +} diff --git a/systemcontractindex/stakingindex/voteview_test.go b/systemcontractindex/stakingindex/voteview_test.go new file mode 100644 index 0000000000..bc075cd0c1 --- /dev/null +++ b/systemcontractindex/stakingindex/voteview_test.go @@ -0,0 +1,78 @@ +package stakingindex + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/iotexproject/iotex-address/address" + + "github.com/iotexproject/iotex-core/v2/action/protocol" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking" + "github.com/iotexproject/iotex-core/v2/action/protocol/staking/contractstaking" + "github.com/iotexproject/iotex-core/v2/test/identityset" +) + +func TestVoteView(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIndexer := staking.NewMockContractStakingIndexer(ctrl) + mr := NewCandidateVotesManager(identityset.Address(0)) + vv := NewVoteView( + mockIndexer, + &VoteViewConfig{ + ContractAddr: identityset.Address(0), + }, + 100, + nil, + nil, + mr, + func(b *contractstaking.Bucket, h uint64) *big.Int { + return big.NewInt(1) + }, + ) + mockHandler := staking.NewMockEventHandler(ctrl) + t.Run("Migrate", func(t *testing.T) { + buckets := make(map[uint64]*contractstaking.Bucket) + buckets[1] = &contractstaking.Bucket{ + Candidate: identityset.Address(1), + Owner: identityset.Address(11), + StakedAmount: big.NewInt(1000000000000000000), + StakedDuration: 10, + CreatedAt: 123456, + UnlockedAt: 0, + UnstakedAt: 0, + IsTimestampBased: false, + Muted: false, + } + buckets[2] = &contractstaking.Bucket{ + Candidate: identityset.Address(2), + Owner: identityset.Address(12), + StakedAmount: big.NewInt(1000000000000000000), + StakedDuration: 10, + CreatedAt: 123456, + UnlockedAt: 234567, + UnstakedAt: 0, + IsTimestampBased: true, + Muted: false, + } + migratedBuckets := make(map[uint64]*contractstaking.Bucket) + ctx := protocol.WithBlockCtx(context.Background(), protocol.BlockCtx{ + BlockHeight: 100, + }) + mockIndexer.EXPECT().StartHeight().Return(uint64(0)).Times(1) + mockIndexer.EXPECT().ContractStakingBuckets().Return(uint64(99), buckets, nil) + mockHandler.EXPECT().PutBucket(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(addr address.Address, id uint64, bucket *contractstaking.Bucket) error { + require.Equal(identityset.Address(0), addr) + migratedBuckets[id] = bucket + return nil + }).Times(len(buckets)) + require.NoError(vv.Migrate(ctx, mockHandler)) + require.Equal(buckets, migratedBuckets) + }) +} diff --git a/systemcontracts/generic_storage.go b/systemcontracts/generic_storage.go index 24d0413e14..002c0bc5d4 100644 --- a/systemcontracts/generic_storage.go +++ b/systemcontracts/generic_storage.go @@ -5,6 +5,7 @@ import ( "math/big" "strings" + "github.com/erigontech/erigon/core/vm" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -142,16 +143,16 @@ func (g *GenericStorageContract) Get(key []byte) (*GetResult, error) { } // Remove deletes data by key -func (g *GenericStorageContract) Remove(key []byte) error { +func (g *GenericStorageContract) Remove(key []byte) (bool, error) { // Validate input if len(key) == 0 { - return errors.New("key cannot be empty") + return false, errors.New("key cannot be empty") } // Pack the function call data, err := g.abi.Pack("remove", key) if err != nil { - return errors.Wrap(err, "failed to pack remove call") + return false, errors.Wrap(err, "failed to pack remove call") } // Execute the transaction @@ -164,13 +165,16 @@ func (g *GenericStorageContract) Remove(key []byte) error { } if err := g.backend.Handle(callMsg); err != nil { - return errors.Wrap(err, "failed to execute remove") + if errors.Is(err, vm.ErrExecutionReverted) && strings.Contains(err.Error(), "Key does not exist") { + return false, nil + } + return false, errors.Wrap(err, "failed to execute remove") } log.L().Debug("Successfully removed data", zap.String("key", string(key))) - return nil + return true, nil } // BatchGet retrieves multiple values by their keys diff --git a/systemcontracts/namespace_storage.go b/systemcontracts/namespace_storage.go index 654a57979b..1f30fb5466 100644 --- a/systemcontracts/namespace_storage.go +++ b/systemcontracts/namespace_storage.go @@ -5,6 +5,7 @@ import ( "math/big" "strings" + "github.com/erigontech/erigon/core/vm" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -154,19 +155,19 @@ func (ns *NamespaceStorageContract) Get(namespace string, key []byte) (*Namespac } // Remove deletes data by namespace and key -func (ns *NamespaceStorageContract) Remove(namespace string, key []byte) error { +func (ns *NamespaceStorageContract) Remove(namespace string, key []byte) (bool, error) { // Validate input if len(namespace) == 0 { - return errors.New("namespace cannot be empty") + return false, errors.New("namespace cannot be empty") } if len(key) == 0 { - return errors.New("key cannot be empty") + return false, errors.New("key cannot be empty") } // Pack the function call data, err := ns.abi.Pack("remove", namespace, key) if err != nil { - return errors.Wrap(err, "failed to pack remove call") + return false, errors.Wrap(err, "failed to pack remove call") } // Execute the transaction @@ -179,14 +180,18 @@ func (ns *NamespaceStorageContract) Remove(namespace string, key []byte) error { } if err := ns.backend.Handle(callMsg); err != nil { - return errors.Wrap(err, "failed to execute remove") + if errors.Is(err, vm.ErrExecutionReverted) && (strings.Contains(err.Error(), "Key does not exist") || + strings.Contains(err.Error(), "Namespace does not exist")) { + return false, nil + } + return false, errors.Wrap(err, "failed to execute remove") } log.L().Debug("Successfully removed data", zap.String("namespace", namespace), zap.String("key", string(key))) - return nil + return true, nil } // Exists checks if a key exists in a namespace diff --git a/systemcontracts/namespace_storage_wrapper.go b/systemcontracts/namespace_storage_wrapper.go index c994ced1cc..0b7167ccd7 100644 --- a/systemcontracts/namespace_storage_wrapper.go +++ b/systemcontracts/namespace_storage_wrapper.go @@ -45,7 +45,7 @@ func (ns *NamespaceStorageContractWrapper) Get(key []byte) (*NamespaceGetResult, } // Remove deletes data by namespace and key -func (ns *NamespaceStorageContractWrapper) Remove(key []byte) error { +func (ns *NamespaceStorageContractWrapper) Remove(key []byte) (bool, error) { return ns.contract.Remove(ns.ns, key) } diff --git a/systemcontracts/storage.go b/systemcontracts/storage.go index 5217b50330..821ab6d36a 100644 --- a/systemcontracts/storage.go +++ b/systemcontracts/storage.go @@ -51,7 +51,7 @@ type ( Address() common.Address Put(key []byte, value GenericValue) error Get(key []byte) (*GetResult, error) - Remove(key []byte) error + Remove(key []byte) (bool, error) Exists(key []byte) (bool, error) List(uint64, uint64) (*ListResult, error) ListKeys(uint64, uint64) (*ListKeysResult, error) diff --git a/testutil/genesis.go b/testutil/genesis.go index 5be6a2a11d..93465db195 100644 --- a/testutil/genesis.go +++ b/testutil/genesis.go @@ -32,6 +32,7 @@ func NormalizeGenesisHeights(g *genesis.Blockchain) { &g.UpernavikBlockHeight, &g.VanuatuBlockHeight, &g.WakeBlockHeight, + &g.XinguBlockHeight, &g.ToBeEnabledBlockHeight, } for i := len(heights) - 2; i >= 0; i-- {