diff --git a/core/tx_pool.go b/core/tx_pool.go index d918490223..2457da3854 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -850,15 +850,11 @@ func (pool *TxPool) validateStakingTx(tx *staking.StakingTransaction) error { } currentBlockNumber := pool.chain.CurrentBlock().Number() pendingBlockNumber := new(big.Int).Add(currentBlockNumber, big.NewInt(1)) - pendingEpoch := pool.chain.CurrentBlock().Epoch() - if shard.Schedule.IsLastBlock(currentBlockNumber.Uint64()) { - pendingEpoch = new(big.Int).Add(pendingEpoch, big.NewInt(1)) - } chainContext, ok := pool.chain.(ChainContext) if !ok { chainContext = nil // might use testing blockchain, set to nil for verifier to handle. } - _, err = VerifyAndCreateValidatorFromMsg(pool.currentState, chainContext, pendingEpoch, pendingBlockNumber, stkMsg) + _, err = VerifyAndCreateValidatorFromMsg(pool.currentState, chainContext, pool.pendingEpoch(), pendingBlockNumber, stkMsg) return err case staking.DirectiveEditValidator: msg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveEditValidator) @@ -932,7 +928,6 @@ func (pool *TxPool) validateStakingTx(tx *staking.StakingTransaction) error { if from != stkMsg.DelegatorAddress { return errors.WithMessagef(ErrInvalidSender, "staking transaction sender is %s", b32) } - _, err = VerifyAndUndelegateFromMsg(pool.currentState, pool.pendingEpoch(), stkMsg) return err case staking.DirectiveCollectRewards: @@ -964,11 +959,12 @@ func (pool *TxPool) validateStakingTx(tx *staking.StakingTransaction) error { } } +// pendingEpoch refers to the epoch of the pending block func (pool *TxPool) pendingEpoch() *big.Int { currentBlock := pool.chain.CurrentBlock() pendingEpoch := currentBlock.Epoch() if shard.Schedule.IsLastBlock(currentBlock.Number().Uint64()) { - pendingEpoch.Add(pendingEpoch, big.NewInt(1)) + pendingEpoch = new(big.Int).Add(pendingEpoch, common.Big1) } return pendingEpoch } diff --git a/hmy/staking.go b/hmy/staking.go index 9d99b7908e..83e800544c 100644 --- a/hmy/staking.go +++ b/hmy/staking.go @@ -143,6 +143,11 @@ func (hmy *Harmony) IsNoEarlyUnlockEpoch(epoch *big.Int) bool { return hmy.BlockChain.Config().IsNoEarlyUnlock(epoch) } +// IsMaxRate ... +func (hmy *Harmony) IsMaxRate(epoch *big.Int) bool { + return hmy.BlockChain.Config().IsMaxRate(epoch) +} + // IsCommitteeSelectionBlock checks if the given block is the committee selection block func (hmy *Harmony) IsCommitteeSelectionBlock(header *block.Header) bool { return chain.IsCommitteeSelectionBlock(hmy.BlockChain, header) @@ -592,6 +597,7 @@ func (hmy *Harmony) GetUndelegationPayouts( return undelegationPayouts, nil } + isMaxRate := hmy.IsMaxRate(epoch) lockingPeriod := hmy.GetDelegationLockingPeriodInEpoch(undelegationPayoutBlock.Epoch()) for _, validator := range hmy.GetAllValidatorAddresses() { wrapper, err := hmy.BlockChain.ReadValidatorInformationAtRoot(validator, undelegationPayoutBlock.Root()) @@ -600,7 +606,7 @@ func (hmy *Harmony) GetUndelegationPayouts( } noEarlyUnlock := hmy.IsNoEarlyUnlockEpoch(epoch) for _, delegation := range wrapper.Delegations { - withdraw := delegation.RemoveUnlockedUndelegations(epoch, wrapper.LastEpochInCommittee, lockingPeriod, noEarlyUnlock) + withdraw := delegation.RemoveUnlockedUndelegations(epoch, wrapper.LastEpochInCommittee, lockingPeriod, noEarlyUnlock, isMaxRate) if withdraw.Cmp(bigZero) == 1 { undelegationPayouts.SetPayoutByDelegatorAddrAndValidatorAddr(validator, delegation.DelegatorAddress, withdraw) } diff --git a/internal/chain/engine.go b/internal/chain/engine.go index c77d86487d..d0e8e02db7 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -370,9 +370,15 @@ func payoutUndelegations( const msg = "[Finalize] failed to read all validators" return errors.New(msg) } - // Payout undelegated/unlocked tokens + // Payout undelegated/unlocked tokens at the end of each epoch lockPeriod := GetLockPeriodInEpoch(chain, header.Epoch()) noEarlyUnlock := chain.Config().IsNoEarlyUnlock(header.Epoch()) + newShardState, err := header.GetShardState() + if err != nil { + const msg = "[Finalize] failed to read shard state" + return errors.New(msg) + } + isMaxRate := chain.Config().IsMaxRate(newShardState.Epoch) for _, validator := range validators { wrapper, err := state.ValidatorWrapper(validator, true, false) if err != nil { @@ -383,7 +389,7 @@ func payoutUndelegations( for i := range wrapper.Delegations { delegation := &wrapper.Delegations[i] totalWithdraw := delegation.RemoveUnlockedUndelegations( - header.Epoch(), wrapper.LastEpochInCommittee, lockPeriod, noEarlyUnlock, + header.Epoch(), wrapper.LastEpochInCommittee, lockPeriod, noEarlyUnlock, isMaxRate, ) if totalWithdraw.Sign() != 0 { state.AddBalance(delegation.DelegatorAddress, totalWithdraw) @@ -426,6 +432,7 @@ func setElectionEpochAndMinFee(chain engine.ChainReader, header *block.Header, s map[common.Address]struct{}, len(newShardState.StakedValidators().Addrs), ) + // this loop is for elected validators only for _, addr := range newShardState.StakedValidators().Addrs { wrapper, err := state.ValidatorWrapper(addr, true, false) if err != nil { @@ -459,8 +466,9 @@ func setElectionEpochAndMinFee(chain engine.ChainReader, header *block.Header, s // 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) { + // but only after the HIP-30 hard fork is effective + // this loop applies to all validators, but excludes the ones in isElected + if config.IsHIP30(newShardState.Epoch) && minRateNotZero { for _, addr := range chain.ValidatorCandidates() { // skip elected validator if _, ok := isElected[addr]; ok { @@ -474,6 +482,19 @@ func setElectionEpochAndMinFee(chain engine.ChainReader, header *block.Header, s } } } + + // for all validators which have MaxRate < minRate + maxChangeRate + // set their MaxRate equal to the minRate + MaxChangeRate + // this will allow the wrapper.SanityCheck to pass if Rate is set to a value + // higher than the the MaxRate by UpdateMinimumCommissionFee above + if config.IsMaxRate(newShardState.Epoch) && minRateNotZero { + for _, addr := range chain.ValidatorCandidates() { + if _, err := availability.UpdateMaxCommissionFee(state, addr, minRate); err != nil { + return err + } + } + } + return nil } diff --git a/internal/params/config.go b/internal/params/config.go index 703dea062b..67e9ff453d 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -633,6 +633,9 @@ func (c *ChainConfig) mustValid() { // capabilities required to transfer balance across shards require(c.HIP30Epoch.Cmp(c.CrossTxEpoch) > 0, "must satisfy: HIP30Epoch > CrossTxEpoch") + // max rate (7%) fix is applied on or after hip30 + require(c.MaxRateEpoch.Cmp(c.HIP30Epoch) >= 0, + "must satisfy: MaxRateEpoch >= HIP30Epoch") } // IsEIP155 returns whether epoch is either equal to the EIP155 fork epoch or greater. diff --git a/rpc/blockchain.go b/rpc/blockchain.go index ae588e7500..c9a6a13138 100644 --- a/rpc/blockchain.go +++ b/rpc/blockchain.go @@ -464,7 +464,11 @@ func (s *PublicBlockchainService) GetBlockReceipts( r, err = v2.NewReceipt(tx, blockHash, block.NumberU64(), index, rmap[tx.Hash()]) case Eth: if tx, ok := tx.(*types.Transaction); ok { - r, err = eth.NewReceipt(tx.ConvertToEth(), blockHash, block.NumberU64(), index, rmap[tx.Hash()]) + from, err := tx.SenderAddress() + if err != nil { + return nil, err + } + r, err = eth.NewReceipt(from, tx.ConvertToEth(), blockHash, block.NumberU64(), index, rmap[tx.Hash()]) } default: return nil, ErrUnknownRPCVersion diff --git a/rpc/eth/types.go b/rpc/eth/types.go index f1ad725eb7..a319a8fc12 100644 --- a/rpc/eth/types.go +++ b/rpc/eth/types.go @@ -74,19 +74,9 @@ type Transaction struct { // representation, with the given location metadata set (if available). // Note that all txs on Harmony are replay protected (post EIP155 epoch). func NewTransaction( - tx *types.EthTransaction, blockHash common.Hash, + from common.Address, tx *types.EthTransaction, blockHash common.Hash, blockNumber uint64, timestamp uint64, index uint64, ) (*Transaction, error) { - from := common.Address{} - var err error - if tx.IsEthCompatible() { - from, err = tx.SenderAddress() - } else { - from, err = tx.ConvertToHmy().SenderAddress() - } - if err != nil { - return nil, err - } v, r, s := tx.RawSignatureValues() result := &Transaction{ @@ -143,14 +133,9 @@ func NewTransactionFromTransaction( } // NewReceipt returns the RPC data for a new receipt -func NewReceipt(tx *types.EthTransaction, blockHash common.Hash, blockNumber, blockIndex uint64, receipt *types.Receipt) (map[string]interface{}, error) { - senderAddr, err := tx.SenderAddress() - if err != nil { - return nil, err - } - +func NewReceipt(senderAddr common.Address, tx *types.EthTransaction, blockHash common.Hash, blockNumber, blockIndex uint64, receipt *types.Receipt) (map[string]interface{}, error) { ethTxHash := tx.Hash() - for i, _ := range receipt.Logs { + for i := range receipt.Logs { // Override log txHash with receipt's receipt.Logs[i].TxHash = ethTxHash } @@ -240,7 +225,11 @@ func blockWithFullTxFromBlock(b *types.Block) (*BlockWithFullTx, error) { } for idx, tx := range b.Transactions() { - fmtTx, err := NewTransaction(tx.ConvertToEth(), b.Hash(), b.NumberU64(), b.Time().Uint64(), uint64(idx)) + from, err := tx.SenderAddress() + if err != nil { + return nil, err + } + fmtTx, err := NewTransaction(from, tx.ConvertToEth(), b.Hash(), b.NumberU64(), b.Time().Uint64(), uint64(idx)) if err != nil { return nil, err } @@ -257,5 +246,10 @@ func NewTransactionFromBlockIndex(b *types.Block, index uint64) (*Transaction, e "tx index %v greater than or equal to number of transactions on block %v", index, b.Hash().String(), ) } - return NewTransaction(txs[index].ConvertToEth(), b.Hash(), b.NumberU64(), b.Time().Uint64(), index) + tx := txs[index].ConvertToEth() + from, err := tx.SenderAddress() + if err != nil { + return nil, err + } + return NewTransaction(from, tx, b.Hash(), b.NumberU64(), b.Time().Uint64(), index) } diff --git a/rpc/pool.go b/rpc/pool.go index c0d4858c2e..ee4e34828f 100644 --- a/rpc/pool.go +++ b/rpc/pool.go @@ -253,7 +253,14 @@ func (s *PublicPoolService) PendingTransactions( continue // Legacy behavior is to not return error here } case Eth: - tx, err = eth.NewTransaction(plainTx.ConvertToEth(), common.Hash{}, 0, 0, 0) + from, err := plainTx.SenderAddress() + if err != nil { + utils.Logger().Debug(). + Err(err). + Msgf("%v error at %v", LogTag, "PendingTransactions") + continue // Legacy behavior is to not return error here + } + tx, err = eth.NewTransaction(from, plainTx.ConvertToEth(), common.Hash{}, 0, 0, 0) if err != nil { utils.Logger().Debug(). Err(err). diff --git a/rpc/transaction.go b/rpc/transaction.go index 4106425c27..4b45045851 100644 --- a/rpc/transaction.go +++ b/rpc/transaction.go @@ -236,7 +236,13 @@ func (s *PublicTransactionService) newRPCTransaction(tx *types.Transaction, bloc } return NewStructuredResponse(tx) case Eth: - tx, err := eth.NewTransactionFromTransaction(tx, blockHash, blockNumber, timestamp, index) + // calculate SenderAddress before ConvertToEth + senderAddr, err := tx.SenderAddress() + if err != nil { + DoMetricRPCQueryInfo(GetTransactionByHash, FailedNumber) + return nil, err + } + tx, err := eth.NewTransaction(senderAddr, tx.ConvertToEth(), blockHash, blockNumber, timestamp, index) if err != nil { DoMetricRPCQueryInfo(GetTransactionByHash, FailedNumber) return nil, err @@ -751,7 +757,7 @@ func (s *PublicTransactionService) GetTransactionReceipt( return nil, err } return NewStructuredResponse(RPCReceipt) - case V2, Eth: + case V2: if tx == nil { RPCReceipt, err = v2.NewReceipt(stx, blockHash, blockNumber, index, receipt) } else { @@ -761,6 +767,19 @@ func (s *PublicTransactionService) GetTransactionReceipt( return nil, err } return NewStructuredResponse(RPCReceipt) + case Eth: + if tx != nil { + // calculate SenderAddress before ConvertToEth + senderAddr, err := tx.SenderAddress() + if err != nil { + return nil, err + } + RPCReceipt, err = eth.NewReceipt(senderAddr, tx.ConvertToEth(), blockHash, blockNumber, index, receipt) + } + if err != nil { + return nil, err + } + return NewStructuredResponse(RPCReceipt) default: return nil, ErrUnknownRPCVersion } diff --git a/staking/types/delegation.go b/staking/types/delegation.go index 9f0a0c622e..c222048e44 100644 --- a/staking/types/delegation.go +++ b/staking/types/delegation.go @@ -178,15 +178,21 @@ func (d *Delegation) DeleteEntry(epoch *big.Int) { // RemoveUnlockedUndelegations removes all fully unlocked // undelegations and returns the total sum func (d *Delegation) RemoveUnlockedUndelegations( - curEpoch, lastEpochInCommittee *big.Int, lockPeriod int, noEarlyUnlock bool, + curEpoch, lastEpochInCommittee *big.Int, lockPeriod int, noEarlyUnlock bool, isMaxRate bool, ) *big.Int { totalWithdraw := big.NewInt(0) count := 0 for j := range d.Undelegations { - if big.NewInt(0).Sub(curEpoch, d.Undelegations[j].Epoch).Int64() >= int64(lockPeriod) || - (!noEarlyUnlock && big.NewInt(0).Sub(curEpoch, lastEpochInCommittee).Int64() >= int64(lockPeriod)) { - // need to wait at least 7 epochs to withdraw; or the validator has been out of committee for 7 epochs - totalWithdraw.Add(totalWithdraw, d.Undelegations[j].Amount) + epochsSinceUndelegation := big.NewInt(0).Sub(curEpoch, d.Undelegations[j].Epoch).Int64() + // >=7 epochs have passed since undelegation, or + lockPeriodApplies := epochsSinceUndelegation >= int64(lockPeriod) + // >=7 epochs have passed since unelection during the noEarlyUnlock configuration + earlyUnlockPeriodApplies := big.NewInt(0).Sub(curEpoch, lastEpochInCommittee).Int64() >= int64(lockPeriod) && !noEarlyUnlock + maxRateApplies := isMaxRate && epochsSinceUndelegation > int64(lockPeriod) + if lockPeriodApplies || earlyUnlockPeriodApplies { + if !maxRateApplies { + totalWithdraw.Add(totalWithdraw, d.Undelegations[j].Amount) + } count++ } else { break diff --git a/staking/types/delegation_test.go b/staking/types/delegation_test.go index c40750e4c6..ff0c25c02b 100644 --- a/staking/types/delegation_test.go +++ b/staking/types/delegation_test.go @@ -75,7 +75,7 @@ func TestUnlockedLastEpochInCommittee(t *testing.T) { amount4 := big.NewInt(4000) delegation.Undelegate(epoch4, amount4) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false, false) if result.Cmp(big.NewInt(8000)) != 0 { t.Errorf("removing an unlocked undelegation fails") } @@ -90,7 +90,7 @@ func TestUnlockedLastEpochInCommitteeFail(t *testing.T) { amount4 := big.NewInt(4000) delegation.Undelegate(epoch4, amount4) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false, false) if result.Cmp(big.NewInt(0)) != 0 { t.Errorf("premature delegation shouldn't be unlocked") } @@ -104,7 +104,7 @@ func TestUnlockedFullPeriod(t *testing.T) { amount5 := big.NewInt(4000) delegation.Undelegate(epoch5, amount5) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false, false) if result.Cmp(big.NewInt(4000)) != 0 { t.Errorf("removing an unlocked undelegation fails") } @@ -118,7 +118,7 @@ func TestQuickUnlock(t *testing.T) { amount7 := big.NewInt(4000) delegation.Undelegate(epoch7, amount7) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 0, false) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 0, false, false) if result.Cmp(big.NewInt(4000)) != 0 { t.Errorf("removing an unlocked undelegation fails") } @@ -133,7 +133,7 @@ func TestUnlockedFullPeriodFail(t *testing.T) { amount5 := big.NewInt(4000) delegation.Undelegate(epoch5, amount5) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false, false) if result.Cmp(big.NewInt(0)) != 0 { t.Errorf("premature delegation shouldn't be unlocked") } @@ -147,7 +147,7 @@ func TestUnlockedPremature(t *testing.T) { amount6 := big.NewInt(4000) delegation.Undelegate(epoch6, amount6) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, false, false) if result.Cmp(big.NewInt(0)) != 0 { t.Errorf("premature delegation shouldn't be unlocked") } @@ -161,8 +161,128 @@ func TestNoEarlyUnlock(t *testing.T) { amount4 := big.NewInt(4000) delegation.Undelegate(epoch4, amount4) - result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true) + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true, false) if result.Cmp(big.NewInt(0)) != 0 { t.Errorf("should not allow early unlock") } } + +func TestMaxRateAtLess(t *testing.T) { + // recreate it so that all tests can run + delegation := NewDelegation(delegatorAddr, delegationAmt) + lastEpochInCommittee := big.NewInt(1) + curEpoch := big.NewInt(27) + epoch := big.NewInt(21) + amount := big.NewInt(4000) + delegation.Undelegate(epoch, amount) + initialLength := len(delegation.Undelegations) + + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true, true) + if result.Cmp(big.NewInt(0)) != 0 { + t.Errorf("should not allow unlock before 7") + } + finalLength := len(delegation.Undelegations) + if initialLength != finalLength { + t.Errorf("should not remove undelegations before 7") + } +} + +func TestMaxRateAtEqual(t *testing.T) { + // recreate it so that all tests can run + delegation := NewDelegation(delegatorAddr, delegationAmt) + lastEpochInCommittee := big.NewInt(1) + curEpoch := big.NewInt(28) + epoch := big.NewInt(21) + amount := big.NewInt(4000) + delegation.Undelegate(epoch, amount) + initialLength := len(delegation.Undelegations) + + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true, true) + if result.Cmp(big.NewInt(4000)) != 0 { + t.Errorf("should withdraw at 7") + } + finalLength := len(delegation.Undelegations) + if initialLength == finalLength { + t.Errorf("should remove undelegations at 7") + } +} + +func TestMaxRateAtExcess(t *testing.T) { + // recreate it so that all tests can run + delegation := NewDelegation(delegatorAddr, delegationAmt) + lastEpochInCommittee := big.NewInt(1) + curEpoch := big.NewInt(29) + epoch := big.NewInt(21) + amount := big.NewInt(4000) + delegation.Undelegate(epoch, amount) + initialLength := len(delegation.Undelegations) + + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true, true) + if result.Cmp(big.NewInt(0)) != 0 { + t.Errorf("should not withdraw at 8") + } + finalLength := len(delegation.Undelegations) + if initialLength == finalLength { + t.Errorf("should remove undelegations at 8") + } +} + +func TestNoMaxRateAtLess(t *testing.T) { + // recreate it so that all tests can run + delegation := NewDelegation(delegatorAddr, delegationAmt) + lastEpochInCommittee := big.NewInt(1) + curEpoch := big.NewInt(27) + epoch := big.NewInt(21) + amount := big.NewInt(4000) + delegation.Undelegate(epoch, amount) + initialLength := len(delegation.Undelegations) + + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true, false) + if result.Cmp(big.NewInt(0)) != 0 { + t.Errorf("should not allow unlock before 7") + } + finalLength := len(delegation.Undelegations) + if initialLength != finalLength { + t.Errorf("should not remove undelegations before 7") + } +} + +func TestNoMaxRateAtEqual(t *testing.T) { + // recreate it so that all tests can run + delegation := NewDelegation(delegatorAddr, delegationAmt) + lastEpochInCommittee := big.NewInt(1) + curEpoch := big.NewInt(28) + epoch := big.NewInt(21) + amount := big.NewInt(4000) + delegation.Undelegate(epoch, amount) + initialLength := len(delegation.Undelegations) + + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true, false) + if result.Cmp(big.NewInt(4000)) != 0 { + t.Errorf("should withdraw at 7") + } + finalLength := len(delegation.Undelegations) + if initialLength == finalLength { + t.Errorf("should remove undelegations at 7") + } +} + +func TestNoMaxRateAtExcess(t *testing.T) { + // recreate it so that all tests can run + delegation := NewDelegation(delegatorAddr, delegationAmt) + lastEpochInCommittee := big.NewInt(1) + curEpoch := big.NewInt(29) + epoch := big.NewInt(21) + amount := big.NewInt(4000) + delegation.Undelegate(epoch, amount) + initialLength := len(delegation.Undelegations) + + result := delegation.RemoveUnlockedUndelegations(curEpoch, lastEpochInCommittee, 7, true, false) + if result.Cmp(big.NewInt(4000)) != 0 { + t.Errorf("should withdraw at 8") + } + finalLength := len(delegation.Undelegations) + if initialLength == finalLength { + t.Errorf("should remove undelegations at 8") + } +}