From d500c4ede6cea3f8d6535f96abe4ba21a22108f3 Mon Sep 17 00:00:00 2001 From: Max <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 21 Jan 2022 01:45:06 +0000 Subject: [PATCH] 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 --- core/blockchain.go | 78 ++-- core/blockchain_test.go | 85 +++++ core/chain_makers.go | 2 +- core/evm.go | 282 ++++++++++++++- core/evm_test.go | 478 +++++++++++++++++++++++++ core/offchain.go | 3 +- core/staking_verifier.go | 117 ++++++ core/staking_verifier_test.go | 9 + core/state_processor.go | 55 +-- core/state_transition.go | 205 +---------- core/state_transition_test.go | 83 +++++ core/tx_pool.go | 5 +- core/types.go | 3 +- core/vm/contracts.go | 56 +++ core/vm/contracts_write.go | 145 ++++++++ core/vm/contracts_write_test.go | 213 +++++++++++ core/vm/evm.go | 46 ++- core/vm/evm_test.go | 35 ++ core/vm/gas.go | 42 +++ hmy/tracer.go | 4 +- internal/params/config.go | 52 ++- node/worker/worker.go | 5 +- rosetta/services/construction_check.go | 4 +- staking/precompile.go | 233 ++++++++++++ staking/precompile_test.go | 203 +++++++++++ staking/types/messages.go | 51 +++ staking/types/test/equal.go | 4 +- 27 files changed, 2215 insertions(+), 283 deletions(-) create mode 100644 core/blockchain_test.go create mode 100644 core/evm_test.go create mode 100644 core/state_transition_test.go create mode 100644 core/vm/contracts_write.go create mode 100644 core/vm/contracts_write_test.go create mode 100644 core/vm/evm_test.go create mode 100644 staking/precompile.go create mode 100644 staking/precompile_test.go diff --git a/core/blockchain.go b/core/blockchain.go index af88cc960..c0557c68a 100644 --- a/core/blockchain.go +++ b/core/blockchain.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,27 +2817,19 @@ 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) - - 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 - } - } - if delegations, err = bc.addDelegationIndex( - delegations, delegate.DelegatorAddress, delegate.ValidatorAddress, state, blockNum, - ); err != nil { + if err := processDelegateMetadata(delegate, + newDelegations, + state, + bc, + blockNum); err != nil { return nil, nil, err } - newDelegations[delegate.DelegatorAddress] = delegations + case staking.DirectiveUndelegate: case staking.DirectiveCollectRewards: default: @@ -2830,6 +2839,27 @@ func (bc *BlockChain) prepareStakingMetaData( 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 err + } + } + if delegations, err = bc.addDelegationIndex( + delegations, delegate.DelegatorAddress, delegate.ValidatorAddress, state, blockNum, + ); err != nil { + return err + } + newDelegations[delegate.DelegatorAddress] = delegations + return nil +} + // ReadBlockRewardAccumulator must only be called on beaconchain // Note that block rewards are only for staking era. func (bc *BlockChain) ReadBlockRewardAccumulator(number uint64) (*big.Int, error) { diff --git a/core/blockchain_test.go b/core/blockchain_test.go new file mode 100644 index 000000000..ce3113f26 --- /dev/null +++ b/core/blockchain_test.go @@ -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 +} diff --git a/core/chain_makers.go b/core/chain_makers.go index 0db67ccdb..4d2ac483a 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -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 { diff --git a/core/evm.go b/core/evm.go index f4360253b..d9f524239 100644 --- a/core/evm.go +++ b/core/evm.go @@ -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. @@ -66,19 +74,257 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author copy(vrf[:], vrfAndProof[:32]) } return vm.Context{ - CanTransfer: CanTransfer, - Transfer: Transfer, - IsValidator: IsValidator, - GetHash: GetHashFn(header, chain), - GetVRF: GetVRFFn(header, chain), - Origin: msg.From(), - Coinbase: beneficiary, - BlockNumber: header.Number(), - EpochNumber: header.Epoch(), - VRF: vrf, - Time: header.Time(), - GasLimit: header.GasLimit(), - GasPrice: new(big.Int).Set(msg.GasPrice()), + CanTransfer: CanTransfer, + Transfer: Transfer, + 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(), + EpochNumber: header.Epoch(), + VRF: vrf, + 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 + ) + } } } diff --git a/core/evm_test.go b/core/evm_test.go new file mode 100644 index 000000000..9980c9c66 --- /dev/null +++ b/core/evm_test.go @@ -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)) + } +} diff --git a/core/offchain.go b/core/offchain.go index d97f64255..220014d75 100644 --- a/core/offchain.go +++ b/core/offchain.go @@ -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") diff --git a/core/staking_verifier.go b/core/staking_verifier.go index 491ae73c2..541d26f4e 100644 --- a/core/staking_verifier.go +++ b/core/staking_verifier.go @@ -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. diff --git a/core/staking_verifier_test.go b/core/staking_verifier_test.go index 024d9d099..b356f37fe 100644 --- a/core/staking_verifier_test.go +++ b/core/staking_verifier_test.go @@ -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") } diff --git a/core/state_processor.go b/core/state_processor.go index dafe81cd9..4fd6dade0 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -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,39 +98,43 @@ 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 } } var ( - receipts types.Receipts - outcxs types.CXReceipts - incxs = block.IncomingReceipts() - usedGas = new(uint64) - header = block.Header() - allLogs []*types.Log - gp = new(GasPool).AddGas(block.GasLimit()) + receipts types.Receipts + outcxs types.CXReceipts + incxs = block.IncomingReceipts() + usedGas = new(uint64) + 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 diff --git a/core/state_transition.go b/core/state_transition.go index b8b087661..a566f435a 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -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 -} diff --git a/core/state_transition_test.go b/core/state_transition_test.go new file mode 100644 index 000000000..43b592c0d --- /dev/null +++ b/core/state_transition_test.go @@ -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)) + } + } + }) +} diff --git a/core/tx_pool.go b/core/tx_pool.go index 2ee7ad90d..ec0e61e58 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -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 diff --git a/core/types.go b/core/types.go index 1f89096d5..799fba725 100644 --- a/core/types.go +++ b/core/types.go @@ -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) diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 64754a15f..01625cb3d 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -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{} diff --git a/core/vm/contracts_write.go b/core/vm/contracts_write.go new file mode 100644 index 000000000..c339f1f51 --- /dev/null +++ b/core/vm/contracts_write.go @@ -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") +} diff --git a/core/vm/contracts_write_test.go b/core/vm/contracts_write_test.go new file mode 100644 index 000000000..1a28721fc --- /dev/null +++ b/core/vm/contracts_write_test.go @@ -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", + }, +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 4bb02409d..ce799b389 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -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) diff --git a/core/vm/evm_test.go b/core/vm/evm_test.go new file mode 100644 index 000000000..10e50bdbf --- /dev/null +++ b/core/vm/evm_test.go @@ -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") + } +} diff --git a/core/vm/gas.go b/core/vm/gas.go index bd8b4f104..1b7075340 100644 --- a/core/vm/gas.go +++ b/core/vm/gas.go @@ -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 +} diff --git a/hmy/tracer.go b/hmy/tracer.go index 4def0c166..1294bd2c0 100644 --- a/hmy/tracer.go +++ b/hmy/tracer.go @@ -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) } diff --git a/internal/params/config.go b/internal/params/config.go index ba289fd24..50363a82e 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -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() { @@ -610,13 +628,18 @@ func isForked(s, epoch *big.Int) bool { // Rules is a one time interface meaning that it shouldn't be used in between transition // phases. type Rules struct { - ChainID *big.Int - EthChainID *big.Int - IsCrossLink, IsEIP155, IsS3, IsReceiptLog, IsIstanbul, IsVRF, IsPrevVRF, IsSHA3 bool + ChainID *big.Int + EthChainID *big.Int + 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) @@ -626,15 +649,16 @@ func (c *ChainConfig) Rules(epoch *big.Int) Rules { ethChainID = new(big.Int) } return Rules{ - ChainID: new(big.Int).Set(chainID), - EthChainID: new(big.Int).Set(ethChainID), - IsCrossLink: c.IsCrossLink(epoch), - IsEIP155: c.IsEIP155(epoch), - IsS3: c.IsS3(epoch), - IsReceiptLog: c.IsReceiptLog(epoch), - IsIstanbul: c.IsIstanbul(epoch), - IsVRF: c.IsVRF(epoch), - IsPrevVRF: c.IsPrevVRF(epoch), - IsSHA3: c.IsSHA3(epoch), + ChainID: new(big.Int).Set(chainID), + EthChainID: new(big.Int).Set(ethChainID), + IsCrossLink: c.IsCrossLink(epoch), + IsEIP155: c.IsEIP155(epoch), + IsS3: c.IsS3(epoch), + IsReceiptLog: c.IsReceiptLog(epoch), + IsIstanbul: c.IsIstanbul(epoch), + IsVRF: c.IsVRF(epoch), + IsPrevVRF: c.IsPrevVRF(epoch), + IsSHA3: c.IsSHA3(epoch), + IsStakingPrecompile: c.IsStakingPrecompile(epoch), } } diff --git a/node/worker/worker.go b/node/worker/worker.go index cd023328f..205b2a589 100644 --- a/node/worker/worker.go +++ b/node/worker/worker.go @@ -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, } } diff --git a/rosetta/services/construction_check.go b/rosetta/services/construction_check.go index f53ddd6b5..d850b64f5 100644 --- a/rosetta/services/construction_check.go +++ b/rosetta/services/construction_check.go @@ -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 diff --git a/staking/precompile.go b/staking/precompile.go new file mode 100644 index 000000000..e8d0b06f9 --- /dev/null +++ b/staking/precompile.go @@ -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 + } +} diff --git a/staking/precompile_test.go b/staking/precompile_test.go new file mode 100644 index 000000000..fae85b4ea --- /dev/null +++ b/staking/precompile_test.go @@ -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 ") + 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 ") + 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) + } +} diff --git a/staking/types/messages.go b/staking/types/messages.go index 0cf592c2c..bddcbacf0 100644 --- a/staking/types/messages.go +++ b/staking/types/messages.go @@ -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 +} diff --git a/staking/types/test/equal.go b/staking/types/test/equal.go index 0cc359573..2c20c3ebd 100644 --- a/staking/types/test/equal.go +++ b/staking/types/test/equal.go @@ -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)) }