From b0bf39861d394e45d4ff0506bc001fee69d5fef9 Mon Sep 17 00:00:00 2001 From: Rongjian Lan Date: Thu, 20 May 2021 13:17:02 -0700 Subject: [PATCH] Add min commission rate logic (#3715) * Add min commission rate logic --- core/evm.go | 5 +++ core/staking_verifier.go | 10 ++++++ core/staking_verifier_test.go | 63 ++++++++++++++++++++++++++++++++- core/state/statedb.go | 17 +++++++++ core/state_transition.go | 1 + core/vm/interface.go | 1 + internal/chain/engine.go | 19 ++++++++-- internal/params/config.go | 27 ++++++++++++++ staking/availability/measure.go | 35 ++++++++++++++++++ staking/params.go | 18 +++++----- staking/types/test/prototype.go | 4 +-- 11 files changed, 187 insertions(+), 13 deletions(-) diff --git a/core/evm.go b/core/evm.go index c58a4f8c8..608b79fbb 100644 --- a/core/evm.go +++ b/core/evm.go @@ -19,6 +19,8 @@ package core import ( "math/big" + "github.com/harmony-one/harmony/internal/params" + "github.com/ethereum/go-ethereum/common" "github.com/harmony-one/harmony/block" consensus_engine "github.com/harmony-one/harmony/consensus/engine" @@ -44,6 +46,9 @@ type ChainContext interface { // ReadValidatorList returns the list of all validators ReadValidatorList() ([]common.Address, error) + + // Config returns chain config + Config() *params.ChainConfig } // NewEVMContext creates a new context for use in the EVM. diff --git a/core/staking_verifier.go b/core/staking_verifier.go index c3c12e98d..2eda5ed57 100644 --- a/core/staking_verifier.go +++ b/core/staking_verifier.go @@ -4,6 +4,8 @@ import ( "bytes" "math/big" + "github.com/harmony-one/harmony/staking/availability" + "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/crypto/bls" @@ -166,6 +168,14 @@ func VerifyAndEditValidatorFromMsg( return nil, errCommissionRateChangeTooHigh } + if chainContext.Config().IsMinCommissionRate(epoch) && newRate.LT(availability.MinCommissionRate) { + firstEpoch := stateDB.GetValidatorFirstElectionEpoch(msg.ValidatorAddress) + promoPeriod := chainContext.Config().MinCommissionPromoPeriod.Int64() + if firstEpoch.Uint64() != 0 && big.NewInt(0).Sub(epoch, firstEpoch).Int64() >= promoPeriod { + return nil, errCommissionRateChangeTooLow + } + } + snapshotValidator, err := chainContext.ReadValidatorSnapshot(wrapper.Address) if err != nil { return nil, errors.WithMessage(err, "validator snapshot not found.") diff --git a/core/staking_verifier_test.go b/core/staking_verifier_test.go index 86afe12e0..e6fa4022c 100644 --- a/core/staking_verifier_test.go +++ b/core/staking_verifier_test.go @@ -55,8 +55,10 @@ var ( hundredKOnes = new(big.Int).Mul(big.NewInt(100000), oneBig) negRate = numeric.NewDecWithPrec(-1, 10) + pointZeroOneDec = numeric.NewDecWithPrec(1, 2) pointOneDec = numeric.NewDecWithPrec(1, 1) pointTwoDec = numeric.NewDecWithPrec(2, 1) + pointFourDec = numeric.NewDecWithPrec(4, 1) pointFiveDec = numeric.NewDecWithPrec(5, 1) pointSevenDec = numeric.NewDecWithPrec(7, 1) pointEightFiveDec = numeric.NewDecWithPrec(85, 2) @@ -465,7 +467,7 @@ func TestVerifyAndEditValidatorFromMsg(t *testing.T) { expWrapper: func() staking.ValidatorWrapper { vw := defaultExpWrapperEditValidator() vw.UpdateHeight = big.NewInt(defaultSnapBlockNumber) - vw.Rate = pointFiveDec + vw.Rate = pointFourDec return vw }(), }, @@ -655,6 +657,51 @@ func TestVerifyAndEditValidatorFromMsg(t *testing.T) { expErr: errors.New("banned status"), }, + { + // 14: Rate is lower than min rate of 5% + sdb: makeStateDBForStake(t), + bc: func() *fakeChainContext { + chain := makeFakeChainContextForStake() + vw := chain.vWrappers[validatorAddr] + vw.Rate = pointFourDec + chain.vWrappers[validatorAddr] = vw + return chain + }(), + epoch: big.NewInt(20), + blockNum: big.NewInt(defaultBlockNumber), + msg: func() staking.EditValidator { + msg := defaultMsgEditValidator() + msg.CommissionRate = &pointZeroOneDec + return msg + }(), + + expErr: errCommissionRateChangeTooLow, + }, + { + // 15: Rate is ok within the promo period + sdb: makeStateDBForStake(t), + bc: func() *fakeChainContext { + chain := makeFakeChainContextForStake() + vw := chain.vWrappers[validatorAddr] + vw.Rate = pointFourDec + chain.vWrappers[validatorAddr] = vw + return chain + }(), + epoch: big.NewInt(15), + blockNum: big.NewInt(defaultBlockNumber), + msg: func() staking.EditValidator { + msg := defaultMsgEditValidator() + msg.CommissionRate = &pointZeroOneDec + return msg + }(), + + expWrapper: func() staking.ValidatorWrapper { + vw := defaultExpWrapperEditValidator() + vw.UpdateHeight = big.NewInt(defaultBlockNumber) + vw.Rate = pointZeroOneDec + return vw + }(), + }, } for i, test := range tests { w, err := VerifyAndEditValidatorFromMsg(test.sdb, test.bc, test.epoch, test.blockNum, @@ -1588,6 +1635,7 @@ func updateStateValidators(sdb *state.DB, ws []*staking.ValidatorWrapper) error for i, w := range ws { sdb.SetValidatorFlag(w.Address) sdb.AddBalance(w.Address, hundredKOnes) + sdb.SetValidatorFirstElectionEpoch(w.Address, big.NewInt(10)) if err := sdb.UpdateValidatorWrapper(w.Address, w); err != nil { return fmt.Errorf("update %v vw error: %v", i, err) } @@ -1675,6 +1723,13 @@ func (chain *fakeChainContext) Engine() consensus_engine.Engine { return nil } +func (chain *fakeChainContext) Config() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.MinCommissionRateEpoch = big.NewInt(0) + config.MinCommissionPromoPeriod = big.NewInt(10) + return config +} + func (chain *fakeChainContext) GetHeader(common.Hash, uint64) *block.Header { return nil } @@ -1709,6 +1764,12 @@ func (chain *fakeErrChainContext) GetHeader(common.Hash, uint64) *block.Header { return nil } +func (chain *fakeErrChainContext) Config() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.MinCommissionRateEpoch = big.NewInt(10) + return config +} + func (chain *fakeErrChainContext) ReadDelegationsByDelegator(common.Address) (staking.DelegationIndexes, error) { return nil, nil } diff --git a/core/state/statedb.go b/core/state/statedb.go index 32273657e..e28e6dd55 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -859,6 +859,23 @@ func (db *DB) UpdateValidatorWrapper( return nil } +// SetValidatorFirstElectionEpoch sets the epoch when the validator is first elected +func (db *DB) SetValidatorFirstElectionEpoch(addr common.Address, epoch *big.Int) { + firstEpoch := db.GetValidatorFirstElectionEpoch(addr) + if firstEpoch.Uint64() == 0 { + // Set only when it's not set (or it's 0) + bytes := common.BigToHash(epoch) + db.SetState(addr, staking.FirstElectionEpochKey, bytes) + } +} + +// GetValidatorFirstElectionEpoch gets the epoch when the validator was first elected +func (db *DB) GetValidatorFirstElectionEpoch(addr common.Address) *big.Int { + so := db.getStateObject(addr) + value := so.GetState(db.db, staking.FirstElectionEpochKey) + return value.Big() +} + // SetValidatorFlag checks whether it is a validator object func (db *DB) SetValidatorFlag(addr common.Address) { db.SetState(addr, staking.IsValidatorKey, staking.IsValidator) diff --git a/core/state_transition.go b/core/state_transition.go index 342f11703..00121b516 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -43,6 +43,7 @@ var ( errNoDelegationToUndelegate = errors.New("no delegation to undelegate") errCommissionRateChangeTooFast = errors.New("change on commission rate can not be more than max change rate within the same epoch") errCommissionRateChangeTooHigh = errors.New("commission rate can not be higher than maximum commission rate") + errCommissionRateChangeTooLow = errors.New("commission rate can not be lower than min rate of 5%") errNoRewardsToCollect = errors.New("no rewards to collect") errNegativeAmount = errors.New("amount can not be negative") errDupIdentity = errors.New("validator identity exists") diff --git a/core/vm/interface.go b/core/vm/interface.go index bec445224..d5dba0dde 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -47,6 +47,7 @@ type StateDB interface { SetValidatorFlag(common.Address) UnsetValidatorFlag(common.Address) IsValidator(common.Address) bool + GetValidatorFirstElectionEpoch(addr common.Address) *big.Int AddReward(*staking.ValidatorWrapper, *big.Int, map[common.Address]numeric.Dec) error AddRefund(uint64) diff --git a/internal/chain/engine.go b/internal/chain/engine.go index e0c34994a..051ad1554 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -5,6 +5,8 @@ import ( "math/big" "sort" + "github.com/harmony-one/harmony/internal/params" + bls2 "github.com/harmony-one/bls/ffi/go/bls" blsvrf "github.com/harmony-one/harmony/crypto/vrf/bls" @@ -290,7 +292,7 @@ func (e *engineImpl) Finalize( // Needs to be after payoutUndelegations because payoutUndelegations // depends on the old LastEpochInCommittee - if err := setLastEpochInCommittee(header, state); err != nil { + if err := setElectionEpochAndMinFee(header, state, chain.Config()); err != nil { return nil, nil, err } @@ -392,7 +394,7 @@ func IsCommitteeSelectionBlock(chain engine.ChainReader, header *block.Header) b return isBeaconChain && header.IsLastBlockInEpoch() && inPreStakingEra } -func setLastEpochInCommittee(header *block.Header, state *state.DB) error { +func setElectionEpochAndMinFee(header *block.Header, state *state.DB, config *params.ChainConfig) error { newShardState, err := header.GetShardState() if err != nil { const msg = "[Finalize] failed to read shard state" @@ -405,7 +407,20 @@ func setLastEpochInCommittee(header *block.Header, state *state.DB) error { "[Finalize] failed to get validator from state to finalize", ) } + // Set last epoch in committee wrapper.LastEpochInCommittee = newShardState.Epoch + + if config.IsMinCommissionRate(newShardState.Epoch) { + // Set first election epoch + state.SetValidatorFirstElectionEpoch(addr, newShardState.Epoch) + + // Update minimum commission fee + if err := availability.UpdateMinimumCommissionFee( + newShardState.Epoch, state, addr, config.MinCommissionPromoPeriod.Int64(), + ); err != nil { + return err + } + } } return nil } diff --git a/internal/params/config.go b/internal/params/config.go index c9bdfdb64..450891bc9 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -52,6 +52,8 @@ var ( NoEarlyUnlockEpoch: big.NewInt(530), // Around Monday Apr 12th 2021, 22:30 UTC VRFEpoch: EpochTBD, MinDelegation100Epoch: EpochTBD, + MinCommissionRateEpoch: EpochTBD, + MinCommissionPromoPeriod: big.NewInt(100), EPoSBound35Epoch: EpochTBD, EIP155Epoch: big.NewInt(28), S3Epoch: big.NewInt(28), @@ -77,6 +79,8 @@ var ( NoEarlyUnlockEpoch: big.NewInt(73580), VRFEpoch: EpochTBD, MinDelegation100Epoch: EpochTBD, + MinCommissionRateEpoch: EpochTBD, + MinCommissionPromoPeriod: big.NewInt(10), EPoSBound35Epoch: EpochTBD, EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), @@ -103,6 +107,8 @@ var ( NoEarlyUnlockEpoch: big.NewInt(0), VRFEpoch: big.NewInt(0), MinDelegation100Epoch: big.NewInt(0), + MinCommissionRateEpoch: big.NewInt(0), + MinCommissionPromoPeriod: big.NewInt(10), EPoSBound35Epoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), @@ -129,6 +135,8 @@ var ( NoEarlyUnlockEpoch: big.NewInt(0), VRFEpoch: big.NewInt(0), MinDelegation100Epoch: big.NewInt(0), + MinCommissionRateEpoch: big.NewInt(0), + MinCommissionPromoPeriod: big.NewInt(10), EPoSBound35Epoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), @@ -155,6 +163,8 @@ var ( NoEarlyUnlockEpoch: big.NewInt(0), VRFEpoch: big.NewInt(0), MinDelegation100Epoch: big.NewInt(0), + MinCommissionRateEpoch: big.NewInt(0), + MinCommissionPromoPeriod: big.NewInt(10), EPoSBound35Epoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), @@ -180,6 +190,8 @@ var ( NoEarlyUnlockEpoch: big.NewInt(0), VRFEpoch: big.NewInt(0), MinDelegation100Epoch: big.NewInt(0), + MinCommissionRateEpoch: big.NewInt(0), + MinCommissionPromoPeriod: big.NewInt(10), EPoSBound35Epoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), @@ -207,6 +219,8 @@ var ( big.NewInt(0), // NoEarlyUnlockEpoch big.NewInt(0), // VRFEpoch big.NewInt(0), // MinDelegation100Epoch + big.NewInt(0), // MinCommissionRateEpoch + big.NewInt(10), // MinCommissionPromoPeriod big.NewInt(0), // EPoSBound35Epoch big.NewInt(0), // EIP155Epoch big.NewInt(0), // S3Epoch @@ -234,6 +248,8 @@ var ( big.NewInt(0), // NoEarlyUnlockEpoch big.NewInt(0), // VRFEpoch big.NewInt(0), // MinDelegation100Epoch + big.NewInt(0), // MinCommissionRateEpoch + big.NewInt(10), // MinCommissionPromoPeriod big.NewInt(0), // EPoSBound35Epoch big.NewInt(0), // EIP155Epoch big.NewInt(0), // S3Epoch @@ -318,6 +334,12 @@ type ChainConfig struct { // MinDelegation100Epoch is the epoch when min delegation is reduced from 1000 ONE to 100 ONE MinDelegation100Epoch *big.Int `json:"min-delegation-100-epoch,omitempty"` + // MinCommissionRateEpoch is the epoch when policy for minimum comission rate of 5% is started + MinCommissionRateEpoch *big.Int `json:"min-commission-rate-epoch,omitempty"` + + // MinCommissionPromoPeriod is the number of epochs when newly elected validators can have 0% commission + MinCommissionPromoPeriod *big.Int `json:"commission-promo-period,omitempty"` + // EPoSBound35Epoch is the epoch when the EPoS bound parameter c is changed from 15% to 35% EPoSBound35Epoch *big.Int `json:"epos-bound-35-epoch,omitempty"` @@ -417,6 +439,11 @@ func (c *ChainConfig) IsMinDelegation100(epoch *big.Int) bool { return isForked(c.MinDelegation100Epoch, epoch) } +// IsMinCommissionRate determines whether it is the epoch to start the policy of 5% min commission +func (c *ChainConfig) IsMinCommissionRate(epoch *big.Int) bool { + return isForked(c.MinCommissionRateEpoch, epoch) +} + // IsEPoSBound35 determines whether it is the epoch to extend the EPoS bound to 35% func (c *ChainConfig) IsEPoSBound35(epoch *big.Int) bool { return isForked(c.EPoSBound35Epoch, epoch) diff --git a/staking/availability/measure.go b/staking/availability/measure.go index 81ba382f6..bab3a9200 100644 --- a/staking/availability/measure.go +++ b/staking/availability/measure.go @@ -3,6 +3,8 @@ package availability import ( "math/big" + "github.com/harmony-one/harmony/core/state" + "github.com/ethereum/go-ethereum/common" "github.com/harmony-one/harmony/crypto/bls" "github.com/harmony-one/harmony/internal/utils" @@ -15,6 +17,8 @@ import ( var ( measure = numeric.NewDec(2).Quo(numeric.NewDec(3)) + + MinCommissionRate = numeric.MustNewDecFromStr("0.05") // ErrDivByZero .. ErrDivByZero = errors.New("toSign of availability cannot be 0, mistake in protocol") ) @@ -215,3 +219,34 @@ func ComputeAndMutateEPOSStatus( return nil } + +// UpdateMinimumCommissionFee update the validator commission fee to the minimum 5% +// if the validator has a lower commission rate and 100 epochs have passed after +// the validator was first elected. +func UpdateMinimumCommissionFee( + electionEpoch *big.Int, + state *state.DB, + addr common.Address, + promoPeriod int64, +) error { + utils.Logger().Info().Msg("begin update min commission fee") + + wrapper, err := state.ValidatorWrapper(addr) + if err != nil { + return err + } + + firstElectionEpoch := state.GetValidatorFirstElectionEpoch(addr) + + if firstElectionEpoch.Uint64() != 0 && big.NewInt(0).Sub(electionEpoch, firstElectionEpoch).Int64() >= int64(promoPeriod) { + if wrapper.Rate.LT(MinCommissionRate) { + utils.Logger().Info(). + Str("addr", addr.Hex()). + Str("old rate", wrapper.Rate.String()). + Str("firstElectionEpoch", firstElectionEpoch.String()). + Msg("updating min commission rate") + wrapper.Rate.SetBytes(MinCommissionRate.Bytes()) + } + } + return nil +} diff --git a/staking/params.go b/staking/params.go index aef5c64d5..71edd9169 100644 --- a/staking/params.go +++ b/staking/params.go @@ -5,16 +5,18 @@ import ( ) const ( - isValidatorKeyStr = "Harmony/IsValidator/Key/v1" - isValidatorStr = "Harmony/IsValidator/Value/v1" - collectRewardsStr = "Harmony/CollectRewards" - delegateStr = "Harmony/Delegate" + isValidatorKeyStr = "Harmony/IsValidator/Key/v1" + isValidatorStr = "Harmony/IsValidator/Value/v1" + collectRewardsStr = "Harmony/CollectRewards" + delegateStr = "Harmony/Delegate" + firstElectionEpochStr = "Harmony/FirstElectionEpoch/Key/v1" ) // keys used to retrieve staking related informatio var ( - IsValidatorKey = crypto.Keccak256Hash([]byte(isValidatorKeyStr)) - IsValidator = crypto.Keccak256Hash([]byte(isValidatorStr)) - CollectRewardsTopic = crypto.Keccak256Hash([]byte(collectRewardsStr)) - DelegateTopic = crypto.Keccak256Hash([]byte(delegateStr)) + IsValidatorKey = crypto.Keccak256Hash([]byte(isValidatorKeyStr)) + IsValidator = crypto.Keccak256Hash([]byte(isValidatorStr)) + CollectRewardsTopic = crypto.Keccak256Hash([]byte(collectRewardsStr)) + DelegateTopic = crypto.Keccak256Hash([]byte(delegateStr)) + FirstElectionEpochKey = crypto.Keccak256Hash([]byte(firstElectionEpochStr)) ) diff --git a/staking/types/test/prototype.go b/staking/types/test/prototype.go index eff31ceda..2bf3b3a12 100644 --- a/staking/types/test/prototype.go +++ b/staking/types/test/prototype.go @@ -59,9 +59,9 @@ var ( } commissionRates = staking.CommissionRates{ - Rate: numeric.NewDecWithPrec(5, 1), + Rate: numeric.NewDecWithPrec(4, 1), MaxRate: numeric.NewDecWithPrec(9, 1), - MaxChangeRate: numeric.NewDecWithPrec(3, 1), + MaxChangeRate: numeric.NewDecWithPrec(4, 1), } commission = staking.Commission{