diff --git a/core/staking_verifier.go b/core/staking_verifier.go index 541d26f4e6..7de6cdd39f 100644 --- a/core/staking_verifier.go +++ b/core/staking_verifier.go @@ -171,11 +171,20 @@ func VerifyAndEditValidatorFromMsg( return nil, errCommissionRateChangeTooHigh } - if chainContext.Config().IsMinCommissionRate(epoch) && newRate.LT(availability.MinCommissionRate) { + minRate := availability.MinCommissionRate( + chainContext.Config().IsMinCommissionRate(epoch), + chainContext.Config().IsHIP30(epoch), + ) + if newRate.LT(minRate) { 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 + return nil, + errors.Errorf( + "%s %d%%", + errCommissionRateChangeTooLowT, + minRate.MulInt64(100).Int64(), + ) } } diff --git a/core/staking_verifier_test.go b/core/staking_verifier_test.go index 01d24cc8d4..1a33f9848e 100644 --- a/core/staking_verifier_test.go +++ b/core/staking_verifier_test.go @@ -676,7 +676,7 @@ func TestVerifyAndEditValidatorFromMsg(t *testing.T) { return msg }(), - expErr: errCommissionRateChangeTooLow, + expErr: errCommissionRateChangeTooLowT, }, { // 15: Rate is ok within the promo period diff --git a/core/state_transition.go b/core/state_transition.go index 9684812cbb..93e7f2322d 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -40,7 +40,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%") + errCommissionRateChangeTooLowT = errors.New("commission rate can not be lower than min rate of ") errNoRewardsToCollect = errors.New("no rewards to collect") errNegativeAmount = errors.New("amount can not be negative") errDupIdentity = errors.New("validator identity exists") diff --git a/internal/chain/engine.go b/internal/chain/engine.go index 71bb0d3052..4f3aac9ff4 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -6,7 +6,9 @@ import ( "sort" "time" + "github.com/harmony-one/harmony/common/denominations" "github.com/harmony-one/harmony/internal/params" + "github.com/harmony-one/harmony/numeric" bls2 "github.com/harmony-one/bls/ffi/go/bls" blsvrf "github.com/harmony-one/harmony/crypto/vrf/bls" @@ -285,7 +287,7 @@ func (e *engineImpl) Finalize( // depends on the old LastEpochInCommittee startTime = time.Now() - if err := setElectionEpochAndMinFee(header, state, chain.Config()); err != nil { + if err := setElectionEpochAndMinFee(chain, header, state, chain.Config()); err != nil { return nil, nil, err } utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("SetElectionEpochAndMinFee") @@ -311,7 +313,7 @@ func (e *engineImpl) Finalize( // Accumulate block rewards and commit the final state root // Header seems complete, assemble into a block and return - payout, err := AccumulateRewardsAndCountSigs( + remainder, payout, err := AccumulateRewardsAndCountSigs( chain, state, header, beacon, sigsReady, ) if err != nil { @@ -331,6 +333,23 @@ func (e *engineImpl) Finalize( // TODO: make the viewID fetch from caller of the block proposal. header.SetViewID(new(big.Int).SetUint64(viewID())) + // Add the emission recovery split to the balance + if chain.Config().IsHIP30(header.Epoch()) { + // convert to ONE - note that numeric.Dec + // is designed for staking decimals and not + // ONE balances so we use big.Int for this math + remainderOne := new(big.Int).Div( + remainder.Int, big.NewInt(denominations.One), + ) + // this goes directly to the balance (on shard 0, of course) + // because the reward mechanism isn't built to handle + // rewards not obtained from any delegations + state.AddBalance( + shard.Schedule.InstanceForEpoch(header.Epoch()). + HIP30RecoveryAddress(), + remainderOne, + ) + } // Finalize the state root header.SetRoot(state.IntermediateRoot(chain.Config().IsS3(header.Epoch()))) return types.NewBlock(header, txs, receipts, outcxs, incxs, stks), payout, nil @@ -390,12 +409,23 @@ func IsCommitteeSelectionBlock(chain engine.ChainReader, header *block.Header) b return isBeaconChain && header.IsLastBlockInEpoch() && inPreStakingEra } -func setElectionEpochAndMinFee(header *block.Header, state *state.DB, config *params.ChainConfig) error { +func setElectionEpochAndMinFee(chain engine.ChainReader, 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" return errors.New(msg) } + // these 2 should be created outside of loop to optimize + minRate := availability.MinCommissionRate( + config.IsMinCommissionRate(newShardState.Epoch), + config.IsHIP30(newShardState.Epoch), + ) + minRateNotZero := !minRate.Equal(numeric.ZeroDec()) + // elected validators have their fee updated, if required to do so + isElected := make( + map[common.Address]struct{}, + len(newShardState.StakedValidators().Addrs), + ) for _, addr := range newShardState.StakedValidators().Addrs { wrapper, err := state.ValidatorWrapper(addr, true, false) if err != nil { @@ -405,14 +435,32 @@ func setElectionEpochAndMinFee(header *block.Header, state *state.DB, config *pa } // Set last epoch in committee wrapper.LastEpochInCommittee = newShardState.Epoch - - if config.IsMinCommissionRate(newShardState.Epoch) { - // Set first election epoch + if minRateNotZero { + // Set first election epoch (applies only if previously unset) state.SetValidatorFirstElectionEpoch(addr, newShardState.Epoch) - // Update minimum commission fee - if err := availability.UpdateMinimumCommissionFee( - newShardState.Epoch, state, addr, config.MinCommissionPromoPeriod.Int64(), + if _, err := availability.UpdateMinimumCommissionFee( + newShardState.Epoch, state, addr, minRate, + config.MinCommissionPromoPeriod.Uint64(), + ); err != nil { + return err + } + } + isElected[addr] = struct{}{} + } + // due to a bug in the old implementation of the minimum fee, + // unelected validators did not have their fee updated even + // when the protocol required them to do so. here we fix it, + // but only after the HIP-30 hard fork is effective. + if config.IsHIP30(newShardState.Epoch) { + for _, addr := range chain.ValidatorCandidates() { + // skip elected validator + if _, ok := isElected[addr]; ok { + continue + } + if _, err := availability.UpdateMinimumCommissionFee( + newShardState.Epoch, state, addr, minRate, + config.MinCommissionPromoPeriod.Uint64(), ); err != nil { return err } diff --git a/internal/chain/reward.go b/internal/chain/reward.go index f06556412f..7843380933 100644 --- a/internal/chain/reward.go +++ b/internal/chain/reward.go @@ -139,11 +139,11 @@ func lookupDelegatorShares( func accumulateRewardsAndCountSigsBeforeStaking( bc engine.ChainReader, state *state.DB, header *block.Header, sigsReady chan bool, -) (reward.Reader, error) { +) (numeric.Dec, reward.Reader, error) { parentHeader := bc.GetHeaderByHash(header.ParentHash()) if parentHeader == nil { - return network.EmptyPayout, errors.Errorf( + return numeric.ZeroDec(), network.EmptyPayout, errors.Errorf( "cannot find parent block header in DB at parent hash %s", header.ParentHash().Hex(), ) @@ -151,11 +151,11 @@ func accumulateRewardsAndCountSigsBeforeStaking( if parentHeader.Number().Cmp(common.Big0) == 0 { // Parent is an epoch block, // which is not signed in the usual manner therefore rewards nothing. - return network.EmptyPayout, nil + return numeric.ZeroDec(), network.EmptyPayout, nil } parentShardState, err := bc.ReadShardState(parentHeader.Epoch()) if err != nil { - return nil, errors.Wrapf( + return numeric.ZeroDec(), nil, errors.Wrapf( err, "cannot read shard state at epoch %v", parentHeader.Epoch(), ) } @@ -163,7 +163,7 @@ func accumulateRewardsAndCountSigsBeforeStaking( // Block here until the commit sigs are ready or timeout. // sigsReady signal indicates that the commit sigs are already populated in the header object. if err := waitForCommitSigs(sigsReady); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } _, signers, _, err := availability.BallotResult( @@ -171,7 +171,7 @@ func accumulateRewardsAndCountSigsBeforeStaking( ) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } totalAmount := big.NewInt(0) @@ -194,12 +194,12 @@ func accumulateRewardsAndCountSigsBeforeStaking( Int64("block-reward", stakingReward.PreStakedBlocks.Int64()). Int64("total-amount-paid-out", totalAmount.Int64()). Msg("Total paid out was not equal to block-reward") - return nil, errors.Wrapf( + return numeric.ZeroDec(), nil, errors.Wrapf( network.ErrPayoutNotEqualBlockReward, "payout "+totalAmount.String(), ) } - return network.NewPreStakingEraRewarded(totalAmount), nil + return numeric.ZeroDec(), network.NewPreStakingEraRewarded(totalAmount), nil } // getDefaultStakingReward returns the static default reward based on the the block production interval and the chain. @@ -240,14 +240,14 @@ func getDefaultStakingReward(bc engine.ChainReader, epoch *big.Int, blockNum uin func AccumulateRewardsAndCountSigs( bc engine.ChainReader, state *state.DB, header *block.Header, beaconChain engine.ChainReader, sigsReady chan bool, -) (reward.Reader, error) { +) (numeric.Dec, reward.Reader, error) { blockNum := header.Number().Uint64() epoch := header.Epoch() isBeaconChain := bc.CurrentHeader().ShardID() == shard.BeaconChainShardID if blockNum == 0 { err := waitForCommitSigs(sigsReady) // wait for commit signatures, or timeout and return err. - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } // Pre-staking era @@ -258,12 +258,12 @@ func AccumulateRewardsAndCountSigs( // Rewards are accumulated only in the beaconchain, so just wait for commit sigs and return. if !isBeaconChain { err := waitForCommitSigs(sigsReady) - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } defaultReward := getDefaultStakingReward(bc, epoch, blockNum) if defaultReward.IsNegative() { // TODO: Figure out whether that's possible. - return network.EmptyPayout, nil + return numeric.ZeroDec(), network.EmptyPayout, nil } // Handle rewards on pre-aggregated rewards era. @@ -275,12 +275,12 @@ func AccumulateRewardsAndCountSigs( // Wait for commit signatures, or timeout and return err. if err := waitForCommitSigs(sigsReady); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } // Only do reward distribution at the 63th block in the modulus. if blockNum%RewardFrequency != RewardFrequency-1 { - return network.EmptyPayout, nil + return numeric.ZeroDec(), network.EmptyPayout, nil } return distributeRewardAfterAggregateEpoch(bc, state, header, beaconChain, defaultReward) @@ -300,7 +300,16 @@ func waitForCommitSigs(sigsReady chan bool) error { } func distributeRewardAfterAggregateEpoch(bc engine.ChainReader, state *state.DB, header *block.Header, beaconChain engine.ChainReader, - defaultReward numeric.Dec) (reward.Reader, error) { + rewardToDistribute numeric.Dec) (numeric.Dec, reward.Reader, error) { + epoch := header.Epoch() + defaultReward := rewardToDistribute + remainingReward := numeric.ZeroDec() + if bc.Config().IsHIP30(epoch) { + fractionToRecovery := shard.Schedule.InstanceForEpoch(epoch).HIP30EmissionFraction() + fractionToValidators := numeric.OneDec().Sub(fractionToRecovery) + defaultReward = rewardToDistribute.Mul(fractionToValidators) + remainingReward = rewardToDistribute.Mul(fractionToRecovery) + } newRewards, payouts := big.NewInt(0), []reward.Payout{} @@ -331,7 +340,7 @@ func distributeRewardAfterAggregateEpoch(bc engine.ChainReader, state *state.DB, if cxLinks := curHeader.CrossLinks(); len(cxLinks) > 0 { crossLinks := types.CrossLinks{} if err := rlp.DecodeBytes(cxLinks, &crossLinks); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } allCrossLinks = append(allCrossLinks, crossLinks...) } @@ -346,7 +355,7 @@ func distributeRewardAfterAggregateEpoch(bc engine.ChainReader, state *state.DB, payables, _, err := processOneCrossLink(bc, state, cxLink, defaultReward, i) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } allPayables = append(allPayables, payables...) @@ -385,29 +394,29 @@ func distributeRewardAfterAggregateEpoch(bc engine.ChainReader, state *state.DB, for _, addr := range allAddresses { snapshot, err := bc.ReadValidatorSnapshot(addr) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } due := allValidatorPayable[addr] newRewards.Add(newRewards, due) shares, err := lookupDelegatorShares(snapshot) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } if err := state.AddReward(snapshot.Validator, due, shares); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } } utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTimeLocal).Milliseconds()).Msg("After Chain Reward (AddReward)") utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("After Chain Reward") - return network.NewStakingEraRewardForRound( + return remainingReward, network.NewStakingEraRewardForRound( newRewards, payouts, ), nil } func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB, header *block.Header, beaconChain engine.ChainReader, - defaultReward numeric.Dec, sigsReady chan bool) (reward.Reader, error) { + defaultReward numeric.Dec, sigsReady chan bool) (numeric.Dec, reward.Reader, error) { newRewards, payouts := big.NewInt(0), []reward.Payout{} @@ -417,7 +426,7 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB startTime := time.Now() crossLinks := types.CrossLinks{} if err := rlp.DecodeBytes(cxLinks, &crossLinks); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("Decode Cross Links") @@ -427,7 +436,7 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB payables, _, err := processOneCrossLink(bc, state, cxLink, defaultReward, i) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } allPayables = append(allPayables, payables...) @@ -461,17 +470,17 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB payable.EcdsaAddress, ) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } due := resultsHandle[bucket][payThem].payout newRewards.Add(newRewards, due) shares, err := lookupDelegatorShares(snapshot) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } if err := state.AddReward(snapshot.Validator, due, shares); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } payouts = append(payouts, reward.Payout{ Addr: payable.EcdsaAddress, @@ -487,14 +496,14 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB // Block here until the commit sigs are ready or timeout. // sigsReady signal indicates that the commit sigs are already populated in the header object. if err := waitForCommitSigs(sigsReady); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } startTime := time.Now() // Take care of my own beacon chain committee, _ is missing, for slashing parentE, members, payable, missing, err := ballotResultBeaconchain(beaconChain, header) if err != nil { - return network.EmptyPayout, errors.Wrapf(err, "shard 0 block %d reward error with bitmap %x", header.Number(), header.LastCommitBitmap()) + return numeric.ZeroDec(), network.EmptyPayout, errors.Wrapf(err, "shard 0 block %d reward error with bitmap %x", header.Number(), header.LastCommitBitmap()) } subComm := shard.Committee{ShardID: shard.BeaconChainShardID, Slots: members} @@ -505,13 +514,13 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB payable, missing, ); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } votingPower, err := lookupVotingPower( parentE, &subComm, ) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } allSignersShare := numeric.ZeroDec() @@ -528,7 +537,7 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB if !voter.IsHarmonyNode { snapshot, err := bc.ReadValidatorSnapshot(voter.EarningAccount) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } due := defaultReward.Mul( voter.OverallPercent.Quo(allSignersShare), @@ -537,10 +546,10 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB shares, err := lookupDelegatorShares(snapshot) if err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } if err := state.AddReward(snapshot.Validator, due, shares); err != nil { - return network.EmptyPayout, err + return numeric.ZeroDec(), network.EmptyPayout, err } payouts = append(payouts, reward.Payout{ Addr: voter.EarningAccount, @@ -551,7 +560,7 @@ func distributeRewardBeforeAggregateEpoch(bc engine.ChainReader, state *state.DB } utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("Beacon Chain Reward") - return network.NewStakingEraRewardForRound( + return numeric.ZeroDec(), network.NewStakingEraRewardForRound( newRewards, payouts, ), nil } diff --git a/internal/configs/sharding/instance.go b/internal/configs/sharding/instance.go index 1bcd79a34f..0930c20b05 100644 --- a/internal/configs/sharding/instance.go +++ b/internal/configs/sharding/instance.go @@ -108,6 +108,20 @@ func NewInstance( "emission split must be within [0, 1]", ) } + if !emissionFractionToRecovery.Equal(numeric.ZeroDec()) { + if recoveryAddress == (ethCommon.Address{}) { + return nil, errors.Errorf( + "have non-zero emission split but no target address", + ) + } + } + if recoveryAddress != (ethCommon.Address{}) { + if emissionFractionToRecovery.Equal(numeric.ZeroDec()) { + return nil, errors.Errorf( + "have target address but no emission split", + ) + } + } return instance{ numShards: numShards, diff --git a/internal/configs/sharding/localnet.go b/internal/configs/sharding/localnet.go index ebb494196b..fb6050b1de 100644 --- a/internal/configs/sharding/localnet.go +++ b/internal/configs/sharding/localnet.go @@ -22,6 +22,9 @@ var feeCollectorsLocalnet = FeeCollectors{ mustAddress("0x1563915e194D8CfBA1943570603F7606A3115508"): numeric.MustNewDecFromStr("0.5"), } +// pk: 0x3333333333333333333333333333333333333333333333333333333333333333 +var hip30CollectionAddressLocalnet = mustAddress("0x5CbDd86a2FA8Dc4bDdd8a8f69dBa48572EeC07FB") + type localnetSchedule struct{} const ( @@ -36,6 +39,8 @@ const ( func (ls localnetSchedule) InstanceForEpoch(epoch *big.Int) Instance { switch { + case params.LocalnetChainConfig.IsHIP30(epoch): + return localnetV4 case params.LocalnetChainConfig.IsFeeCollectEpoch(epoch): return localnetV3_2 case params.LocalnetChainConfig.IsSixtyPercent(epoch): @@ -203,4 +208,11 @@ var ( numeric.ZeroDec(), ethCommon.Address{}, localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpoch(), ) + localnetV4 = MustNewInstance( + 2, 9, 6, 0, numeric.MustNewDecFromStr("0.68"), + genesis.LocalHarmonyAccountsV2, genesis.LocalFnAccountsV2, + emptyAllowlist, feeCollectorsLocalnet, + numeric.MustNewDecFromStr("0.25"), hip30CollectionAddressLocalnet, + localnetReshardingEpoch, LocalnetSchedule.BlocksPerEpoch(), + ) ) diff --git a/internal/configs/sharding/mainnet.go b/internal/configs/sharding/mainnet.go index e395912e05..2106b58d8c 100644 --- a/internal/configs/sharding/mainnet.go +++ b/internal/configs/sharding/mainnet.go @@ -54,8 +54,8 @@ var ( mustAddress("0xbdFeE8587d347Cd8df002E6154763325265Fa84c"): numeric.MustNewDecFromStr("0.5"), } - hip30CollectionAddress = ethCommon.Address{} - // hip30CollectionAddress = mustAddress("0xMustAddress") + // Emission DAO + hip30CollectionAddress = mustAddress("0xD8194284df879f465ed61DBA6fa8300940cacEA3") ) func mustAddress(addrStr string) ethCommon.Address { diff --git a/internal/configs/sharding/partner.go b/internal/configs/sharding/partner.go index 7514656d32..2730726e0a 100644 --- a/internal/configs/sharding/partner.go +++ b/internal/configs/sharding/partner.go @@ -42,6 +42,8 @@ const ( func (ps partnerSchedule) InstanceForEpoch(epoch *big.Int) Instance { switch { + case params.PartnerChainConfig.IsHIP30(epoch): + return partnerV4 case params.PartnerChainConfig.IsFeeCollectEpoch(epoch): return partnerV3 case epoch.Cmp(feeCollectEpochV1) >= 0: @@ -121,3 +123,11 @@ var partnerV3 = MustNewInstance( feeCollectorsDevnet[1], numeric.ZeroDec(), ethCommon.Address{}, partnerReshardingEpoch, PartnerSchedule.BlocksPerEpoch(), ) +var partnerV4 = MustNewInstance( + 2, 5, 4, 0, + numeric.MustNewDecFromStr("0.9"), genesis.TNHarmonyAccounts, + genesis.TNFoundationalAccounts, emptyAllowlist, + feeCollectorsDevnet[1], numeric.MustNewDecFromStr("0.25"), + hip30CollectionAddressTestnet, partnerReshardingEpoch, + PartnerSchedule.BlocksPerEpoch(), +) diff --git a/internal/configs/sharding/testnet.go b/internal/configs/sharding/testnet.go index da0143ef93..93daeaf61f 100644 --- a/internal/configs/sharding/testnet.go +++ b/internal/configs/sharding/testnet.go @@ -21,6 +21,8 @@ var feeCollectorsTestnet = FeeCollectors{ mustAddress("0xb41B6B8d9e68fD44caC8342BC2EEf4D59531d7d7"): numeric.MustNewDecFromStr("0.5"), } +var hip30CollectionAddressTestnet = mustAddress("0x58dB8BeCe892F343350D125ff22B242784a8BA38") + type testnetSchedule struct{} const ( @@ -40,6 +42,8 @@ const ( func (ts testnetSchedule) InstanceForEpoch(epoch *big.Int) Instance { switch { + case params.TestnetChainConfig.IsHIP30(epoch): + return testnetV5 case params.TestnetChainConfig.IsFeeCollectEpoch(epoch): return testnetV4 case epoch.Cmp(shardReductionEpoch) >= 0: @@ -157,4 +161,13 @@ var ( feeCollectorsTestnet, numeric.ZeroDec(), ethCommon.Address{}, testnetReshardingEpoch, TestnetSchedule.BlocksPerEpoch(), ) + + testnetV5 = MustNewInstance( + 2, 30, 8, 0.15, + numeric.MustNewDecFromStr("0.90"), genesis.TNHarmonyAccountsV1, + genesis.TNFoundationalAccounts, emptyAllowlist, + feeCollectorsTestnet, numeric.MustNewDecFromStr("0.25"), + hip30CollectionAddressTestnet, testnetReshardingEpoch, + TestnetSchedule.BlocksPerEpoch(), + ) ) diff --git a/staking/availability/measure.go b/staking/availability/measure.go index 680092aa0d..881baa8553 100644 --- a/staking/availability/measure.go +++ b/staking/availability/measure.go @@ -18,11 +18,25 @@ import ( var ( measure = numeric.NewDec(2).Quo(numeric.NewDec(3)) - MinCommissionRate = numeric.MustNewDecFromStr("0.05") + minCommissionRateEra1 = numeric.MustNewDecFromStr("0.05") + minCommissionRateEra2 = numeric.MustNewDecFromStr("0.07") // ErrDivByZero .. ErrDivByZero = errors.New("toSign of availability cannot be 0, mistake in protocol") ) +// Returns the minimum commission rate between the two options. +// The later rate supersedes the earlier rate. +// If neither is applicable, returns 0. +func MinCommissionRate(era1, era2 bool) numeric.Dec { + if era2 { + return minCommissionRateEra2 + } + if era1 { + return minCommissionRateEra1 + } + return numeric.ZeroDec() +} + // BlockSigners .. func BlockSigners( bitmap []byte, parentCommittee *shard.Committee, @@ -217,33 +231,39 @@ 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. +// UpdateMinimumCommissionFee update the validator commission fee to the minRate +// if the validator has a lower commission rate and promoPeriod epochs have passed after +// the validator was first elected. It returns true if the commission was updated func UpdateMinimumCommissionFee( electionEpoch *big.Int, state *state.DB, addr common.Address, - promoPeriod int64, -) error { + minRate numeric.Dec, + promoPeriod uint64, +) (bool, error) { utils.Logger().Info().Msg("begin update min commission fee") wrapper, err := state.ValidatorWrapper(addr, true, false) if err != nil { - return err + return false, err } firstElectionEpoch := state.GetValidatorFirstElectionEpoch(addr) - if firstElectionEpoch.Uint64() != 0 && big.NewInt(0).Sub(electionEpoch, firstElectionEpoch).Int64() >= int64(promoPeriod) { - if wrapper.Rate.LT(MinCommissionRate) { + // convert all to uint64 for easy maths + // this can take decades of time without overflowing + first := firstElectionEpoch.Uint64() + election := electionEpoch.Uint64() + if first != 0 && election-first >= promoPeriod && election >= first { + if wrapper.Rate.LT(minRate) { 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()) + wrapper.Rate.SetBytes(minRate.Bytes()) + return true, nil } } - return nil + return false, nil }