Resolve harmony-one/bounties#77: Staking precompiles (#3906)

* Resolve harmony-one/bounties#77: Staking precompiles

Create write capable precompiles that can perform staking transactions
Add hard fork logic (EpochTBD) for these precompiles
Tests for new code with at least 80% unit test coverage
Staking library + tests in MaxMustermann2/harmony-staking-precompiles

* Fix small typo in comment

* Run goimports on files to fix Travis

* Do not activate staking precompile on shard 0

* Cascade readOnly to WriteCapableContract

* No overlap in readOnly + writeCapable precompiles

* Use function selector instead of directive

From Solidity, use abi.encodeWithSelector and match it against the
exact ABI of the functions. This allows us to remove the need for
a directive (32) being encoded, and thus saves 28 bytes of data.

* Do not allow contracts to become validators

As discussed with Jacky on #3906

* Merge harmony-one/harmony/main properly this time

* Run goimports

* Update gas calculation for staking precompile

Please see comment in core/vm/contracts_write.go RequiredGas

* Do not allow contract to become validator (2/2)

* Cache StakeMsgs from precompiled transactions

Add the StakeMsgs to ProcessorResult and cascade them in insertChain

* Remove ContractCode fields from validators

Since smart contracts can no longer beecome validators,
this field is superfluous. Remove it from the Wrapper
structure, and do not assign it a value when creating
a validator. Build and goimports checked

* Update comments in response to feedback

(1) Comments to start with function names
(2) Comments for public variables
(3) Comment to match function name RunPrecompiledContract
(4) Clarify that CreateValidatorFunc + EditValidatorFunc are still used

* Fix Travis build by reverting rosetta change

* Add revert capability to 3 staking tx types

 - Delegate
 - Undelegate
 - CollectRewards

* Fix build: Update evm_test for ValidatorWrapper

* Merge main into harmony-staking-precompiles

* Add gas for precompile calls and allow EOA usage

 - Each time the precompile is called, charge the base gas fee plus data
   cost (if data can be parsed successfully). A gas fee is added to
   prevent benevolent contract deployers from subsidizing the staking
   transactions for EOAs through repeated assembly `delegatecall`.
 - Allow EOAs to use the staking precompile directly. Some changes to
   the Solidity library are associated with this change.
 - Remove bytes from parsing address, since the ABI unpacks it into an
   address format correctly.
 - Add or update tests. Test coverage report to be attached to the PR
   shortly.

* Run goimports

* Check read only and write capable for overlap

* Handle precompile stakeMsgs for block proposer

The staking precompile generates staking messages which are cascaded to
the block via the EVM in `state_processor.go`. This change cascades them
in `worker.go` to allow block proposers and block verifiers to keep the
same state.

* Run goimports for cf2dfac4081444e36a120c9432f4e..

* Update staking precompile epoch to 2 for localnet

Bring it in line with staking epoch. Change effects all configurations
except mainnet and testnet. `goimports` included.

* Add read only precompile to fetch the epoch num

* Move epoch precompile to 250

* precompiles: left pad the returned epoch number

* chainConfig: check epochs for precompiles

panic if staking precompile epoch < pre staking epoch

* Add staking migration precompile

 - Lives at address 251
 - Migrates delegations + pending undelegations from address A to B
 - Useful if address A is hacked
 - Charges gas of 21k + cost of bytes for two addresses
 - Does not remove existing delegations, just sets them to zero.
   Replicates current undelegate setup
 - Unit tests and `goimports` included. Integration test following
   shortly in MaxMustermann2/harmony-staking-precompiles

* Migration precompile: merge into staking

Merge the two precompiles into one, add gas calculation for migration
precompile. Move epoch precompile to 251 as a result. When migrating,
add undelegations to `To`'s existing undelegations, if any match the
epoch.

* Add migration gas test, remove panic, add check

In response to review comments, add tests for migration gas wherein
there are 0/1/2 delegations to migrate. Add the index out of bound check
to migration gas calculator and remove panics. Lastly, re-sort
migrated undelegations if no existing undelegation in the same epoch was
found on `To`.

* Move undelegations sorting to end of loop
pull/4024/head
Max 3 years ago committed by GitHub
parent 55f8c769a0
commit d500c4ede6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 70
      core/blockchain.go
  2. 85
      core/blockchain_test.go
  3. 2
      core/chain_makers.go
  4. 256
      core/evm.go
  5. 478
      core/evm_test.go
  6. 3
      core/offchain.go
  7. 117
      core/staking_verifier.go
  8. 9
      core/staking_verifier_test.go
  9. 41
      core/state_processor.go
  10. 205
      core/state_transition.go
  11. 83
      core/state_transition_test.go
  12. 5
      core/tx_pool.go
  13. 3
      core/types.go
  14. 56
      core/vm/contracts.go
  15. 145
      core/vm/contracts_write.go
  16. 213
      core/vm/contracts_write_test.go
  17. 46
      core/vm/evm.go
  18. 35
      core/vm/evm_test.go
  19. 42
      core/vm/gas.go
  20. 4
      hmy/tracer.go
  21. 28
      internal/params/config.go
  22. 5
      node/worker/worker.go
  23. 4
      rosetta/services/construction_check.go
  24. 233
      staking/precompile.go
  25. 203
      staking/precompile_test.go
  26. 51
      staking/types/messages.go
  27. 4
      staking/types/test/equal.go

@ -268,7 +268,7 @@ func (bc *BlockChain) ValidateNewBlock(block *types.Block) error {
// NOTE Order of mutating state here matters.
// Process block using the parent state as reference point.
// Do not read cache from processor.
receipts, cxReceipts, _, usedGas, _, _, err := bc.processor.Process(
receipts, cxReceipts, _, _, usedGas, _, _, err := bc.processor.Process(
block, state, bc.vmConfig, false,
)
if err != nil {
@ -1148,6 +1148,7 @@ func (bc *BlockChain) WriteBlockWithoutState(block *types.Block, td *big.Int) (e
func (bc *BlockChain) WriteBlockWithState(
block *types.Block, receipts []*types.Receipt,
cxReceipts []*types.CXReceipt,
stakeMsgs []staking.StakeMsg,
paid reward.Reader,
state *state.DB,
) (status WriteStatus, err error) {
@ -1239,7 +1240,8 @@ func (bc *BlockChain) WriteBlockWithState(
// Write offchain data
if status, err := bc.CommitOffChainData(
batch, block, receipts,
cxReceipts, paid, state,
cxReceipts, stakeMsgs,
paid, state,
); err != nil {
return status, err
}
@ -1465,7 +1467,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifyHeaders bool) (int,
}
// Process block using the parent state as reference point.
receipts, cxReceipts, logs, usedGas, payout, newState, err := bc.processor.Process(
receipts, cxReceipts, stakeMsgs, logs, usedGas, payout, newState, err := bc.processor.Process(
block, state, bc.vmConfig, true,
)
state = newState // update state in case the new state is cached.
@ -1485,7 +1487,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifyHeaders bool) (int,
// Write the block to the chain and get the status.
status, err := bc.WriteBlockWithState(
block, receipts, cxReceipts, payout, state,
block, receipts, cxReceipts, stakeMsgs, payout, state,
)
if err != nil {
return i, events, coalescedLogs, err
@ -2692,9 +2694,10 @@ func (bc *BlockChain) writeDelegationsByDelegator(
// Note: this should only be called within the blockchain insert process.
func (bc *BlockChain) UpdateStakingMetaData(
batch rawdb.DatabaseWriter, block *types.Block,
stakeMsgs []staking.StakeMsg,
state *state.DB, epoch, newEpoch *big.Int,
) (newValidators []common.Address, err error) {
newValidators, newDelegations, err := bc.prepareStakingMetaData(block, state)
newValidators, newDelegations, err := bc.prepareStakingMetaData(block, stakeMsgs, state)
if err != nil {
utils.Logger().Warn().Msgf("oops, prepareStakingMetaData failed, err: %+v", err)
return newValidators, err
@ -2758,13 +2761,27 @@ func (bc *BlockChain) UpdateStakingMetaData(
// newValidators - the addresses of the newly created validators
// newDelegations - the map of delegator address and their updated delegation indexes
func (bc *BlockChain) prepareStakingMetaData(
block *types.Block, state *state.DB,
) (newValidators []common.Address,
newDelegations map[common.Address]staking.DelegationIndexes,
err error,
block *types.Block, stakeMsgs []staking.StakeMsg, state *state.DB,
) ([]common.Address,
map[common.Address]staking.DelegationIndexes,
error,
) {
newDelegations = map[common.Address]staking.DelegationIndexes{}
var newValidators []common.Address
newDelegations := map[common.Address]staking.DelegationIndexes{}
blockNum := block.Number()
for _, stakeMsg := range stakeMsgs {
if delegate, ok := stakeMsg.(*staking.Delegate); ok {
if err := processDelegateMetadata(delegate,
newDelegations,
state,
bc,
blockNum); err != nil {
return nil, nil, err
}
} else {
panic("Only *staking.Delegate stakeMsgs are supported at the moment")
}
}
for _, txn := range block.StakingTransactions() {
payload, err := txn.RLPEncodeStakeMsg()
if err != nil {
@ -2800,34 +2817,47 @@ func (bc *BlockChain) prepareStakingMetaData(
return nil, nil, err
}
}
delegations = append(delegations, selfIndex)
newDelegations[createValidator.ValidatorAddress] = delegations
case staking.DirectiveEditValidator:
case staking.DirectiveDelegate:
delegate := decodePayload.(*staking.Delegate)
if err := processDelegateMetadata(delegate,
newDelegations,
state,
bc,
blockNum); err != nil {
return nil, nil, err
}
case staking.DirectiveUndelegate:
case staking.DirectiveCollectRewards:
default:
}
}
return newValidators, newDelegations, nil
}
func processDelegateMetadata(delegate *staking.Delegate,
newDelegations map[common.Address]staking.DelegationIndexes,
state *state.DB, bc *BlockChain, blockNum *big.Int,
) (err error) {
delegations, ok := newDelegations[delegate.DelegatorAddress]
if !ok {
// If the cache doesn't have it, load it from DB for the first time.
delegations, err = bc.ReadDelegationsByDelegator(delegate.DelegatorAddress)
if err != nil {
return nil, nil, err
return err
}
}
if delegations, err = bc.addDelegationIndex(
delegations, delegate.DelegatorAddress, delegate.ValidatorAddress, state, blockNum,
); err != nil {
return nil, nil, err
return err
}
newDelegations[delegate.DelegatorAddress] = delegations
case staking.DirectiveUndelegate:
case staking.DirectiveCollectRewards:
default:
}
}
return newValidators, newDelegations, nil
return nil
}
// ReadBlockRewardAccumulator must only be called on beaconchain

@ -0,0 +1,85 @@
package core
import (
"crypto/ecdsa"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/harmony-one/harmony/core/types"
staking "github.com/harmony-one/harmony/staking/types"
)
func TestPrepareStakingMetadata(t *testing.T) {
key, _ := crypto.GenerateKey()
chain, db, header, _ := getTestEnvironment(*key)
// fake transaction
tx := types.NewTransaction(1, common.BytesToAddress([]byte{0x11}), 0, big.NewInt(111), 1111, big.NewInt(11111), []byte{0x11, 0x11, 0x11})
txs := []*types.Transaction{tx}
// fake staking transactions
stx1 := signedCreateValidatorStakingTxn(key)
stx2 := signedDelegateStakingTxn(key)
stxs := []*staking.StakingTransaction{stx1, stx2}
// make a fake block header
block := types.NewBlock(header, txs, []*types.Receipt{types.NewReceipt([]byte{}, false, 0), types.NewReceipt([]byte{}, false, 0),
types.NewReceipt([]byte{}, false, 0)}, nil, nil, stxs)
// run it
if _, _, err := chain.prepareStakingMetaData(block, []staking.StakeMsg{&staking.Delegate{}}, db); err != nil {
if err.Error() != "address not present in state" { // when called in test for core/vm
t.Errorf("Got error %v in prepareStakingMetaData", err)
}
} else {
// when called independently there is no error
}
}
func signedCreateValidatorStakingTxn(key *ecdsa.PrivateKey) *staking.StakingTransaction {
stakePayloadMaker := func() (staking.Directive, interface{}) {
return staking.DirectiveCreateValidator, sampleCreateValidator(*key)
}
stx, _ := staking.NewStakingTransaction(0, 1e10, big.NewInt(10000), stakePayloadMaker)
signed, _ := staking.Sign(stx, staking.NewEIP155Signer(stx.ChainID()), key)
return signed
}
func signedEditValidatorStakingTxn(key *ecdsa.PrivateKey) *staking.StakingTransaction {
stakePayloadMaker := func() (staking.Directive, interface{}) {
return staking.DirectiveEditValidator, sampleEditValidator(*key)
}
stx, _ := staking.NewStakingTransaction(0, 1e10, big.NewInt(10000), stakePayloadMaker)
signed, _ := staking.Sign(stx, staking.NewEIP155Signer(stx.ChainID()), key)
return signed
}
func signedDelegateStakingTxn(key *ecdsa.PrivateKey) *staking.StakingTransaction {
stakePayloadMaker := func() (staking.Directive, interface{}) {
return staking.DirectiveDelegate, sampleDelegate(*key)
}
// nonce, gasLimit uint64, gasPrice *big.Int, f StakeMsgFulfiller
stx, _ := staking.NewStakingTransaction(0, 1e10, big.NewInt(10000), stakePayloadMaker)
signed, _ := staking.Sign(stx, staking.NewEIP155Signer(stx.ChainID()), key)
return signed
}
func signedUndelegateStakingTxn(key *ecdsa.PrivateKey) *staking.StakingTransaction {
stakePayloadMaker := func() (staking.Directive, interface{}) {
return staking.DirectiveUndelegate, sampleUndelegate(*key)
}
// nonce, gasLimit uint64, gasPrice *big.Int, f StakeMsgFulfiller
stx, _ := staking.NewStakingTransaction(0, 1e10, big.NewInt(10000), stakePayloadMaker)
signed, _ := staking.Sign(stx, staking.NewEIP155Signer(stx.ChainID()), key)
return signed
}
func signedCollectRewardsStakingTxn(key *ecdsa.PrivateKey) *staking.StakingTransaction {
stakePayloadMaker := func() (staking.Directive, interface{}) {
return staking.DirectiveCollectRewards, sampleCollectRewards(*key)
}
// nonce, gasLimit uint64, gasPrice *big.Int, f StakeMsgFulfiller
stx, _ := staking.NewStakingTransaction(0, 1e10, big.NewInt(10000), stakePayloadMaker)
signed, _ := staking.Sign(stx, staking.NewEIP155Signer(stx.ChainID()), key)
return signed
}

@ -101,7 +101,7 @@ func (b *BlockGen) AddTxWithChain(bc *BlockChain, tx *types.Transaction) {
b.statedb.Prepare(tx.Hash(), common.Hash{}, len(b.txs))
coinbase := b.header.Coinbase()
gasUsed := b.header.GasUsed()
receipt, _, _, err := ApplyTransaction(b.config, bc, &coinbase, b.gasPool, b.statedb, b.header, tx, &gasUsed, vm.Config{})
receipt, _, _, _, err := ApplyTransaction(b.config, bc, &coinbase, b.gasPool, b.statedb, b.header, tx, &gasUsed, vm.Config{})
b.header.SetGasUsed(gasUsed)
b.header.SetCoinbase(coinbase)
if err != nil {

@ -17,16 +17,22 @@
package core
import (
"bytes"
"errors"
"math"
"math/big"
"github.com/harmony-one/harmony/internal/params"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/harmony-one/harmony/block"
consensus_engine "github.com/harmony-one/harmony/consensus/engine"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm"
staking "github.com/harmony-one/harmony/staking/types"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/internal/utils"
staking "github.com/harmony-one/harmony/staking"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
// ChainContext supports retrieving headers and consensus parameters from the
@ -39,16 +45,18 @@ type ChainContext interface {
GetHeader(common.Hash, uint64) *block.Header
// ReadDelegationsByDelegator returns the validators list of a delegator
ReadDelegationsByDelegator(common.Address) (staking.DelegationIndexes, error)
ReadDelegationsByDelegator(common.Address) (stakingTypes.DelegationIndexes, error)
// ReadValidatorSnapshot returns the snapshot of validator at the beginning of current epoch.
ReadValidatorSnapshot(common.Address) (*staking.ValidatorSnapshot, error)
ReadValidatorSnapshot(common.Address) (*stakingTypes.ValidatorSnapshot, error)
// ReadValidatorList returns the list of all validators
ReadValidatorList() ([]common.Address, error)
// Config returns chain config
Config() *params.ChainConfig
ShardID() uint32 // this is implemented by blockchain.go already
}
// NewEVMContext creates a new context for use in the EVM.
@ -71,6 +79,13 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author
IsValidator: IsValidator,
GetHash: GetHashFn(header, chain),
GetVRF: GetVRFFn(header, chain),
CreateValidator: CreateValidatorFn(header, chain),
EditValidator: EditValidatorFn(header, chain),
Delegate: DelegateFn(header, chain),
Undelegate: UndelegateFn(header, chain),
CollectRewards: CollectRewardsFn(header, chain),
MigrateDelegations: MigrateDelegationsFn(header, chain),
CalculateMigrationGas: CalculateMigrationGasFn(chain),
Origin: msg.From(),
Coinbase: beneficiary,
BlockNumber: header.Number(),
@ -79,6 +94,237 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author
Time: header.Time(),
GasLimit: header.GasLimit(),
GasPrice: new(big.Int).Set(msg.GasPrice()),
ShardID: chain.ShardID(),
}
}
// HandleStakeMsgFn returns a function which accepts
// (1) the chain state database
// (2) the processed staking parameters
// the function can then be called through the EVM context
func CreateValidatorFn(ref *block.Header, chain ChainContext) vm.CreateValidatorFunc {
// moved from state_transition.go to here, with some modifications
return func(db vm.StateDB, createValidator *stakingTypes.CreateValidator) error {
wrapper, err := VerifyAndCreateValidatorFromMsg(
db, chain, ref.Epoch(), ref.Number(), createValidator,
)
if err != nil {
return err
}
if err := db.UpdateValidatorWrapper(wrapper.Address, wrapper); err != nil {
return err
}
db.SetValidatorFlag(createValidator.ValidatorAddress)
db.SubBalance(createValidator.ValidatorAddress, createValidator.Amount)
return nil
}
}
func EditValidatorFn(ref *block.Header, chain ChainContext) vm.EditValidatorFunc {
// moved from state_transition.go to here, with some modifications
return func(db vm.StateDB, editValidator *stakingTypes.EditValidator) error {
wrapper, err := VerifyAndEditValidatorFromMsg(
db, chain, ref.Epoch(), ref.Number(), editValidator,
)
if err != nil {
return err
}
return db.UpdateValidatorWrapper(wrapper.Address, wrapper)
}
}
func DelegateFn(ref *block.Header, chain ChainContext) vm.DelegateFunc {
// moved from state_transition.go to here, with some modifications
return func(db vm.StateDB, delegate *stakingTypes.Delegate) error {
delegations, err := chain.ReadDelegationsByDelegator(delegate.DelegatorAddress)
if err != nil {
return err
}
updatedValidatorWrappers, balanceToBeDeducted, fromLockedTokens, err := VerifyAndDelegateFromMsg(
db, ref.Epoch(), delegate, delegations, chain.Config())
if err != nil {
return err
}
for _, wrapper := range updatedValidatorWrappers {
if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil {
return err
}
}
db.SubBalance(delegate.DelegatorAddress, balanceToBeDeducted)
if len(fromLockedTokens) > 0 {
sortedKeys := []common.Address{}
for key := range fromLockedTokens {
sortedKeys = append(sortedKeys, key)
}
sort.SliceStable(sortedKeys, func(i, j int) bool {
return bytes.Compare(sortedKeys[i][:], sortedKeys[j][:]) < 0
})
// Add log if everything is good
for _, key := range sortedKeys {
redelegatedToken, ok := fromLockedTokens[key]
if !ok {
return errors.New("Key missing for delegation receipt")
}
encodedRedelegationData := []byte{}
addrBytes := key.Bytes()
encodedRedelegationData = append(encodedRedelegationData, addrBytes...)
encodedRedelegationData = append(encodedRedelegationData, redelegatedToken.Bytes()...)
// The data field format is:
// [first 20 bytes]: Validator address from which the locked token is used for redelegation.
// [rest of the bytes]: the bigInt serialized bytes for the token amount.
db.AddLog(&types.Log{
Address: delegate.DelegatorAddress,
Topics: []common.Hash{staking.DelegateTopic},
Data: encodedRedelegationData,
BlockNumber: ref.Number().Uint64(),
})
}
}
return nil
}
}
func UndelegateFn(ref *block.Header, chain ChainContext) vm.UndelegateFunc {
// moved from state_transition.go to here, with some modifications
return func(db vm.StateDB, undelegate *stakingTypes.Undelegate) error {
wrapper, err := VerifyAndUndelegateFromMsg(db, ref.Epoch(), undelegate)
if err != nil {
return err
}
return db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper)
}
}
func CollectRewardsFn(ref *block.Header, chain ChainContext) vm.CollectRewardsFunc {
return func(db vm.StateDB, collectRewards *stakingTypes.CollectRewards) error {
if chain == nil {
return errors.New("[CollectRewards] No chain context provided")
}
delegations, err := chain.ReadDelegationsByDelegator(collectRewards.DelegatorAddress)
if err != nil {
return err
}
updatedValidatorWrappers, totalRewards, err := VerifyAndCollectRewardsFromDelegation(
db, delegations,
)
if err != nil {
return err
}
for _, wrapper := range updatedValidatorWrappers {
if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil {
return err
}
}
db.AddBalance(collectRewards.DelegatorAddress, totalRewards)
// Add log if everything is good
db.AddLog(&types.Log{
Address: collectRewards.DelegatorAddress,
Topics: []common.Hash{staking.CollectRewardsTopic},
Data: totalRewards.Bytes(),
BlockNumber: ref.Number().Uint64(),
})
return nil
}
}
func MigrateDelegationsFn(ref *block.Header, chain ChainContext) vm.MigrateDelegationsFunc {
return func(db vm.StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error) {
// get existing delegations
fromDelegations, err := chain.ReadDelegationsByDelegator(migrationMsg.From)
if err != nil {
return nil, err
}
// get list of modified wrappers
wrappers, delegates, err := VerifyAndMigrateFromMsg(db, migrationMsg, fromDelegations)
if err != nil {
return nil, err
}
// add to state db
for _, wrapper := range wrappers {
if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil {
return nil, err
}
}
return delegates, nil
}
}
// calculate the gas for migration; no checks done here similar to other functions
// the checks are handled by staking_verifier.go, ex, if you try to delegate to an address
// who is not a validator - you will be charged all gas passed in two steps
// - 22k initially when gas is calculated
// - remainder when the tx inevitably is a no-op
// i have followed the same logic here, this only produces an error if can't read from db
func CalculateMigrationGasFn(chain ChainContext) vm.CalculateMigrationGasFunc {
return func(db vm.StateDB, migrationMsg *stakingTypes.MigrationMsg, homestead bool, istanbul bool) (uint64, error) {
var gas uint64 = 0
delegations, err := chain.ReadDelegationsByDelegator(migrationMsg.From)
if err != nil {
return 0, err
}
for i := range delegations {
delegationIndex := &delegations[i]
wrapper, err := db.ValidatorWrapper(delegationIndex.ValidatorAddress, true, false)
if err != nil {
return 0, err
}
if uint64(len(wrapper.Delegations)) <= delegationIndex.Index {
utils.Logger().Warn().
Str("validator", delegationIndex.ValidatorAddress.String()).
Uint64("delegation index", delegationIndex.Index).
Int("delegations length", len(wrapper.Delegations)).
Msg("Delegation index out of bound")
return 0, errors.New("Delegation index out of bound")
}
foundDelegation := &wrapper.Delegations[delegationIndex.Index]
// no need to migrate if amount and undelegations are 0
if foundDelegation.Amount.Cmp(common.Big0) == 0 && len(foundDelegation.Undelegations) == 0 {
continue
}
delegate := stakingTypes.Delegate{
DelegatorAddress: migrationMsg.From,
ValidatorAddress: delegationIndex.ValidatorAddress,
Amount: foundDelegation.Amount,
}
encoded, err := rlp.EncodeToBytes(delegate)
if err != nil {
return 0, err
}
thisGas, err := vm.IntrinsicGas(
encoded,
false,
homestead,
istanbul,
false, // isValidatorCreation
)
if err != nil {
return 0, err
}
// overflow when gas + thisGas > Math.MaxUint64
// or Math.MaxUint64 < gas + thisGas
// or Math.MaxUint64 - gas < thisGas
if (math.MaxUint64 - gas) < thisGas {
return 0, vm.ErrOutOfGas
}
gas += thisGas
}
if gas != 0 {
return gas, nil
} else {
// base gas fee if nothing to do, for example, when
// there are no delegations to migrate
return vm.IntrinsicGas(
[]byte{},
false,
homestead,
istanbul,
false, // isValidatorCreation
)
}
}
}

@ -0,0 +1,478 @@
package core
import (
"crypto/ecdsa"
"errors"
"fmt"
"math"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
bls_core "github.com/harmony-one/bls/ffi/go/bls"
"github.com/harmony-one/harmony/block"
blockfactory "github.com/harmony-one/harmony/block/factory"
"github.com/harmony-one/harmony/common/denominations"
"github.com/harmony-one/harmony/core/state"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/crypto/bls"
"github.com/harmony-one/harmony/crypto/hash"
chain2 "github.com/harmony-one/harmony/internal/chain"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/numeric"
"github.com/harmony-one/harmony/staking/effective"
staking "github.com/harmony-one/harmony/staking/types"
staketest "github.com/harmony-one/harmony/staking/types/test"
)
func getTestEnvironment(testBankKey ecdsa.PrivateKey) (*BlockChain, *state.DB, *block.Header, ethdb.Database) {
// initialize
var (
testBankAddress = crypto.PubkeyToAddress(testBankKey.PublicKey)
testBankFunds = new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(40000))
chainConfig = params.TestChainConfig
blockFactory = blockfactory.ForTest
database = rawdb.NewMemoryDatabase()
gspec = Genesis{
Config: chainConfig,
Factory: blockFactory,
Alloc: GenesisAlloc{testBankAddress: {Balance: testBankFunds}},
ShardID: 0,
}
engine = chain2.NewEngine()
)
genesis := gspec.MustCommit(database)
// fake blockchain
chain, _ := NewBlockChain(database, nil, gspec.Config, engine, vm.Config{}, nil)
db, _ := chain.StateAt(genesis.Root())
// make a fake block header (use epoch 1 so that locked tokens can be tested)
header := blockFactory.NewHeader(common.Big0)
return chain, db, header, database
}
func TestEVMStaking(t *testing.T) {
key, _ := crypto.GenerateKey()
chain, db, header, database := getTestEnvironment(*key)
batch := database.NewBatch()
// fake transaction
tx := types.NewTransaction(1, common.BytesToAddress([]byte{0x11}), 0, big.NewInt(111), 1111, big.NewInt(11111), []byte{0x11, 0x11, 0x11})
// transaction as message (chainId = 2)
msg, _ := tx.AsMessage(types.NewEIP155Signer(common.Big2))
// context
ctx := NewEVMContext(msg, header, chain, nil /* coinbase */)
// createValidator test
createValidator := sampleCreateValidator(*key)
err := ctx.CreateValidator(db, &createValidator)
if err != nil {
t.Errorf("Got error %v in CreateValidator", err)
}
// write it to snapshot so that we can use it in edit
// use a copy because we are editing below (wrapper.Delegations)
wrapper, err := db.ValidatorWrapper(createValidator.ValidatorAddress, false, true)
err = chain.WriteValidatorSnapshot(batch, &staking.ValidatorSnapshot{wrapper, header.Epoch()})
// also write the delegation so we can use it in CollectRewards
selfIndex := staking.DelegationIndex{
createValidator.ValidatorAddress,
uint64(0),
common.Big0, // block number at which delegation starts
}
err = chain.writeDelegationsByDelegator(batch, createValidator.ValidatorAddress, []staking.DelegationIndex{selfIndex})
// editValidator test
editValidator := sampleEditValidator(*key)
editValidator.SlotKeyToRemove = &createValidator.SlotPubKeys[0]
err = ctx.EditValidator(db, &editValidator)
if err != nil {
t.Errorf("Got error %v in EditValidator", err)
}
// delegate test
delegate := sampleDelegate(*key)
// add undelegations in epoch0
wrapper.Delegations[0].Undelegations = []staking.Undelegation{
staking.Undelegation{
new(big.Int).Mul(big.NewInt(denominations.One),
big.NewInt(10000)),
common.Big0,
},
}
// redelegate using epoch1, so that we can cover the locked tokens use case as well
ctx2 := NewEVMContext(msg, blockfactory.ForTest.NewHeader(common.Big1), chain, nil)
err = db.UpdateValidatorWrapper(wrapper.Address, wrapper)
err = ctx2.Delegate(db, &delegate)
if err != nil {
t.Errorf("Got error %v in Delegate", err)
}
// undelegate test
undelegate := sampleUndelegate(*key)
err = ctx.Undelegate(db, &undelegate)
if err != nil {
t.Errorf("Got error %v in Undelegate", err)
}
// collectRewards test
collectRewards := sampleCollectRewards(*key)
// add block rewards to make sure there are some to collect
wrapper.Delegations[0].Undelegations = []staking.Undelegation{}
wrapper.Delegations[0].Reward = common.Big257
db.UpdateValidatorWrapper(wrapper.Address, wrapper)
err = ctx.CollectRewards(db, &collectRewards)
if err != nil {
t.Errorf("Got error %v in CollectRewards", err)
}
// migration test - when from has no delegations
toKey, _ := crypto.GenerateKey()
migration := sampleMigrationMsg(*toKey, *key)
delegates, err := ctx.MigrateDelegations(db, &migration)
expectedError := errors.New("No delegations to migrate")
if err != nil && expectedError.Error() != err.Error() {
t.Errorf("Got error %v in MigrateDelegations but expected %v", err, expectedError)
}
if len(delegates) > 0 {
t.Errorf("Got delegates to migrate when none were expected")
}
// migration test - when from == to
migration = sampleMigrationMsg(*toKey, *toKey)
delegates, err = ctx.MigrateDelegations(db, &migration)
expectedError = errors.New("From and To are the same address")
if err != nil && expectedError.Error() != err.Error() {
t.Errorf("Got error %v in MigrateDelegations but expected %v", err, expectedError)
}
if len(delegates) > 0 {
t.Errorf("Got delegates to migrate when none were expected")
}
// migration test - when `to` has no delegations
snapshot := db.Snapshot()
migration = sampleMigrationMsg(*key, *toKey)
wrapper, _ = db.ValidatorWrapper(wrapper.Address, true, false)
expectedAmount := wrapper.Delegations[0].Amount
delegates, err = ctx.MigrateDelegations(db, &migration)
if err != nil {
t.Errorf("Got error %v in MigrateDelegations", err)
}
if len(delegates) != 1 {
t.Errorf("Got %d delegations to migrate, expected just 1", len(delegates))
}
wrapper, _ = db.ValidatorWrapper(createValidator.ValidatorAddress, false, true)
if wrapper.Delegations[0].Amount.Cmp(common.Big0) != 0 {
t.Errorf("Expected delegation at index 0 to have amount 0, but it has amount %d",
wrapper.Delegations[0].Amount)
}
if wrapper.Delegations[1].Amount.Cmp(expectedAmount) != 0 {
t.Errorf("Expected delegation at index 1 to have amount %d, but it has amount %d",
expectedAmount, wrapper.Delegations[1].Amount)
}
db.RevertToSnapshot(snapshot)
snapshot = db.Snapshot()
// migration test - when `from` delegation amount = 0 and no undelegations
wrapper, _ = db.ValidatorWrapper(createValidator.ValidatorAddress, false, true)
wrapper.Delegations[0].Undelegations = make([]staking.Undelegation, 0)
wrapper.Delegations[0].Amount = common.Big0
wrapper.Status = effective.Inactive
err = db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper)
if err != nil {
t.Errorf("Got error %v in UpdateValidatorWrapperWithRevert", err)
}
delegates, err = ctx.MigrateDelegations(db, &migration)
if err != nil {
t.Errorf("Got error %v in MigrateDelegations", err)
}
if len(delegates) != 0 {
t.Errorf("Got %d delegations to migrate, expected none", len(delegates))
}
db.RevertToSnapshot(snapshot)
snapshot = db.Snapshot()
// migration test - when `to` has one delegation
wrapper, _ = db.ValidatorWrapper(createValidator.ValidatorAddress, false, true)
wrapper.Delegations = append(wrapper.Delegations, staking.NewDelegation(
migration.To, new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(100))))
expectedAmount = big.NewInt(0).Add(
wrapper.Delegations[0].Amount, wrapper.Delegations[1].Amount,
)
err = db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper)
if err != nil {
t.Errorf("Got error %v in UpdateValidatorWrapperWithRevert", err)
}
delegates, err = ctx.MigrateDelegations(db, &migration)
if err != nil {
t.Errorf("Got error %v in MigrateDelegations", err)
}
if len(delegates) != 1 {
t.Errorf("Got %d delegations to migrate, expected just 1", len(delegates))
}
// read from updated wrapper
wrapper, _ = db.ValidatorWrapper(createValidator.ValidatorAddress, true, false)
if wrapper.Delegations[0].Amount.Cmp(common.Big0) != 0 {
t.Errorf("Expected delegation at index 0 to have amount 0, but it has amount %d",
wrapper.Delegations[0].Amount)
}
if wrapper.Delegations[1].Amount.Cmp(expectedAmount) != 0 {
t.Errorf("Expected delegation at index 1 to have amount %d, but it has amount %d",
expectedAmount, wrapper.Delegations[1].Amount)
}
db.RevertToSnapshot(snapshot)
snapshot = db.Snapshot()
// migration test - when `to` has one undelegation in the current epoch and so does `from`
wrapper, _ = db.ValidatorWrapper(createValidator.ValidatorAddress, false, true)
delegation := staking.NewDelegation(migration.To, big.NewInt(0))
delegation.Undelegations = []staking.Undelegation{
staking.Undelegation{
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(100)),
Epoch: common.Big0,
},
}
wrapper.Delegations[0].Undelegate(
big.NewInt(0), new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(100)),
)
expectedAmount = big.NewInt(0).Add(
wrapper.Delegations[0].Amount, big.NewInt(0),
)
wrapper.Delegations = append(wrapper.Delegations, delegation)
expectedDelegations := []staking.Delegation{
staking.Delegation{
DelegatorAddress: wrapper.Address,
Amount: big.NewInt(0),
Reward: big.NewInt(0),
Undelegations: []staking.Undelegation{},
},
staking.Delegation{
DelegatorAddress: crypto.PubkeyToAddress(toKey.PublicKey),
Amount: expectedAmount,
Undelegations: []staking.Undelegation{
staking.Undelegation{
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(200)),
Epoch: common.Big0,
},
},
},
}
err = db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper)
if err != nil {
t.Errorf("Got error %v in UpdateValidatorWrapperWithRevert", err)
}
delegates, err = ctx.MigrateDelegations(db, &migration)
if err != nil {
t.Errorf("Got error %v in MigrateDelegations", err)
}
if len(delegates) != 1 {
t.Errorf("Got %d delegations to migrate, expected just 1", len(delegates))
}
// read from updated wrapper
wrapper, _ = db.ValidatorWrapper(createValidator.ValidatorAddress, true, false)
// and finally the check for delegations being equal
if err := staketest.CheckDelegationsEqual(
wrapper.Delegations,
expectedDelegations,
); err != nil {
t.Errorf("Got error %s in CheckDelegationsEqual", err)
}
// test for migration gas
db.RevertToSnapshot(snapshot)
// to calculate gas we need to test 0 / 1 / 2 delegations to migrate as per ReadDelegationsByDelegator
// case 0
evm := vm.NewEVM(ctx, db, params.TestChainConfig, vm.Config{})
gas, err := ctx.CalculateMigrationGas(db, &staking.MigrationMsg{
From: crypto.PubkeyToAddress(toKey.PublicKey),
To: crypto.PubkeyToAddress(key.PublicKey),
}, evm.ChainConfig().IsS3(evm.EpochNumber), evm.ChainConfig().IsS3(evm.EpochNumber))
var expectedGasMin uint64 = params.TxGas
if err != nil {
t.Errorf("Gas error %s", err)
}
if gas < expectedGasMin {
t.Errorf("Gas for 0 migration was expected to be at least %d but got %d", expectedGasMin, gas)
}
// case 1
gas, err = ctx.CalculateMigrationGas(db, &migration,
evm.ChainConfig().IsS3(evm.EpochNumber), evm.ChainConfig().IsS3(evm.EpochNumber))
expectedGasMin = params.TxGas
if err != nil {
t.Errorf("Gas error %s", err)
}
if gas < expectedGasMin {
t.Errorf("Gas for 1 migration was expected to be at least %d but got %d", expectedGasMin, gas)
}
// case 2
createValidator = sampleCreateValidator(*toKey)
db.AddBalance(createValidator.ValidatorAddress, createValidator.Amount)
err = ctx.CreateValidator(db, &createValidator)
delegate = sampleDelegate(*toKey)
delegate.DelegatorAddress = crypto.PubkeyToAddress(key.PublicKey)
_ = ctx.Delegate(db, &delegate)
delegationIndex := staking.DelegationIndex{
ValidatorAddress: crypto.PubkeyToAddress(toKey.PublicKey),
Index: uint64(1),
BlockNum: common.Big0,
}
err = chain.writeDelegationsByDelegator(batch, migration.From, []staking.DelegationIndex{selfIndex, delegationIndex})
gas, err = ctx.CalculateMigrationGas(db, &migration,
evm.ChainConfig().IsS3(evm.EpochNumber), evm.ChainConfig().IsS3(evm.EpochNumber))
expectedGasMin = 2 * params.TxGas
if err != nil {
t.Errorf("Gas error %s", err)
}
if gas < expectedGasMin {
t.Errorf("Gas for 2 migrations was expected to be at least %d but got %d", expectedGasMin, gas)
}
}
func generateBLSKeyAndSig() (bls.SerializedPublicKey, bls.SerializedSignature) {
p := &bls_core.PublicKey{}
p.DeserializeHexStr(testBLSPubKey)
pub := bls.SerializedPublicKey{}
pub.FromLibBLSPublicKey(p)
messageBytes := []byte(staking.BLSVerificationStr)
privateKey := &bls_core.SecretKey{}
privateKey.DeserializeHexStr(testBLSPrvKey)
msgHash := hash.Keccak256(messageBytes)
signature := privateKey.SignHash(msgHash[:])
var sig bls.SerializedSignature
copy(sig[:], signature.Serialize())
return pub, sig
}
func sampleCreateValidator(key ecdsa.PrivateKey) staking.CreateValidator {
pub, sig := generateBLSKeyAndSig()
ra, _ := numeric.NewDecFromStr("0.7")
maxRate, _ := numeric.NewDecFromStr("1")
maxChangeRate, _ := numeric.NewDecFromStr("0.5")
return staking.CreateValidator{
Description: staking.Description{
Name: "SuperHero",
Identity: "YouWouldNotKnow",
Website: "Secret Website",
SecurityContact: "LicenseToKill",
Details: "blah blah blah",
},
CommissionRates: staking.CommissionRates{
Rate: ra,
MaxRate: maxRate,
MaxChangeRate: maxChangeRate,
},
MinSelfDelegation: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(10000)),
MaxTotalDelegation: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(20000)),
ValidatorAddress: crypto.PubkeyToAddress(key.PublicKey),
SlotPubKeys: []bls.SerializedPublicKey{pub},
SlotKeySigs: []bls.SerializedSignature{sig},
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(15000)),
}
}
func sampleEditValidator(key ecdsa.PrivateKey) staking.EditValidator {
// generate new key and sig
slotKeyToAdd, slotKeyToAddSig := generateBLSKeyAndSig()
// rate
ra, _ := numeric.NewDecFromStr("0.8")
return staking.EditValidator{
Description: staking.Description{
Name: "Alice",
Identity: "alice",
Website: "alice.harmony.one",
SecurityContact: "Bob",
Details: "Don't mess with me!!!",
},
CommissionRate: &ra,
MinSelfDelegation: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(10000)),
MaxTotalDelegation: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(20000)),
SlotKeyToRemove: nil,
SlotKeyToAdd: &slotKeyToAdd,
SlotKeyToAddSig: &slotKeyToAddSig,
ValidatorAddress: crypto.PubkeyToAddress(key.PublicKey),
}
}
func sampleDelegate(key ecdsa.PrivateKey) staking.Delegate {
address := crypto.PubkeyToAddress(key.PublicKey)
return staking.Delegate{
DelegatorAddress: address,
ValidatorAddress: address,
// additional delegation of 1000 ONE
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(1000)),
}
}
func sampleUndelegate(key ecdsa.PrivateKey) staking.Undelegate {
address := crypto.PubkeyToAddress(key.PublicKey)
return staking.Undelegate{
DelegatorAddress: address,
ValidatorAddress: address,
// undelegate the delegation of 1000 ONE
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(1000)),
}
}
func sampleCollectRewards(key ecdsa.PrivateKey) staking.CollectRewards {
address := crypto.PubkeyToAddress(key.PublicKey)
return staking.CollectRewards{
DelegatorAddress: address,
}
}
func sampleMigrationMsg(from ecdsa.PrivateKey, to ecdsa.PrivateKey) staking.MigrationMsg {
fromAddress := crypto.PubkeyToAddress(from.PublicKey)
toAddress := crypto.PubkeyToAddress(to.PublicKey)
return staking.MigrationMsg{
From: fromAddress,
To: toAddress,
}
}
func TestWriteCapablePrecompilesIntegration(t *testing.T) {
key, _ := crypto.GenerateKey()
chain, db, header, _ := getTestEnvironment(*key)
// gp := new(GasPool).AddGas(math.MaxUint64)
tx := types.NewTransaction(1, common.BytesToAddress([]byte{0x11}), 0, big.NewInt(111), 1111, big.NewInt(11111), []byte{0x11, 0x11, 0x11})
msg, _ := tx.AsMessage(types.NewEIP155Signer(common.Big2))
ctx := NewEVMContext(msg, header, chain, nil /* coinbase */)
evm := vm.NewEVM(ctx, db, params.TestChainConfig, vm.Config{})
// interpreter := vm.NewEVMInterpreter(evm, vm.Config{})
address := common.BytesToAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252})
// caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int)
_, _, err := evm.Call(vm.AccountRef(common.Address{}), address,
[]byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19},
math.MaxUint64, new(big.Int))
expectedError := errors.New("abi: cannot marshal in to go type: length insufficient 31 require 32")
if err != nil {
if err.Error() != expectedError.Error() {
t.Errorf(fmt.Sprintf("Got error %v in evm.Call but expected %v", err, expectedError))
}
}
// now add a validator, and send its address as caller
createValidator := sampleCreateValidator(*key)
err = ctx.CreateValidator(db, &createValidator)
_, _, err = evm.Call(vm.AccountRef(common.Address{}),
createValidator.ValidatorAddress,
[]byte{},
math.MaxUint64, new(big.Int))
if err != nil {
t.Errorf(fmt.Sprintf("Got error %v in evm.Call", err))
}
// now without staking precompile
cfg := params.TestChainConfig
cfg.StakingPrecompileEpoch = big.NewInt(10000000)
evm = vm.NewEVM(ctx, db, cfg, vm.Config{})
_, _, err = evm.Call(vm.AccountRef(common.Address{}),
createValidator.ValidatorAddress,
[]byte{},
math.MaxUint64, new(big.Int))
if err != nil {
t.Errorf(fmt.Sprintf("Got error %v in evm.Call", err))
}
}

@ -28,6 +28,7 @@ func (bc *BlockChain) CommitOffChainData(
block *types.Block,
receipts []*types.Receipt,
cxReceipts []*types.CXReceipt,
stakeMsgs []staking.StakeMsg,
payout reward.Reader,
state *state.DB,
) (status WriteStatus, err error) {
@ -118,7 +119,7 @@ func (bc *BlockChain) CommitOffChainData(
// Do bookkeeping for new staking txns
newVals, err := bc.UpdateStakingMetaData(
batch, block, state, epoch, nextBlockEpoch,
batch, block, stakeMsgs, state, epoch, nextBlockEpoch,
)
if err != nil {
utils.Logger().Err(err).Msg("UpdateStakingMetaData failed")

@ -2,7 +2,9 @@ package core
import (
"bytes"
"fmt"
"math/big"
"sort"
"github.com/harmony-one/harmony/staking/availability"
@ -396,6 +398,121 @@ func VerifyAndUndelegateFromMsg(
return nil, errNoDelegationToUndelegate
}
// VerifyAndMigrateFromMsg verifies and transfers all delegations of
// msg.From to msg.To. Returns all modified validator wrappers and delegate msgs
// for metadata
// Note that this function never updates the stateDB, it only reads from stateDB.
func VerifyAndMigrateFromMsg(
stateDB vm.StateDB,
msg *staking.MigrationMsg,
fromDelegations []staking.DelegationIndex,
) ([]*staking.ValidatorWrapper,
[]interface{},
error) {
if bytes.Equal(msg.From.Bytes(), msg.To.Bytes()) {
return nil, nil, errors.New("From and To are the same address")
}
if len(fromDelegations) == 0 {
return nil, nil, errors.New("No delegations to migrate")
}
modifiedWrappers := make([]*staking.ValidatorWrapper, 0)
stakeMsgs := make([]interface{}, 0)
// iterate over all delegationIndexes by `From`
for i := range fromDelegations {
delegationIndex := &fromDelegations[i]
// find the wrapper for each delegationIndex
// request a copy, and since delegations will be changed, copy them too
wrapper, err := stateDB.ValidatorWrapper(delegationIndex.ValidatorAddress, false, true)
if err != nil {
return nil, nil, err
}
if uint64(len(wrapper.Delegations)) <= delegationIndex.Index {
utils.Logger().Warn().
Str("validator", delegationIndex.ValidatorAddress.String()).
Uint64("delegation index", delegationIndex.Index).
Int("delegations length", len(wrapper.Delegations)).
Msg("Delegation index out of bound")
return nil, nil, errors.New("Delegation index out of bound")
}
// and then find matching delegation to remove from wrapper
foundDelegation := &wrapper.Delegations[delegationIndex.Index] // note: pointer
if !bytes.Equal(foundDelegation.DelegatorAddress.Bytes(), msg.From.Bytes()) {
return nil, nil, errors.New(fmt.Sprintf("Expected %s but got %s",
msg.From.Hex(),
foundDelegation.DelegatorAddress.Hex()))
}
// Skip delegations with zero amount and empty undelegation
if foundDelegation.Amount.Cmp(common.Big0) == 0 && len(foundDelegation.Undelegations) == 0 {
continue
}
delegationAmountToMigrate := big.NewInt(0).Add(foundDelegation.Amount, big.NewInt(0))
undelegationsToMigrate := foundDelegation.Undelegations
// when undelegating we don't remove, just set the amount to zero
// to be coherent, do the same thing here (effective on wrapper since pointer)
foundDelegation.Amount = big.NewInt(0)
foundDelegation.Undelegations = make([]staking.Undelegation, 0)
// find `To` and give it to them
totalAmount := big.NewInt(0)
found := false
for i := range wrapper.Delegations {
delegation := &wrapper.Delegations[i]
if bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.To.Bytes()) {
found = true
// add to existing delegation
totalAmount = delegation.Amount.Add(delegation.Amount, delegationAmountToMigrate)
// and the undelegations
for _, undelegationToMigrate := range undelegationsToMigrate {
exist := false
for _, entry := range delegation.Undelegations {
if entry.Epoch.Cmp(undelegationToMigrate.Epoch) == 0 {
exist = true
entry.Amount.Add(entry.Amount, undelegationToMigrate.Amount)
break
}
}
if !exist {
delegation.Undelegations = append(delegation.Undelegations,
undelegationToMigrate)
}
}
// Always sort the undelegate by epoch in increasing order
sort.SliceStable(
delegation.Undelegations,
func(i, j int) bool {
return delegation.Undelegations[i].Epoch.Cmp(delegation.Undelegations[j].Epoch) < 0
},
)
break
}
}
if !found { // add the delegation
wrapper.Delegations = append(
wrapper.Delegations, staking.NewDelegation(
msg.To, delegationAmountToMigrate,
),
)
totalAmount = delegationAmountToMigrate
}
if err := wrapper.SanityCheck(); err != nil {
// allow self delegation to go below min self delegation
// but set the status to inactive
if errors.Cause(err) == staking.ErrInvalidSelfDelegation {
wrapper.Status = effective.Inactive
} else {
return nil, nil, err
}
}
modifiedWrappers = append(modifiedWrappers, wrapper)
delegate := &staking.Delegate{
ValidatorAddress: wrapper.Address,
DelegatorAddress: msg.To,
Amount: totalAmount,
}
stakeMsgs = append(stakeMsgs, delegate)
}
return modifiedWrappers, stakeMsgs, nil
}
// VerifyAndCollectRewardsFromDelegation verifies and collects rewards
// from the given delegation slice using the stateDB. It returns all of the
// edited validatorWrappers and the sum total of the rewards.

@ -20,6 +20,7 @@ import (
"github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/crypto/hash"
"github.com/harmony-one/harmony/numeric"
"github.com/harmony-one/harmony/shard"
"github.com/harmony-one/harmony/staking/effective"
staking "github.com/harmony-one/harmony/staking/types"
staketest "github.com/harmony-one/harmony/staking/types/test"
@ -1738,6 +1739,10 @@ func (chain *fakeChainContext) ReadDelegationsByDelegator(common.Address) (staki
return nil, nil
}
func (chain *fakeChainContext) ShardID() uint32 {
return shard.BeaconChainShardID
}
func (chain *fakeChainContext) ReadValidatorSnapshot(addr common.Address) (*staking.ValidatorSnapshot, error) {
w, ok := chain.vWrappers[addr]
if !ok {
@ -1774,6 +1779,10 @@ func (chain *fakeErrChainContext) ReadDelegationsByDelegator(common.Address) (st
return nil, nil
}
func (chain *fakeErrChainContext) ShardID() uint32 {
return 900 // arbitrary number different from BeaconChainShardID
}
func (chain *fakeErrChainContext) ReadValidatorSnapshot(common.Address) (*staking.ValidatorSnapshot, error) {
return nil, errors.New("error intended")
}

@ -54,9 +54,11 @@ type StateProcessor struct {
resultCache *lru.Cache // Cache for result after a certain block is processed
}
// this structure is cached, and each individual element is returned
type ProcessorResult struct {
Receipts types.Receipts
CxReceipts types.CXReceipts
StakeMsgs []staking.StakeMsg
Logs []*types.Log
UsedGas uint64
Reward reward.Reader
@ -86,7 +88,7 @@ func NewStateProcessor(
func (p *StateProcessor) Process(
block *types.Block, statedb *state.DB, cfg vm.Config, readCache bool,
) (
types.Receipts, types.CXReceipts,
types.Receipts, types.CXReceipts, []staking.StakeMsg,
[]*types.Log, uint64, reward.Reader, *state.DB, error,
) {
cacheKey := block.Hash()
@ -96,7 +98,7 @@ func (p *StateProcessor) Process(
// Only the successful results are cached in case for retry.
result := cached.(*ProcessorResult)
utils.Logger().Info().Str("block num", block.Number().String()).Msg("result cache hit.")
return result.Receipts, result.CxReceipts, result.Logs, result.UsedGas, result.Reward, result.State, nil
return result.Receipts, result.CxReceipts, result.StakeMsgs, result.Logs, result.UsedGas, result.Reward, result.State, nil
}
}
@ -108,27 +110,31 @@ func (p *StateProcessor) Process(
header = block.Header()
allLogs []*types.Log
gp = new(GasPool).AddGas(block.GasLimit())
blockStakeMsgs []staking.StakeMsg = make([]staking.StakeMsg, 0)
)
beneficiary, err := p.bc.GetECDSAFromCoinbase(header)
if err != nil {
return nil, nil, nil, 0, nil, statedb, err
return nil, nil, nil, nil, 0, nil, statedb, err
}
startTime := time.Now()
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
statedb.Prepare(tx.Hash(), block.Hash(), i)
receipt, cxReceipt, _, err := ApplyTransaction(
receipt, cxReceipt, stakeMsgs, _, err := ApplyTransaction(
p.config, p.bc, &beneficiary, gp, statedb, header, tx, usedGas, cfg,
)
if err != nil {
return nil, nil, nil, 0, nil, statedb, err
return nil, nil, nil, nil, 0, nil, statedb, err
}
receipts = append(receipts, receipt)
if cxReceipt != nil {
outcxs = append(outcxs, cxReceipt)
}
if len(stakeMsgs) > 0 {
blockStakeMsgs = append(blockStakeMsgs, stakeMsgs...)
}
allLogs = append(allLogs, receipt.Logs...)
}
utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("Process Normal Txns")
@ -142,7 +148,7 @@ func (p *StateProcessor) Process(
p.config, p.bc, &beneficiary, gp, statedb, header, tx, usedGas, cfg,
)
if err != nil {
return nil, nil, nil, 0, nil, statedb, err
return nil, nil, nil, nil, 0, nil, statedb, err
}
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
@ -156,14 +162,14 @@ func (p *StateProcessor) Process(
p.config, statedb, header, cx,
); err != nil {
return nil, nil,
nil, 0, nil, statedb, errors.New("[Process] Cannot apply incoming receipts")
nil, nil, 0, nil, statedb, errors.New("[Process] Cannot apply incoming receipts")
}
}
slashes := slash.Records{}
if s := header.Slashes(); len(s) > 0 {
if err := rlp.DecodeBytes(s, &slashes); err != nil {
return nil, nil, nil, 0, nil, statedb, errors.New(
return nil, nil, nil, nil, 0, nil, statedb, errors.New(
"[Process] Cannot finalize block",
)
}
@ -180,19 +186,20 @@ func (p *StateProcessor) Process(
receipts, outcxs, incxs, block.StakingTransactions(), slashes, sigsReady, func() uint64 { return header.ViewID().Uint64() },
)
if err != nil {
return nil, nil, nil, 0, nil, statedb, errors.New("[Process] Cannot finalize block")
return nil, nil, nil, nil, 0, nil, statedb, errors.New("[Process] Cannot finalize block")
}
result := &ProcessorResult{
Receipts: receipts,
CxReceipts: outcxs,
StakeMsgs: blockStakeMsgs,
Logs: allLogs,
UsedGas: *usedGas,
Reward: payout,
State: statedb,
}
p.resultCache.Add(cacheKey, result)
return receipts, outcxs, allLogs, *usedGas, payout, statedb, nil
return receipts, outcxs, blockStakeMsgs, allLogs, *usedGas, payout, statedb, nil
}
// CacheProcessorResult caches the process result on the cache key.
@ -223,14 +230,14 @@ func getTransactionType(
// and uses the input parameters for its environment. It returns the receipt
// for the transaction, gas used and an error if the transaction failed,
// indicating the block was invalid.
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.DB, header *block.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, *types.CXReceipt, uint64, error) {
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.DB, header *block.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, *types.CXReceipt, []staking.StakeMsg, uint64, error) {
txType := getTransactionType(config, header, tx)
if txType == types.InvalidTx {
return nil, nil, 0, errors.New("Invalid Transaction Type")
return nil, nil, nil, 0, errors.New("Invalid Transaction Type")
}
if txType != types.SameShardTx && !config.AcceptsCrossTx(header.Epoch()) {
return nil, nil, 0, errors.Errorf(
return nil, nil, nil, 0, errors.Errorf(
"cannot handle cross-shard transaction until after epoch %v (now %v)",
config.CrossTxEpoch, header.Epoch(),
)
@ -239,7 +246,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
var signer types.Signer
if tx.IsEthCompatible() {
if !config.IsEthCompatible(header.Epoch()) {
return nil, nil, 0, errors.New("ethereum compatible transactions not supported at current epoch")
return nil, nil, nil, 0, errors.New("ethereum compatible transactions not supported at current epoch")
}
signer = types.NewEIP155Signer(config.EthCompatibleChainID)
} else {
@ -249,7 +256,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
// skip signer err for additiononly tx
if err != nil {
return nil, nil, 0, err
return nil, nil, nil, 0, err
}
// Create a new context to be used in the EVM environment
@ -261,7 +268,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
// Apply the transaction to the current state (included in the env)
result, err := ApplyMessage(vmenv, msg, gp)
if err != nil {
return nil, nil, 0, err
return nil, nil, nil, 0, err
}
// Update the state with pending changes
var root []byte
@ -297,7 +304,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
cxReceipt = nil
}
return receipt, cxReceipt, result.UsedGas, err
return receipt, cxReceipt, vmenv.StakeMsgs, result.UsedGas, err
}
// ApplyStakingTransaction attempts to apply a staking transaction to the given state database

@ -17,21 +17,15 @@
package core
import (
"bytes"
"fmt"
"math"
"math/big"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/internal/utils"
staking2 "github.com/harmony-one/harmony/staking"
stakingReward "github.com/harmony-one/harmony/staking/reward"
staking "github.com/harmony-one/harmony/staking/types"
stakingTypes "github.com/harmony-one/harmony/staking/types"
"github.com/pkg/errors"
)
@ -132,45 +126,6 @@ func (result *ExecutionResult) Revert() []byte {
return common.CopyBytes(result.ReturnData)
}
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
func IntrinsicGas(data []byte, contractCreation, homestead, istanbul, isValidatorCreation bool) (uint64, error) {
// Set the starting gas for the raw transaction
var gas uint64
if contractCreation && homestead {
gas = params.TxGasContractCreation
} else if isValidatorCreation {
gas = params.TxGasValidatorCreation
} else {
gas = params.TxGas
}
// Bump the required gas by the amount of transactional data
if len(data) > 0 {
// Zero and non-zero bytes are priced differently
var nz uint64
for _, byt := range data {
if byt != 0 {
nz++
}
}
// Make sure we don't exceed uint64 for all data combinations
nonZeroGas := params.TxDataNonZeroGasFrontier
if istanbul {
nonZeroGas = params.TxDataNonZeroGasEIP2028
}
if (math.MaxUint64-gas)/nonZeroGas < nz {
return 0, vm.ErrOutOfGas
}
gas += nz * nonZeroGas
z := uint64(len(data)) - nz
if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
return 0, vm.ErrOutOfGas
}
gas += z * params.TxDataZeroGas
}
return gas, nil
}
// NewStateTransition initialises and returns a new state transition object.
func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool, bc ChainContext) *StateTransition {
return &StateTransition{
@ -264,7 +219,7 @@ func (st *StateTransition) TransitionDb() (ExecutionResult, error) {
contractCreation := msg.To() == nil
// Pay intrinsic gas
gas, err := IntrinsicGas(st.data, contractCreation, homestead, istanbul, false)
gas, err := vm.IntrinsicGas(st.data, contractCreation, homestead, istanbul, false)
if err != nil {
return ExecutionResult{}, err
}
@ -346,7 +301,7 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
istanbul := st.evm.ChainConfig().IsIstanbul(st.evm.EpochNumber)
// Pay intrinsic gas
gas, err := IntrinsicGas(st.data, false, homestead, istanbul, msg.Type() == types.StakeCreateVal)
gas, err := vm.IntrinsicGas(st.data, false, homestead, istanbul, msg.Type() == types.StakeCreateVal)
if err != nil {
return 0, err
@ -358,9 +313,13 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
// from worker.go, we get here with shardID == BeaconChainShardID
// from node_handler.go, via blockchain.go => it is checked that block shard == node shard
// same via consensus
// so only possible to reach here if shardID == BeaconChainShardID (no need to check further)
switch msg.Type() {
case types.StakeCreateVal:
stkMsg := &staking.CreateValidator{}
stkMsg := &stakingTypes.CreateValidator{}
if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil {
return 0, err
}
@ -369,9 +328,9 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
if msg.From() != stkMsg.ValidatorAddress {
return 0, errInvalidSigner
}
err = st.verifyAndApplyCreateValidatorTx(stkMsg, msg.BlockNum())
err = st.evm.CreateValidator(st.evm.StateDB, stkMsg)
case types.StakeEditVal:
stkMsg := &staking.EditValidator{}
stkMsg := &stakingTypes.EditValidator{}
if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil {
return 0, err
}
@ -380,9 +339,9 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
if msg.From() != stkMsg.ValidatorAddress {
return 0, errInvalidSigner
}
err = st.verifyAndApplyEditValidatorTx(stkMsg, msg.BlockNum())
err = st.evm.EditValidator(st.evm.StateDB, stkMsg)
case types.Delegate:
stkMsg := &staking.Delegate{}
stkMsg := &stakingTypes.Delegate{}
if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil {
return 0, err
}
@ -390,9 +349,9 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
if msg.From() != stkMsg.DelegatorAddress {
return 0, errInvalidSigner
}
err = st.verifyAndApplyDelegateTx(stkMsg)
err = st.evm.Delegate(st.evm.StateDB, stkMsg)
case types.Undelegate:
stkMsg := &staking.Undelegate{}
stkMsg := &stakingTypes.Undelegate{}
if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil {
return 0, err
}
@ -400,9 +359,9 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
if msg.From() != stkMsg.DelegatorAddress {
return 0, errInvalidSigner
}
err = st.verifyAndApplyUndelegateTx(stkMsg)
err = st.evm.Undelegate(st.evm.StateDB, stkMsg)
case types.CollectRewards:
stkMsg := &staking.CollectRewards{}
stkMsg := &stakingTypes.CollectRewards{}
if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil {
return 0, err
}
@ -410,9 +369,9 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
if msg.From() != stkMsg.DelegatorAddress {
return 0, errInvalidSigner
}
_, err = st.verifyAndApplyCollectRewards(stkMsg)
err = st.evm.CollectRewards(st.evm.StateDB, stkMsg)
default:
return 0, staking.ErrInvalidStakingKind
return 0, stakingTypes.ErrInvalidStakingKind
}
st.refundGas()
@ -422,131 +381,3 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) {
return st.gasUsed(), err
}
func (st *StateTransition) verifyAndApplyCreateValidatorTx(
createValidator *staking.CreateValidator, blockNum *big.Int,
) error {
wrapper, err := VerifyAndCreateValidatorFromMsg(
st.state, st.bc, st.evm.EpochNumber, blockNum, createValidator,
)
if err != nil {
return err
}
// since createValidator is not accessible to smart contracts
// it should not be reversible (to save resources)
// but it would be trivial to enable it later
if err := st.state.UpdateValidatorWrapper(wrapper.Address, wrapper); err != nil {
return err
}
st.state.SetValidatorFlag(createValidator.ValidatorAddress)
st.state.SubBalance(createValidator.ValidatorAddress, createValidator.Amount)
return nil
}
func (st *StateTransition) verifyAndApplyEditValidatorTx(
editValidator *staking.EditValidator, blockNum *big.Int,
) error {
wrapper, err := VerifyAndEditValidatorFromMsg(
st.state, st.bc, st.evm.EpochNumber, blockNum, editValidator,
)
if err != nil {
return err
}
// since editValidator is not accessible to smart contracts
// it should not be reversible (to save resources)
// but it would be trivial to enable it later
return st.state.UpdateValidatorWrapper(wrapper.Address, wrapper)
}
func (st *StateTransition) verifyAndApplyDelegateTx(delegate *staking.Delegate) error {
delegations, err := st.bc.ReadDelegationsByDelegator(delegate.DelegatorAddress)
if err != nil {
return err
}
updatedValidatorWrappers, balanceToBeDeducted, fromLockedTokens, err := VerifyAndDelegateFromMsg(
st.state, st.evm.EpochNumber, delegate, delegations, st.evm.ChainConfig())
if err != nil {
return err
}
for _, wrapper := range updatedValidatorWrappers {
if err := st.state.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil {
return err
}
}
st.state.SubBalance(delegate.DelegatorAddress, balanceToBeDeducted)
if len(fromLockedTokens) > 0 {
sortedKeys := []common.Address{}
for key := range fromLockedTokens {
sortedKeys = append(sortedKeys, key)
}
sort.SliceStable(sortedKeys, func(i, j int) bool {
return bytes.Compare(sortedKeys[i][:], sortedKeys[j][:]) < 0
})
// Add log if everything is good
for _, key := range sortedKeys {
redelegatedToken, ok := fromLockedTokens[key]
if !ok {
return errors.New("Key missing for delegation receipt")
}
encodedRedelegationData := []byte{}
addrBytes := key.Bytes()
encodedRedelegationData = append(encodedRedelegationData, addrBytes...)
encodedRedelegationData = append(encodedRedelegationData, redelegatedToken.Bytes()...)
// The data field format is:
// [first 20 bytes]: Validator address from which the locked token is used for redelegation.
// [rest of the bytes]: the bigInt serialized bytes for the token amount.
st.state.AddLog(&types.Log{
Address: delegate.DelegatorAddress,
Topics: []common.Hash{staking2.DelegateTopic},
Data: encodedRedelegationData,
BlockNumber: st.evm.BlockNumber.Uint64(),
})
}
}
return nil
}
func (st *StateTransition) verifyAndApplyUndelegateTx(
undelegate *staking.Undelegate,
) error {
wrapper, err := VerifyAndUndelegateFromMsg(st.state, st.evm.EpochNumber, undelegate)
if err != nil {
return err
}
return st.state.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper)
}
func (st *StateTransition) verifyAndApplyCollectRewards(collectRewards *staking.CollectRewards) (*big.Int, error) {
if st.bc == nil {
return stakingReward.None, errors.New("[CollectRewards] No chain context provided")
}
delegations, err := st.bc.ReadDelegationsByDelegator(collectRewards.DelegatorAddress)
if err != nil {
return stakingReward.None, err
}
updatedValidatorWrappers, totalRewards, err := VerifyAndCollectRewardsFromDelegation(
st.state, delegations,
)
if err != nil {
return stakingReward.None, err
}
for _, wrapper := range updatedValidatorWrappers {
if err := st.state.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil {
return stakingReward.None, err
}
}
st.state.AddBalance(collectRewards.DelegatorAddress, totalRewards)
// Add log if everything is good
st.state.AddLog(&types.Log{
Address: collectRewards.DelegatorAddress,
Topics: []common.Hash{staking2.CollectRewardsTopic},
Data: totalRewards.Bytes(),
BlockNumber: st.evm.BlockNumber.Uint64(),
})
return totalRewards, nil
}

@ -0,0 +1,83 @@
package core
import (
"crypto/ecdsa"
"fmt"
"math"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/internal/params"
staking "github.com/harmony-one/harmony/staking/types"
"github.com/pkg/errors"
)
type applyStakingMessageTest struct {
name string
tx *staking.StakingTransaction
expectedError error
}
var ApplyStakingMessageTests []applyStakingMessageTest
var key *ecdsa.PrivateKey
func init() {
key, _ = crypto.GenerateKey()
stakingValidatorMissing := errors.New("staking validator does not exist")
ApplyStakingMessageTests = []applyStakingMessageTest{
{
tx: signedCreateValidatorStakingTxn(key),
name: "ApplyStakingMessage_CreateValidator",
},
{
tx: signedEditValidatorStakingTxn(key),
expectedError: stakingValidatorMissing,
name: "ApplyStakingMessage_EditValidator",
},
{
tx: signedDelegateStakingTxn(key),
expectedError: stakingValidatorMissing,
name: "ApplyStakingMessage_Delegate",
},
{
tx: signedUndelegateStakingTxn(key),
expectedError: stakingValidatorMissing,
name: "ApplyStakingMessage_Undelegate",
},
{
tx: signedCollectRewardsStakingTxn(key),
expectedError: errors.New("no rewards to collect"),
name: "ApplyStakingMessage_CollectRewards",
},
}
}
func TestApplyStakingMessages(t *testing.T) {
for _, test := range ApplyStakingMessageTests {
testApplyStakingMessage(test, t)
}
}
func testApplyStakingMessage(test applyStakingMessageTest, t *testing.T) {
chain, db, header, _ := getTestEnvironment(*key)
gp := new(GasPool).AddGas(math.MaxUint64)
t.Run(fmt.Sprintf("%s", test.name), func(t *testing.T) {
// add a fake staking transaction
msg, _ := StakingToMessage(test.tx, header.Number())
// make EVM
ctx := NewEVMContext(msg, header, chain, nil /* coinbase */)
vmenv := vm.NewEVM(ctx, db, params.TestChainConfig, vm.Config{})
// run the staking tx
_, err := ApplyStakingMessage(vmenv, msg, gp, chain)
if err != nil {
if test.expectedError == nil {
t.Errorf(fmt.Sprintf("Got error %v but expected none", err))
} else if test.expectedError.Error() != err.Error() {
t.Errorf(fmt.Sprintf("Got error %v, but expected %v", err, test.expectedError))
}
}
})
}

@ -34,6 +34,7 @@ import (
"github.com/harmony-one/harmony/block"
"github.com/harmony-one/harmony/core/state"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm"
hmyCommon "github.com/harmony-one/harmony/internal/common"
"github.com/harmony-one/harmony/internal/utils"
"github.com/harmony-one/harmony/shard"
@ -747,9 +748,9 @@ func (pool *TxPool) validateTx(tx types.PoolTransaction, local bool) error {
}
intrGas := uint64(0)
if isStakingTx {
intrGas, err = IntrinsicGas(tx.Data(), false, pool.homestead, pool.istanbul, stakingTx.StakingType() == staking.DirectiveCreateValidator)
intrGas, err = vm.IntrinsicGas(tx.Data(), false, pool.homestead, pool.istanbul, stakingTx.StakingType() == staking.DirectiveCreateValidator)
} else {
intrGas, err = IntrinsicGas(tx.Data(), tx.To() == nil, pool.homestead, pool.istanbul, false)
intrGas, err = vm.IntrinsicGas(tx.Data(), tx.To() == nil, pool.homestead, pool.istanbul, false)
}
if err != nil {
return err

@ -21,6 +21,7 @@ import (
"github.com/harmony-one/harmony/core/state"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
// Validator is an interface which defines the standard for block validation. It
@ -58,7 +59,7 @@ type Validator interface {
// readCache decides whether the method will try reading from result cache.
type Processor interface {
Process(block *types.Block, statedb *state.DB, cfg vm.Config, readCache bool) (
types.Receipts, types.CXReceipts,
types.Receipts, types.CXReceipts, []stakingTypes.StakeMsg,
[]*types.Log, uint64, reward.Reader, *state.DB, error,
)
CacheProcessorResult(cacheKey interface{}, result *ProcessorResult)

@ -20,6 +20,7 @@ import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/crypto/blake2b"
@ -114,6 +115,44 @@ var PrecompiledContractsSHA3FIPS = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{254}): &ecrecoverPublicKey{},
}
// PrecompiledContractsStaking contains the default set of pre-compiled Ethereum
// contracts used in the Istanbul release. plus VRF, SHA3FIPS-202 and staking precompiles
// These are available in the EVM after the StakingPrecompileEpoch
var PrecompiledContractsStaking = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
common.BytesToAddress([]byte{4}): &dataCopy{},
common.BytesToAddress([]byte{5}): &bigModExp{},
common.BytesToAddress([]byte{6}): &bn256AddIstanbul{},
common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{},
common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{},
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{251}): &epoch{},
// marked nil to ensure no overwrite
common.BytesToAddress([]byte{252}): nil, // used by WriteCapablePrecompiledContractsStaking
common.BytesToAddress([]byte{253}): &sha3fip{},
common.BytesToAddress([]byte{254}): &ecrecoverPublicKey{},
common.BytesToAddress([]byte{255}): &vrf{},
}
func init() {
// check that there is no overlap, and panic if there is
readOnlyContracts := PrecompiledContractsStaking
writeCapableContracts := WriteCapablePrecompiledContractsStaking
for address, readOnlyContract := range readOnlyContracts {
if readOnlyContract != nil && writeCapableContracts[address] != nil {
panic(fmt.Errorf("Address %v is included in both readOnlyContracts and writeCapableContracts", address))
}
}
for address, writeCapableContract := range writeCapableContracts {
if writeCapableContract != nil && readOnlyContracts[address] != nil {
panic(fmt.Errorf("Address %v is included in both readOnlyContracts and writeCapableContracts", address))
}
}
}
// RunPrecompiledContract runs and evaluates the output of a precompiled contract.
func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract) (ret []byte, err error) {
gas := p.RequiredGas(input)
@ -540,6 +579,23 @@ func (c *blake2F) Run(input []byte) ([]byte, error) {
return output, nil
}
// epoch returns the current epoch, implemented as a native contract
type epoch struct{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func (c *epoch) RequiredGas(input []byte) uint64 {
return GasQuickStep
}
func (c *epoch) Run(input []byte) ([]byte, error) {
// Note the input was overwritten with the epoch of the current block
// So just format and return
return common.LeftPadBytes(input, 32), nil
}
// VRF implemented as a native contract.
type vrf struct{}

@ -0,0 +1,145 @@
package vm
import (
"errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/harmony-one/harmony/shard"
"github.com/harmony-one/harmony/staking"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
// WriteCapablePrecompiledContractsStaking lists out the write capable precompiled contracts
// which are available after the StakingPrecompileEpoch
// for now, we have only one contract at 252 or 0xfc - which is the staking precompile
var WriteCapablePrecompiledContractsStaking = map[common.Address]WriteCapablePrecompiledContract{
common.BytesToAddress([]byte{252}): &stakingPrecompile{},
}
// WriteCapablePrecompiledContract represents the interface for Native Go contracts
// which are available as a precompile in the EVM
// As with (read-only) PrecompiledContracts, these need a RequiredGas function
// Note that these contracts have the capability to alter the state
// while those in contracts.go do not
type WriteCapablePrecompiledContract interface {
// RequiredGas calculates the contract gas use
RequiredGas(evm *EVM, contract *Contract, input []byte) (uint64, error)
// use a different name from read-only contracts to be safe
RunWriteCapable(evm *EVM, contract *Contract, input []byte) ([]byte, error)
}
// RunWriteCapablePrecompiledContract runs and evaluates the output of a write capable precompiled contract.
func RunWriteCapablePrecompiledContract(
p WriteCapablePrecompiledContract,
evm *EVM,
contract *Contract,
input []byte,
readOnly bool,
) ([]byte, error) {
// immediately error out if readOnly
if readOnly {
return nil, errWriteProtection
}
gas, err := p.RequiredGas(evm, contract, input)
if err != nil {
return nil, err
}
if !contract.UseGas(gas) {
return nil, ErrOutOfGas
}
return p.RunWriteCapable(evm, contract, input)
}
type stakingPrecompile struct{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func (c *stakingPrecompile) RequiredGas(
evm *EVM,
contract *Contract,
input []byte,
) (uint64, error) {
// if invalid data or invalid shard
// set payload to blank and charge minimum gas
var payload []byte = make([]byte, 0)
// availability of staking and precompile has already been checked
if evm.Context.ShardID == shard.BeaconChainShardID {
// check that input is well formed
// meaning all the expected parameters are available
// and that we are only trying to perform staking tx
// on behalf of the correct entity
stakeMsg, err := staking.ParseStakeMsg(contract.Caller(), input)
if err == nil {
// otherwise charge similar to a regular staking tx
if migrationMsg, ok := stakeMsg.(*stakingTypes.MigrationMsg); ok {
// charge per delegation to migrate
return evm.CalculateMigrationGas(evm.StateDB,
migrationMsg,
evm.ChainConfig().IsS3(evm.EpochNumber),
evm.ChainConfig().IsIstanbul(evm.EpochNumber),
)
} else if encoded, err := rlp.EncodeToBytes(stakeMsg); err == nil {
payload = encoded
}
}
}
if gas, err := IntrinsicGas(
payload,
false, // contractCreation
evm.ChainConfig().IsS3(evm.EpochNumber), // homestead
evm.ChainConfig().IsIstanbul(evm.EpochNumber), // istanbul
false, // isValidatorCreation
); err != nil {
return 0, err // ErrOutOfGas occurs when gas payable > uint64
} else {
return gas, nil
}
}
// RunWriteCapable runs the actual contract (that is it performs the staking)
func (c *stakingPrecompile) RunWriteCapable(
evm *EVM,
contract *Contract,
input []byte,
) ([]byte, error) {
if evm.Context.ShardID != shard.BeaconChainShardID {
return nil, errors.New("Staking not supported on this shard")
}
stakeMsg, err := staking.ParseStakeMsg(contract.Caller(), input)
if err != nil {
return nil, err
}
if delegate, ok := stakeMsg.(*stakingTypes.Delegate); ok {
if err := evm.Delegate(evm.StateDB, delegate); err != nil {
return nil, err
} else {
evm.StakeMsgs = append(evm.StakeMsgs, delegate)
return nil, nil
}
}
if undelegate, ok := stakeMsg.(*stakingTypes.Undelegate); ok {
return nil, evm.Undelegate(evm.StateDB, undelegate)
}
if collectRewards, ok := stakeMsg.(*stakingTypes.CollectRewards); ok {
return nil, evm.CollectRewards(evm.StateDB, collectRewards)
}
if migrationMsg, ok := stakeMsg.(*stakingTypes.MigrationMsg); ok {
stakeMsgs, err := evm.MigrateDelegations(evm.StateDB, migrationMsg)
if err != nil {
return nil, err
} else {
for _, stakeMsg := range stakeMsgs {
if delegate, ok := stakeMsg.(*stakingTypes.Delegate); ok {
evm.StakeMsgs = append(evm.StakeMsgs, delegate)
} else {
return nil, errors.New("[StakingPrecompile] Received incompatible stakeMsg from evm.MigrateDelegations")
}
}
return nil, nil
}
}
return nil, errors.New("[StakingPrecompile] Received incompatible stakeMsg from staking.ParseStakeMsg")
}

@ -0,0 +1,213 @@
package vm
import (
"bytes"
"errors"
"fmt"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/internal/params"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
type writeCapablePrecompileTest struct {
input, expected []byte
name string
expectedError error
p *WriteCapablePrecompiledContract
}
func CollectRewardsFn() CollectRewardsFunc {
return func(db StateDB, collectRewards *stakingTypes.CollectRewards) error {
return nil
}
}
func DelegateFn() DelegateFunc {
return func(db StateDB, delegate *stakingTypes.Delegate) error {
return nil
}
}
func UndelegateFn() UndelegateFunc {
return func(db StateDB, undelegate *stakingTypes.Undelegate) error {
return nil
}
}
func CreateValidatorFn() CreateValidatorFunc {
return func(db StateDB, createValidator *stakingTypes.CreateValidator) error {
return nil
}
}
func EditValidatorFn() EditValidatorFunc {
return func(db StateDB, editValidator *stakingTypes.EditValidator) error {
return nil
}
}
func MigrateDelegationsFn() MigrateDelegationsFunc {
return func(db StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error) {
return nil, nil
}
}
func CalculateMigrationGasFn() CalculateMigrationGasFunc {
return func(db StateDB, migrationMsg *stakingTypes.MigrationMsg, homestead bool, istanbul bool) (uint64, error) {
return 0, nil
}
}
func testStakingPrecompile(test writeCapablePrecompileTest, t *testing.T) {
var env = NewEVM(Context{CollectRewards: CollectRewardsFn(),
Delegate: DelegateFn(),
Undelegate: UndelegateFn(),
CreateValidator: CreateValidatorFn(),
EditValidator: EditValidatorFn(),
ShardID: 0,
MigrateDelegations: MigrateDelegationsFn(),
CalculateMigrationGas: CalculateMigrationGasFn(),
}, nil, params.TestChainConfig, Config{})
// use required gas to avoid out of gas errors
p := &stakingPrecompile{}
t.Run(fmt.Sprintf("%s", test.name), func(t *testing.T) {
contract := NewContract(AccountRef(common.HexToAddress("1337")), AccountRef(common.HexToAddress("1338")), new(big.Int), 0)
gas, err := p.RequiredGas(env, contract, test.input)
if err != nil {
t.Error(err)
}
contract.Gas = gas
if res, err := RunWriteCapablePrecompiledContract(p, env, contract, test.input, false); err != nil {
if test.expectedError != nil {
if test.expectedError.Error() != err.Error() {
t.Errorf("Expected error %v, got %v", test.expectedError, err)
}
} else {
t.Error(err)
}
} else {
if test.expectedError != nil {
t.Errorf("Expected an error %v but instead got result %v", test.expectedError, res)
}
if bytes.Compare(res, test.expected) != 0 {
t.Errorf("Expected %v, got %v", test.expected, res)
}
}
})
}
func TestStakingPrecompiles(t *testing.T) {
for _, test := range StakingPrecompileTests {
testStakingPrecompile(test, t)
}
}
func TestWriteCapablePrecompilesReadOnly(t *testing.T) {
p := &stakingPrecompile{}
expectedError := errWriteProtection
res, err := RunWriteCapablePrecompiledContract(p, nil, nil, []byte{}, true)
if err != nil {
if err.Error() != expectedError.Error() {
t.Errorf("Expected error %v, got %v", expectedError, err)
}
} else {
t.Errorf("Expected an error %v but instead got result %v", expectedError, res)
}
}
var StakingPrecompileTests = []writeCapablePrecompileTest{
{
input: []byte{109, 107, 47, 120},
expectedError: errors.New("no method with id: 0x6d6b2f78"),
name: "badStakingKind",
},
{
input: []byte{0, 0},
expectedError: errors.New("data too short (2 bytes) for abi method lookup"),
name: "malformedInput",
},
{
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55},
expected: nil,
name: "collectRewardsSuccess",
},
{
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "collectRewardsAddressMismatch",
},
{
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 31 require 32"),
name: "collectRewardsInvalidABI",
},
{
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expected: nil,
name: "delegateSuccess",
},
{
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"),
name: "delegateInvalidABI",
},
{
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "delegateAddressMismatch",
},
{
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expected: nil,
name: "undelegateSuccess",
},
{
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"),
name: "undelegateInvalidABI",
},
{
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "undelegateAddressMismatch",
},
{
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "undelegateAddressMismatch",
},
{
input: []byte{42, 5, 187, 113},
expectedError: errors.New("abi: attempting to unmarshall an empty string while arguments are expected"),
name: "yesMethodNoData",
},
{
input: []byte{0, 0},
expectedError: errors.New("data too short (2 bytes) for abi method lookup"),
name: "malformedInput",
},
{
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56},
expected: nil,
name: "migrationSuccess",
},
{
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "migrationAddressMismatch",
},
{
input: []byte{42, 6, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55},
expectedError: errors.New("no method with id: 0x2a06bb71"),
name: "migrationNoMatchingMethod",
},
{
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 63 require 64"),
name: "migrationAddressMismatch",
},
}

@ -23,9 +23,9 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/internal/params"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
// emptyCodeHash is used by create to ensure deployment is disallowed to already
@ -45,12 +45,23 @@ type (
// GetVRFFunc returns the nth block vrf in the blockchain
// and is used by the precompile VRF contract.
GetVRFFunc func(uint64) common.Hash
// Below functions are used by staking precompile, and state transition
CreateValidatorFunc func(db StateDB, stakeMsg *stakingTypes.CreateValidator) error
EditValidatorFunc func(db StateDB, stakeMsg *stakingTypes.EditValidator) error
DelegateFunc func(db StateDB, stakeMsg *stakingTypes.Delegate) error
UndelegateFunc func(db StateDB, stakeMsg *stakingTypes.Undelegate) error
CollectRewardsFunc func(db StateDB, stakeMsg *stakingTypes.CollectRewards) error
// Used for migrating delegations via the staking precompile
MigrateDelegationsFunc func(db StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error)
CalculateMigrationGasFunc func(db StateDB, migrationMsg *stakingTypes.MigrationMsg, homestead bool, istanbul bool) (uint64, error)
)
// run runs the given contract and takes care of running precompiles with a fallback to the byte code interpreter.
func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, error) {
if contract.CodeAddr != nil {
precompiles := PrecompiledContractsHomestead
// assign empty write capable precompiles till they are available in the fork
writeCapablePrecompiles := make(map[common.Address]WriteCapablePrecompiledContract)
if evm.ChainConfig().IsS3(evm.EpochNumber) {
precompiles = PrecompiledContractsByzantium
}
@ -63,6 +74,10 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err
if evm.chainRules.IsSHA3 {
precompiles = PrecompiledContractsSHA3FIPS
}
if evm.chainRules.IsStakingPrecompile {
precompiles = PrecompiledContractsStaking
writeCapablePrecompiles = WriteCapablePrecompiledContractsStaking
}
if p := precompiles[*contract.CodeAddr]; p != nil {
if _, ok := p.(*vrf); ok {
if evm.chainRules.IsPrevVRF {
@ -82,10 +97,14 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err
// Override the input with vrf data of the requested block so it can be returned to the contract program.
input = evm.Context.VRF.Bytes()
}
} else if _, ok := p.(*epoch); ok {
input = evm.EpochNumber.Bytes()
}
return RunPrecompiledContract(p, input, contract)
}
if p := writeCapablePrecompiles[*contract.CodeAddr]; p != nil {
return RunWriteCapablePrecompiledContract(p, evm, contract, input, readOnly)
}
}
for _, interpreter := range evm.interpreters {
if interpreter.CanRun(contract.Code) {
@ -138,6 +157,17 @@ type Context struct {
VRF common.Hash // Provides information for VRF
TxType types.TransactionType
CreateValidator CreateValidatorFunc
EditValidator EditValidatorFunc
Delegate DelegateFunc
Undelegate UndelegateFunc
CollectRewards CollectRewardsFunc
MigrateDelegations MigrateDelegationsFunc
CalculateMigrationGas CalculateMigrationGasFunc
// staking precompile checks this before proceeding forward
ShardID uint32
}
// EVM is the Ethereum Virtual Machine base object and provides
@ -175,6 +205,9 @@ type EVM struct {
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
// stored temporarily by stakingPrecompile and cleared immediately after return
// (although the EVM object itself is ephemeral)
StakeMsgs []stakingTypes.StakeMsg
}
// NewEVM returns a new EVM. The returned EVM is not thread safe and should
@ -256,6 +289,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
)
if !evm.StateDB.Exist(addr) && txType != types.SubtractionOnly {
precompiles := PrecompiledContractsHomestead
writeCapablePrecompiles := make(map[common.Address]WriteCapablePrecompiledContract)
if evm.ChainConfig().IsS3(evm.EpochNumber) {
precompiles = PrecompiledContractsByzantium
}
@ -268,8 +302,12 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
if evm.chainRules.IsSHA3 {
precompiles = PrecompiledContractsSHA3FIPS
}
if evm.chainRules.IsStakingPrecompile {
precompiles = PrecompiledContractsStaking
writeCapablePrecompiles = WriteCapablePrecompiledContractsStaking
}
if precompiles[addr] == nil && evm.ChainConfig().IsS3(evm.EpochNumber) && value.Sign() == 0 {
if writeCapablePrecompiles[addr] == nil && precompiles[addr] == nil && evm.ChainConfig().IsS3(evm.EpochNumber) && value.Sign() == 0 {
// Calling a non existing account, don't do anything, but ping the tracer
if evm.vmConfig.Debug && evm.depth == 0 {
evm.vmConfig.Tracer.CaptureStart(evm, caller.Address(), addr, false, input, gas, value)

@ -0,0 +1,35 @@
package vm
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/internal/params"
)
// this test is here so we can cover the input = epoch.bytes() line as well
func TestEpochPrecompile(t *testing.T) {
targetEpoch := big.NewInt(1)
evm := NewEVM(Context{
EpochNumber: targetEpoch,
}, nil, params.TestChainConfig, Config{})
input := []byte{}
precompileAddr := common.BytesToAddress([]byte{251})
contract := Contract{
CodeAddr: &precompileAddr,
Gas: GasQuickStep,
}
result, err := run(evm,
&contract,
input,
true,
)
if err != nil {
t.Fatalf("Got error%v\n", err)
}
resultingEpoch := new(big.Int).SetBytes(result)
if resultingEpoch.Cmp(targetEpoch) != 0 {
t.Error("Epoch did not match")
}
}

@ -17,7 +17,10 @@
package vm
import (
"math"
"math/big"
"github.com/harmony-one/harmony/internal/params"
)
// Gas costs
@ -51,3 +54,42 @@ func callGas(isEip150 bool, availableGas, base uint64, callCost *big.Int) (uint6
return callCost.Uint64(), nil
}
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
func IntrinsicGas(data []byte, contractCreation, homestead, istanbul, isValidatorCreation bool) (uint64, error) {
// Set the starting gas for the raw transaction
var gas uint64
if contractCreation && homestead {
gas = params.TxGasContractCreation
} else if isValidatorCreation {
gas = params.TxGasValidatorCreation
} else {
gas = params.TxGas
}
// Bump the required gas by the amount of transactional data
if len(data) > 0 {
// Zero and non-zero bytes are priced differently
var nz uint64
for _, byt := range data {
if byt != 0 {
nz++
}
}
// Make sure we don't exceed uint64 for all data combinations
nonZeroGas := params.TxDataNonZeroGasFrontier
if istanbul {
nonZeroGas = params.TxDataNonZeroGasEIP2028
}
if (math.MaxUint64-gas)/nonZeroGas < nz {
return 0, ErrOutOfGas
}
gas += nz * nonZeroGas
z := uint64(len(data)) - nz
if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
return 0, ErrOutOfGas
}
gas += z * params.TxDataZeroGas
}
return gas, nil
}

@ -280,7 +280,7 @@ func (hmy *Harmony) TraceChain(ctx context.Context, start, end *types.Block, con
traced += uint64(len(txs))
}
// Generate the next state snapshot fast without tracing
_, _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}, false)
_, _, _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}, false)
if err != nil {
failed = err
break
@ -674,7 +674,7 @@ func (hmy *Harmony) ComputeStateDB(block *types.Block, reexec uint64) (*state.DB
if block = hmy.BlockChain.GetBlockByNumber(block.NumberU64() + 1); block == nil {
return nil, fmt.Errorf("block #%d not found", block.NumberU64()+1)
}
_, _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}, false)
_, _, _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}, false)
if err != nil {
return nil, fmt.Errorf("processing block %d failed: %v", block.NumberU64(), err)
}

@ -64,6 +64,7 @@ var (
ReceiptLogEpoch: big.NewInt(101),
SHA3Epoch: big.NewInt(725), // Around Mon Oct 11 2021, 19:00 UTC
HIP6And8Epoch: big.NewInt(725), // Around Mon Oct 11 2021, 19:00 UTC
StakingPrecompileEpoch: EpochTBD,
}
// TestnetChainConfig contains the chain parameters to run a node on the harmony test network.
@ -96,6 +97,7 @@ var (
ReceiptLogEpoch: big.NewInt(0),
SHA3Epoch: big.NewInt(74570),
HIP6And8Epoch: big.NewInt(74570),
StakingPrecompileEpoch: EpochTBD,
}
// PangaeaChainConfig contains the chain parameters for the Pangaea network.
@ -129,6 +131,7 @@ var (
ReceiptLogEpoch: big.NewInt(0),
SHA3Epoch: big.NewInt(0),
HIP6And8Epoch: big.NewInt(0),
StakingPrecompileEpoch: big.NewInt(2), // same as staking
}
// PartnerChainConfig contains the chain parameters for the Partner network.
@ -162,6 +165,7 @@ var (
ReceiptLogEpoch: big.NewInt(0),
SHA3Epoch: big.NewInt(0),
HIP6And8Epoch: big.NewInt(0),
StakingPrecompileEpoch: big.NewInt(2),
}
// StressnetChainConfig contains the chain parameters for the Stress test network.
@ -195,6 +199,7 @@ var (
ReceiptLogEpoch: big.NewInt(0),
SHA3Epoch: big.NewInt(0),
HIP6And8Epoch: big.NewInt(0),
StakingPrecompileEpoch: big.NewInt(2),
}
// LocalnetChainConfig contains the chain parameters to run for local development.
@ -227,6 +232,7 @@ var (
ReceiptLogEpoch: big.NewInt(0),
SHA3Epoch: big.NewInt(0),
HIP6And8Epoch: EpochTBD, // Never enable it for localnet as localnet has no external validator setup
StakingPrecompileEpoch: big.NewInt(2),
}
// AllProtocolChanges ...
@ -261,6 +267,7 @@ var (
big.NewInt(0), // ReceiptLogEpoch
big.NewInt(0), // SHA3Epoch
big.NewInt(0), // HIP6And8Epoch
big.NewInt(0), // StakingPrecompileEpoch
}
// TestChainConfig ...
@ -295,6 +302,7 @@ var (
big.NewInt(0), // ReceiptLogEpoch
big.NewInt(0), // SHA3Epoch
big.NewInt(0), // HIP6And8Epoch
big.NewInt(0), // StakingPrecompileEpoch
}
// TestRules ...
@ -409,11 +417,14 @@ type ChainConfig struct {
// IsHIP6And8Epoch is the first epoch to support HIP-6 and HIP-8
HIP6And8Epoch *big.Int `json:"hip6_8-epoch,omitempty"`
// StakingPrecompileEpoch is the first epoch to support the staking precompiles
StakingPrecompileEpoch *big.Int `json:"staking-precompile-epoch,omitempty"`
}
// String implements the fmt.Stringer interface.
func (c *ChainConfig) String() string {
return fmt.Sprintf("{ChainID: %v EthCompatibleChainID: %v EIP155: %v CrossTx: %v Staking: %v CrossLink: %v ReceiptLog: %v SHA3Epoch:%v}",
return fmt.Sprintf("{ChainID: %v EthCompatibleChainID: %v EIP155: %v CrossTx: %v Staking: %v CrossLink: %v ReceiptLog: %v SHA3Epoch: %v StakingPrecompileEpoch: %v}",
c.ChainID,
c.EthCompatibleChainID,
c.EIP155Epoch,
@ -422,6 +433,7 @@ func (c *ChainConfig) String() string {
c.CrossLinkEpoch,
c.ReceiptLogEpoch,
c.SHA3Epoch,
c.StakingPrecompileEpoch,
)
}
@ -562,6 +574,12 @@ func (c *ChainConfig) IsHIP6And8Epoch(epoch *big.Int) bool {
return isForked(c.HIP6And8Epoch, epoch)
}
// IsStakingPrecompileEpoch determines whether staking
// precompiles are available in the EVM
func (c *ChainConfig) IsStakingPrecompile(epoch *big.Int) bool {
return isForked(c.StakingPrecompileEpoch, epoch)
}
// UpdateEthChainIDByShard update the ethChainID based on shard ID.
func UpdateEthChainIDByShard(shardID uint32) {
once.Do(func() {
@ -612,11 +630,16 @@ func isForked(s, epoch *big.Int) bool {
type Rules struct {
ChainID *big.Int
EthChainID *big.Int
IsCrossLink, IsEIP155, IsS3, IsReceiptLog, IsIstanbul, IsVRF, IsPrevVRF, IsSHA3 bool
IsCrossLink, IsEIP155, IsS3, IsReceiptLog, IsIstanbul, IsVRF, IsPrevVRF, IsSHA3, IsStakingPrecompile bool
}
// Rules ensures c's ChainID is not nil.
func (c *ChainConfig) Rules(epoch *big.Int) Rules {
if c.IsStakingPrecompile(epoch) {
if !c.IsPreStaking(epoch) {
panic("Cannot have staking precompile epoch if not prestaking epoch")
}
}
chainID := c.ChainID
if chainID == nil {
chainID = new(big.Int)
@ -636,5 +659,6 @@ func (c *ChainConfig) Rules(epoch *big.Int) Rules {
IsVRF: c.IsVRF(epoch),
IsPrevVRF: c.IsPrevVRF(epoch),
IsSHA3: c.IsSHA3(epoch),
IsStakingPrecompile: c.IsStakingPrecompile(epoch),
}
}

@ -48,6 +48,7 @@ type environment struct {
outcxs []*types.CXReceipt // cross shard transaction receipts (source shard)
incxs []*types.CXReceiptsProof // cross shard receipts and its proof (desitinatin shard)
slashes slash.Records
stakeMsgs []staking.StakeMsg
}
// Worker is the main object which takes care of submitting new work to consensus engine
@ -225,7 +226,7 @@ func (w *Worker) commitTransaction(
) error {
snap := w.current.state.Snapshot()
gasUsed := w.current.header.GasUsed()
receipt, cx, _, err := core.ApplyTransaction(
receipt, cx, stakeMsgs, _, err := core.ApplyTransaction(
w.config,
w.chain,
&coinbase,
@ -252,6 +253,7 @@ func (w *Worker) commitTransaction(
w.current.txs = append(w.current.txs, tx)
w.current.receipts = append(w.current.receipts, receipt)
w.current.logs = append(w.current.logs, receipt.Logs...)
w.current.stakeMsgs = append(w.current.stakeMsgs, stakeMsgs...)
if cx != nil {
w.current.outcxs = append(w.current.outcxs, cx)
@ -332,6 +334,7 @@ func (w *Worker) GetCurrentResult() *core.ProcessorResult {
UsedGas: w.current.header.GasUsed(),
Reward: w.current.reward,
State: w.current.state,
StakeMsgs: w.current.stakeMsgs,
}
}

@ -11,7 +11,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
"github.com/harmony-one/harmony/core"
"github.com/harmony-one/harmony/core/vm"
ethRpc "github.com/harmony-one/harmony/eth/rpc"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/rosetta/common"
@ -239,7 +239,7 @@ func (s *ConstructAPI) ConstructionMetadata(
)
}
} else {
estGasUsed, err = core.IntrinsicGas(data, false, false,
estGasUsed, err = vm.IntrinsicGas(data, false, false,
false, options.OperationType == common.CreateValidatorOperation)
estGasUsed *= 2

@ -0,0 +1,233 @@
package staking
import (
"bytes"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/accounts/abi"
stakingTypes "github.com/harmony-one/harmony/staking/types"
"github.com/pkg/errors"
)
var abiStaking abi.ABI
func init() {
// for commission rates => solidity does not support floats directly
// so send commission rates as string
StakingABIJSON := `
[
{
"inputs": [
{
"internalType": "address",
"name": "delegatorAddress",
"type": "address"
}
],
"name": "CollectRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "delegatorAddress",
"type": "address"
},
{
"internalType": "address",
"name": "validatorAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Delegate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "delegatorAddress",
"type": "address"
},
{
"internalType": "address",
"name": "validatorAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Undelegate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "Migrate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
`
abiStaking, _ = abi.JSON(strings.NewReader(StakingABIJSON))
}
// contractCaller (and not Contract) is used here to avoid import cycle
func ParseStakeMsg(contractCaller common.Address, input []byte) (interface{}, error) {
method, err := abiStaking.MethodById(input)
if err != nil {
return nil, err
}
input = input[4:] // drop the method selector
args := map[string]interface{}{} // store into map
if err = method.Inputs.UnpackIntoMap(args, input); err != nil {
return nil, err
}
switch method.Name {
case "Delegate":
{
// in case of assembly call, a contract will delegate its own balance
// in case of assembly delegatecall, contract.Caller() is msg.sender
// which means an EOA can
// (1) deploy a contract which receives delegations and amounts
// (2) call the contract, which then performs the tx on behalf of the EOA
address, err := ValidateContractAddress(contractCaller, args, "delegatorAddress")
if err != nil {
return nil, err
}
validatorAddress, err := ParseAddressFromKey(args, "validatorAddress")
if err != nil {
return nil, err
}
amount, err := ParseBigIntFromKey(args, "amount")
if err != nil {
return nil, err
}
stakeMsg := &stakingTypes.Delegate{
DelegatorAddress: address,
ValidatorAddress: validatorAddress,
Amount: amount,
}
return stakeMsg, nil
}
case "Undelegate":
{
// same validation as above
address, err := ValidateContractAddress(contractCaller, args, "delegatorAddress")
if err != nil {
return nil, err
}
validatorAddress, err := ParseAddressFromKey(args, "validatorAddress")
if err != nil {
return nil, err
}
// this type assertion is needed by Golang
amount, err := ParseBigIntFromKey(args, "amount")
if err != nil {
return nil, err
}
stakeMsg := &stakingTypes.Undelegate{
DelegatorAddress: address,
ValidatorAddress: validatorAddress,
Amount: amount,
}
return stakeMsg, nil
}
case "CollectRewards":
{
// same validation as above
address, err := ValidateContractAddress(contractCaller, args, "delegatorAddress")
if err != nil {
return nil, err
}
stakeMsg := &stakingTypes.CollectRewards{
DelegatorAddress: address,
}
return stakeMsg, nil
}
case "Migrate":
{
from, err := ValidateContractAddress(contractCaller, args, "from")
if err != nil {
return nil, err
}
to, err := ParseAddressFromKey(args, "to")
if err != nil {
return nil, err
}
// no sanity check for migrating to same address, just do nothing
return &stakingTypes.MigrationMsg{
From: from,
To: to,
}, nil
}
default:
{
return nil, errors.New("[StakingPrecompile] Invalid method name from ABI selector")
}
}
}
// used to ensure caller == delegatorAddress
func ValidateContractAddress(contractCaller common.Address, args map[string]interface{}, key string) (common.Address, error) {
address, err := ParseAddressFromKey(args, key)
if err != nil {
return common.Address{}, err
}
if !bytes.Equal(contractCaller.Bytes(), address.Bytes()) {
return common.Address{}, errors.Errorf(
"[StakingPrecompile] Address mismatch, expected %s have %s",
contractCaller.String(), address.String(),
)
} else {
return address, nil
}
}
// used for both delegatorAddress and validatorAddress
func ParseAddressFromKey(args map[string]interface{}, key string) (common.Address, error) {
if address, ok := args[key].(common.Address); ok {
return address, nil
} else {
return common.Address{}, errors.Errorf("Cannot parse address from %v", args[key])
}
}
// used for amounts
func ParseBigIntFromKey(args map[string]interface{}, key string) (*big.Int, error) {
bigInt, ok := args[key].(*big.Int)
if !ok {
return nil, errors.Errorf(
"Cannot parse BigInt from %v", args[key])
} else {
return bigInt, nil
}
}

@ -0,0 +1,203 @@
package staking
import (
"errors"
"fmt"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/common/denominations"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
func TestValidateContractAddress(t *testing.T) {
input := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55}
args := map[string]interface{}{}
expectedError := errors.New("Cannot parse address from <nil>")
if _, err := ValidateContractAddress(common.BytesToAddress(input), args, "ValidatorAddress"); err != nil {
if expectedError.Error() != err.Error() {
t.Errorf("Expected error %v, got %v", expectedError, err)
}
} else {
t.Errorf("Expected error %v, got result", expectedError)
}
}
func TestParseBigIntFromKey(t *testing.T) {
args := map[string]interface{}{}
expectedError := errors.New("Cannot parse BigInt from <nil>")
if _, err := ParseBigIntFromKey(args, "PotentialBigInt"); err != nil {
if expectedError.Error() != err.Error() {
t.Errorf("Expected error %v, got %v", expectedError, err)
}
} else {
t.Errorf("Expected error %v, got result", expectedError)
}
}
type parseTest struct {
input []byte
name string
expectedError error
expected interface{}
}
var ParseStakeMsgTests = []parseTest{
{
input: []byte{109, 107, 47, 120},
expectedError: errors.New("no method with id: 0x6d6b2f78"),
name: "badStakingKind",
},
{
input: []byte{0, 0},
expectedError: errors.New("data too short (2 bytes) for abi method lookup"),
name: "malformedInput",
},
{
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55},
expected: &stakingTypes.CollectRewards{DelegatorAddress: common.HexToAddress("0x1337")},
name: "collectRewardsSuccess",
},
{
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "collectRewardsAddressMismatch",
},
{
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 31 require 32"),
name: "collectRewardsInvalidABI",
},
{
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expected: &stakingTypes.Delegate{
DelegatorAddress: common.HexToAddress("0x1337"),
ValidatorAddress: common.HexToAddress("0x1338"),
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(100)),
},
name: "delegateSuccess",
},
{
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"),
name: "delegateInvalidABI",
},
{
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "delegateAddressMismatch",
},
{
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expected: &stakingTypes.Undelegate{
DelegatorAddress: common.HexToAddress("0x1337"),
ValidatorAddress: common.HexToAddress("0x1338"),
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(100)),
},
name: "undelegateSuccess",
},
{
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"),
name: "undelegateInvalidABI",
},
{
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "undelegateAddressMismatch",
},
{
input: []byte{42, 5, 187, 113},
expectedError: errors.New("abi: attempting to unmarshall an empty string while arguments are expected"),
name: "yesMethodNoData",
},
{
input: []byte{0, 0},
expectedError: errors.New("data too short (2 bytes) for abi method lookup"),
name: "malformedInput",
},
{
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56},
expected: &stakingTypes.MigrationMsg{
From: common.HexToAddress("0x1337"),
To: common.HexToAddress("0x1338"),
},
name: "migrationSuccess",
},
{
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55},
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"),
name: "migrationAddressMismatch",
},
{
input: []byte{42, 6, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55},
expectedError: errors.New("no method with id: 0x2a06bb71"),
name: "migrationNoMatchingMethod",
},
{
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19},
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 63 require 64"),
name: "migrationAddressMismatch",
},
}
func testParseStakeMsg(test parseTest, t *testing.T) {
t.Run(fmt.Sprintf("%s", test.name), func(t *testing.T) {
if res, err := ParseStakeMsg(common.HexToAddress("1337"), test.input); err != nil {
if test.expectedError != nil {
if test.expectedError.Error() != err.Error() {
t.Errorf("Expected error %v, got %v", test.expectedError, err)
}
} else {
t.Error(err)
}
} else {
if test.expectedError != nil {
t.Errorf("Expected an error %v but instead got result %v", test.expectedError, res)
}
if test.expected != nil {
if converted, ok := res.(*stakingTypes.Delegate); ok {
convertedExp, ok := test.expected.(*stakingTypes.Delegate)
if !ok {
t.Errorf("Could not converted test.expected to *stakingTypes.Delegate")
} else if !converted.Equals(*convertedExp) {
t.Errorf("Expected %+v but got %+v", test.expected, converted)
}
} else if converted, ok := res.(*stakingTypes.Undelegate); ok {
convertedExp, ok := test.expected.(*stakingTypes.Undelegate)
if !ok {
t.Errorf("Could not converted test.expected to *stakingTypes.Undelegate")
} else if !converted.Equals(*convertedExp) {
t.Errorf("Expected %+v but got %+v", test.expected, converted)
}
} else if converted, ok := res.(*stakingTypes.CollectRewards); ok {
convertedExp, ok := test.expected.(*stakingTypes.CollectRewards)
if !ok {
t.Errorf("Could not converted test.expected to *stakingTypes.CollectRewards")
} else if !converted.Equals(*convertedExp) {
t.Errorf("Expected %+v but got %+v", test.expected, converted)
}
} else if converted, ok := res.(*stakingTypes.MigrationMsg); ok {
convertedExp, ok := test.expected.(*stakingTypes.MigrationMsg)
if !ok {
t.Errorf("Could not converted test.expected to *stakingTypes.MigrationMsg")
} else if !converted.Equals(*convertedExp) {
t.Errorf("Expected %+v but got %+v", test.expected, converted)
}
} else {
panic("Received unexpected result from ParseStakeMsg")
}
} else if res != nil {
t.Errorf("Expected nil, got %v", res)
}
}
})
}
func TestParseStakeMsgs(t *testing.T) {
for _, test := range ParseStakeMsgTests {
testParseStakeMsg(test, t)
}
}

@ -1,6 +1,7 @@
package types
import (
"bytes"
"fmt"
"math/big"
@ -172,6 +173,20 @@ func (v Delegate) Copy() StakeMsg {
return cp
}
// Equals returns if v and s are equal
func (v Delegate) Equals(s Delegate) bool {
if !bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) {
return false
}
if !bytes.Equal(v.ValidatorAddress.Bytes(), s.ValidatorAddress.Bytes()) {
return false
}
if v.Amount == nil {
return s.Amount == nil
}
return s.Amount != nil && v.Amount.Cmp(s.Amount) == 0 // pointer
}
// Undelegate - type for removing delegation responsibility
type Undelegate struct {
DelegatorAddress common.Address `json:"delegator_address"`
@ -196,6 +211,20 @@ func (v Undelegate) Copy() StakeMsg {
return cp
}
// Equals returns if v and s are equal
func (v Undelegate) Equals(s Undelegate) bool {
if !bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) {
return false
}
if !bytes.Equal(v.ValidatorAddress.Bytes(), s.ValidatorAddress.Bytes()) {
return false
}
if v.Amount == nil {
return s.Amount == nil
}
return s.Amount != nil && v.Amount.Cmp(s.Amount) == 0 // pointer
}
// CollectRewards - type for collecting token rewards
type CollectRewards struct {
DelegatorAddress common.Address `json:"delegator_address"`
@ -212,3 +241,25 @@ func (v CollectRewards) Copy() StakeMsg {
DelegatorAddress: v.DelegatorAddress,
}
}
// Equals returns if v and s are equal
func (v CollectRewards) Equals(s CollectRewards) bool {
return bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes())
}
// Migration Msg - type for switching delegation from one user to next
type MigrationMsg struct {
From common.Address `json:"from" rlp:"nil"`
To common.Address `json:"to" rlp:"nil"`
}
func (v MigrationMsg) Copy() MigrationMsg {
return MigrationMsg{
From: v.From,
To: v.To,
}
}
func (v MigrationMsg) Equals(s MigrationMsg) bool {
return v.From == s.From && v.To == s.To
}

@ -32,7 +32,7 @@ func checkValidatorWrapperEqual(w1, w2 staking.ValidatorWrapper) error {
if err := checkValidatorEqual(w1.Validator, w2.Validator); err != nil {
return fmt.Errorf(".Validator%v", err)
}
if err := checkDelegationsEqual(w1.Delegations, w2.Delegations); err != nil {
if err := CheckDelegationsEqual(w1.Delegations, w2.Delegations); err != nil {
return fmt.Errorf(".Delegations%v", err)
}
if err := checkBigIntEqual(w1.Counters.NumBlocksToSign, w2.Counters.NumBlocksToSign); err != nil {
@ -78,7 +78,7 @@ func checkValidatorEqual(v1, v2 staking.Validator) error {
return nil
}
func checkDelegationsEqual(ds1, ds2 staking.Delegations) error {
func CheckDelegationsEqual(ds1, ds2 staking.Delegations) error {
if len(ds1) != len(ds2) {
return fmt.Errorf(".len not equal: %v / %v", len(ds1), len(ds2))
}

Loading…
Cancel
Save