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 looppull/4024/head
parent
55f8c769a0
commit
d500c4ede6
@ -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 |
||||||
|
} |
@ -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)) |
||||||
|
} |
||||||
|
} |
@ -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)) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,145 @@ |
|||||||
|
package vm |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/ethereum/go-ethereum/rlp" |
||||||
|
"github.com/harmony-one/harmony/shard" |
||||||
|
"github.com/harmony-one/harmony/staking" |
||||||
|
stakingTypes "github.com/harmony-one/harmony/staking/types" |
||||||
|
) |
||||||
|
|
||||||
|
// WriteCapablePrecompiledContractsStaking lists out the write capable precompiled contracts
|
||||||
|
// which are available after the StakingPrecompileEpoch
|
||||||
|
// for now, we have only one contract at 252 or 0xfc - which is the staking precompile
|
||||||
|
var WriteCapablePrecompiledContractsStaking = map[common.Address]WriteCapablePrecompiledContract{ |
||||||
|
common.BytesToAddress([]byte{252}): &stakingPrecompile{}, |
||||||
|
} |
||||||
|
|
||||||
|
// WriteCapablePrecompiledContract represents the interface for Native Go contracts
|
||||||
|
// which are available as a precompile in the EVM
|
||||||
|
// As with (read-only) PrecompiledContracts, these need a RequiredGas function
|
||||||
|
// Note that these contracts have the capability to alter the state
|
||||||
|
// while those in contracts.go do not
|
||||||
|
type WriteCapablePrecompiledContract interface { |
||||||
|
// RequiredGas calculates the contract gas use
|
||||||
|
RequiredGas(evm *EVM, contract *Contract, input []byte) (uint64, error) |
||||||
|
// use a different name from read-only contracts to be safe
|
||||||
|
RunWriteCapable(evm *EVM, contract *Contract, input []byte) ([]byte, error) |
||||||
|
} |
||||||
|
|
||||||
|
// RunWriteCapablePrecompiledContract runs and evaluates the output of a write capable precompiled contract.
|
||||||
|
func RunWriteCapablePrecompiledContract( |
||||||
|
p WriteCapablePrecompiledContract, |
||||||
|
evm *EVM, |
||||||
|
contract *Contract, |
||||||
|
input []byte, |
||||||
|
readOnly bool, |
||||||
|
) ([]byte, error) { |
||||||
|
// immediately error out if readOnly
|
||||||
|
if readOnly { |
||||||
|
return nil, errWriteProtection |
||||||
|
} |
||||||
|
gas, err := p.RequiredGas(evm, contract, input) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if !contract.UseGas(gas) { |
||||||
|
return nil, ErrOutOfGas |
||||||
|
} |
||||||
|
return p.RunWriteCapable(evm, contract, input) |
||||||
|
} |
||||||
|
|
||||||
|
type stakingPrecompile struct{} |
||||||
|
|
||||||
|
// RequiredGas returns the gas required to execute the pre-compiled contract.
|
||||||
|
//
|
||||||
|
// This method does not require any overflow checking as the input size gas costs
|
||||||
|
// required for anything significant is so high it's impossible to pay for.
|
||||||
|
func (c *stakingPrecompile) RequiredGas( |
||||||
|
evm *EVM, |
||||||
|
contract *Contract, |
||||||
|
input []byte, |
||||||
|
) (uint64, error) { |
||||||
|
// if invalid data or invalid shard
|
||||||
|
// set payload to blank and charge minimum gas
|
||||||
|
var payload []byte = make([]byte, 0) |
||||||
|
// availability of staking and precompile has already been checked
|
||||||
|
if evm.Context.ShardID == shard.BeaconChainShardID { |
||||||
|
// check that input is well formed
|
||||||
|
// meaning all the expected parameters are available
|
||||||
|
// and that we are only trying to perform staking tx
|
||||||
|
// on behalf of the correct entity
|
||||||
|
stakeMsg, err := staking.ParseStakeMsg(contract.Caller(), input) |
||||||
|
if err == nil { |
||||||
|
// otherwise charge similar to a regular staking tx
|
||||||
|
if migrationMsg, ok := stakeMsg.(*stakingTypes.MigrationMsg); ok { |
||||||
|
// charge per delegation to migrate
|
||||||
|
return evm.CalculateMigrationGas(evm.StateDB, |
||||||
|
migrationMsg, |
||||||
|
evm.ChainConfig().IsS3(evm.EpochNumber), |
||||||
|
evm.ChainConfig().IsIstanbul(evm.EpochNumber), |
||||||
|
) |
||||||
|
} else if encoded, err := rlp.EncodeToBytes(stakeMsg); err == nil { |
||||||
|
payload = encoded |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if gas, err := IntrinsicGas( |
||||||
|
payload, |
||||||
|
false, // contractCreation
|
||||||
|
evm.ChainConfig().IsS3(evm.EpochNumber), // homestead
|
||||||
|
evm.ChainConfig().IsIstanbul(evm.EpochNumber), // istanbul
|
||||||
|
false, // isValidatorCreation
|
||||||
|
); err != nil { |
||||||
|
return 0, err // ErrOutOfGas occurs when gas payable > uint64
|
||||||
|
} else { |
||||||
|
return gas, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// RunWriteCapable runs the actual contract (that is it performs the staking)
|
||||||
|
func (c *stakingPrecompile) RunWriteCapable( |
||||||
|
evm *EVM, |
||||||
|
contract *Contract, |
||||||
|
input []byte, |
||||||
|
) ([]byte, error) { |
||||||
|
if evm.Context.ShardID != shard.BeaconChainShardID { |
||||||
|
return nil, errors.New("Staking not supported on this shard") |
||||||
|
} |
||||||
|
stakeMsg, err := staking.ParseStakeMsg(contract.Caller(), input) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if delegate, ok := stakeMsg.(*stakingTypes.Delegate); ok { |
||||||
|
if err := evm.Delegate(evm.StateDB, delegate); err != nil { |
||||||
|
return nil, err |
||||||
|
} else { |
||||||
|
evm.StakeMsgs = append(evm.StakeMsgs, delegate) |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
} |
||||||
|
if undelegate, ok := stakeMsg.(*stakingTypes.Undelegate); ok { |
||||||
|
return nil, evm.Undelegate(evm.StateDB, undelegate) |
||||||
|
} |
||||||
|
if collectRewards, ok := stakeMsg.(*stakingTypes.CollectRewards); ok { |
||||||
|
return nil, evm.CollectRewards(evm.StateDB, collectRewards) |
||||||
|
} |
||||||
|
if migrationMsg, ok := stakeMsg.(*stakingTypes.MigrationMsg); ok { |
||||||
|
stakeMsgs, err := evm.MigrateDelegations(evm.StateDB, migrationMsg) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} else { |
||||||
|
for _, stakeMsg := range stakeMsgs { |
||||||
|
if delegate, ok := stakeMsg.(*stakingTypes.Delegate); ok { |
||||||
|
evm.StakeMsgs = append(evm.StakeMsgs, delegate) |
||||||
|
} else { |
||||||
|
return nil, errors.New("[StakingPrecompile] Received incompatible stakeMsg from evm.MigrateDelegations") |
||||||
|
} |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
} |
||||||
|
return nil, errors.New("[StakingPrecompile] Received incompatible stakeMsg from staking.ParseStakeMsg") |
||||||
|
} |
@ -0,0 +1,213 @@ |
|||||||
|
package vm |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"math/big" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/harmony-one/harmony/internal/params" |
||||||
|
stakingTypes "github.com/harmony-one/harmony/staking/types" |
||||||
|
) |
||||||
|
|
||||||
|
type writeCapablePrecompileTest struct { |
||||||
|
input, expected []byte |
||||||
|
name string |
||||||
|
expectedError error |
||||||
|
p *WriteCapablePrecompiledContract |
||||||
|
} |
||||||
|
|
||||||
|
func CollectRewardsFn() CollectRewardsFunc { |
||||||
|
return func(db StateDB, collectRewards *stakingTypes.CollectRewards) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func DelegateFn() DelegateFunc { |
||||||
|
return func(db StateDB, delegate *stakingTypes.Delegate) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func UndelegateFn() UndelegateFunc { |
||||||
|
return func(db StateDB, undelegate *stakingTypes.Undelegate) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func CreateValidatorFn() CreateValidatorFunc { |
||||||
|
return func(db StateDB, createValidator *stakingTypes.CreateValidator) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func EditValidatorFn() EditValidatorFunc { |
||||||
|
return func(db StateDB, editValidator *stakingTypes.EditValidator) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func MigrateDelegationsFn() MigrateDelegationsFunc { |
||||||
|
return func(db StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error) { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func CalculateMigrationGasFn() CalculateMigrationGasFunc { |
||||||
|
return func(db StateDB, migrationMsg *stakingTypes.MigrationMsg, homestead bool, istanbul bool) (uint64, error) { |
||||||
|
return 0, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func testStakingPrecompile(test writeCapablePrecompileTest, t *testing.T) { |
||||||
|
var env = NewEVM(Context{CollectRewards: CollectRewardsFn(), |
||||||
|
Delegate: DelegateFn(), |
||||||
|
Undelegate: UndelegateFn(), |
||||||
|
CreateValidator: CreateValidatorFn(), |
||||||
|
EditValidator: EditValidatorFn(), |
||||||
|
ShardID: 0, |
||||||
|
MigrateDelegations: MigrateDelegationsFn(), |
||||||
|
CalculateMigrationGas: CalculateMigrationGasFn(), |
||||||
|
}, nil, params.TestChainConfig, Config{}) |
||||||
|
// use required gas to avoid out of gas errors
|
||||||
|
p := &stakingPrecompile{} |
||||||
|
t.Run(fmt.Sprintf("%s", test.name), func(t *testing.T) { |
||||||
|
contract := NewContract(AccountRef(common.HexToAddress("1337")), AccountRef(common.HexToAddress("1338")), new(big.Int), 0) |
||||||
|
gas, err := p.RequiredGas(env, contract, test.input) |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
contract.Gas = gas |
||||||
|
if res, err := RunWriteCapablePrecompiledContract(p, env, contract, test.input, false); err != nil { |
||||||
|
if test.expectedError != nil { |
||||||
|
if test.expectedError.Error() != err.Error() { |
||||||
|
t.Errorf("Expected error %v, got %v", test.expectedError, err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if test.expectedError != nil { |
||||||
|
t.Errorf("Expected an error %v but instead got result %v", test.expectedError, res) |
||||||
|
} |
||||||
|
if bytes.Compare(res, test.expected) != 0 { |
||||||
|
t.Errorf("Expected %v, got %v", test.expected, res) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestStakingPrecompiles(t *testing.T) { |
||||||
|
for _, test := range StakingPrecompileTests { |
||||||
|
testStakingPrecompile(test, t) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestWriteCapablePrecompilesReadOnly(t *testing.T) { |
||||||
|
p := &stakingPrecompile{} |
||||||
|
expectedError := errWriteProtection |
||||||
|
res, err := RunWriteCapablePrecompiledContract(p, nil, nil, []byte{}, true) |
||||||
|
if err != nil { |
||||||
|
if err.Error() != expectedError.Error() { |
||||||
|
t.Errorf("Expected error %v, got %v", expectedError, err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
t.Errorf("Expected an error %v but instead got result %v", expectedError, res) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var StakingPrecompileTests = []writeCapablePrecompileTest{ |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 120}, |
||||||
|
expectedError: errors.New("no method with id: 0x6d6b2f78"), |
||||||
|
name: "badStakingKind", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{0, 0}, |
||||||
|
expectedError: errors.New("data too short (2 bytes) for abi method lookup"), |
||||||
|
name: "malformedInput", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55}, |
||||||
|
expected: nil, |
||||||
|
name: "collectRewardsSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "collectRewardsAddressMismatch", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 31 require 32"), |
||||||
|
name: "collectRewardsInvalidABI", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expected: nil, |
||||||
|
name: "delegateSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"), |
||||||
|
name: "delegateInvalidABI", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "delegateAddressMismatch", |
||||||
|
}, |
||||||
|
|
||||||
|
{ |
||||||
|
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expected: nil, |
||||||
|
name: "undelegateSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"), |
||||||
|
name: "undelegateInvalidABI", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "undelegateAddressMismatch", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "undelegateAddressMismatch", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113}, |
||||||
|
expectedError: errors.New("abi: attempting to unmarshall an empty string while arguments are expected"), |
||||||
|
name: "yesMethodNoData", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{0, 0}, |
||||||
|
expectedError: errors.New("data too short (2 bytes) for abi method lookup"), |
||||||
|
name: "malformedInput", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56}, |
||||||
|
expected: nil, |
||||||
|
name: "migrationSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "migrationAddressMismatch", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 6, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55}, |
||||||
|
expectedError: errors.New("no method with id: 0x2a06bb71"), |
||||||
|
name: "migrationNoMatchingMethod", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 63 require 64"), |
||||||
|
name: "migrationAddressMismatch", |
||||||
|
}, |
||||||
|
} |
@ -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") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,233 @@ |
|||||||
|
package staking |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"math/big" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/harmony-one/harmony/accounts/abi" |
||||||
|
stakingTypes "github.com/harmony-one/harmony/staking/types" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
var abiStaking abi.ABI |
||||||
|
|
||||||
|
func init() { |
||||||
|
// for commission rates => solidity does not support floats directly
|
||||||
|
// so send commission rates as string
|
||||||
|
StakingABIJSON := ` |
||||||
|
[ |
||||||
|
{ |
||||||
|
"inputs": [ |
||||||
|
{ |
||||||
|
"internalType": "address", |
||||||
|
"name": "delegatorAddress", |
||||||
|
"type": "address" |
||||||
|
} |
||||||
|
], |
||||||
|
"name": "CollectRewards", |
||||||
|
"outputs": [], |
||||||
|
"stateMutability": "nonpayable", |
||||||
|
"type": "function" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"inputs": [ |
||||||
|
{ |
||||||
|
"internalType": "address", |
||||||
|
"name": "delegatorAddress", |
||||||
|
"type": "address" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"internalType": "address", |
||||||
|
"name": "validatorAddress", |
||||||
|
"type": "address" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"internalType": "uint256", |
||||||
|
"name": "amount", |
||||||
|
"type": "uint256" |
||||||
|
} |
||||||
|
], |
||||||
|
"name": "Delegate", |
||||||
|
"outputs": [], |
||||||
|
"stateMutability": "nonpayable", |
||||||
|
"type": "function" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"inputs": [ |
||||||
|
{ |
||||||
|
"internalType": "address", |
||||||
|
"name": "delegatorAddress", |
||||||
|
"type": "address" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"internalType": "address", |
||||||
|
"name": "validatorAddress", |
||||||
|
"type": "address" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"internalType": "uint256", |
||||||
|
"name": "amount", |
||||||
|
"type": "uint256" |
||||||
|
} |
||||||
|
], |
||||||
|
"name": "Undelegate", |
||||||
|
"outputs": [], |
||||||
|
"stateMutability": "nonpayable", |
||||||
|
"type": "function" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"inputs": [ |
||||||
|
{ |
||||||
|
"internalType": "address", |
||||||
|
"name": "from", |
||||||
|
"type": "address" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"internalType": "address", |
||||||
|
"name": "to", |
||||||
|
"type": "address" |
||||||
|
} |
||||||
|
], |
||||||
|
"name": "Migrate", |
||||||
|
"outputs": [], |
||||||
|
"stateMutability": "nonpayable", |
||||||
|
"type": "function" |
||||||
|
} |
||||||
|
] |
||||||
|
` |
||||||
|
abiStaking, _ = abi.JSON(strings.NewReader(StakingABIJSON)) |
||||||
|
} |
||||||
|
|
||||||
|
// contractCaller (and not Contract) is used here to avoid import cycle
|
||||||
|
func ParseStakeMsg(contractCaller common.Address, input []byte) (interface{}, error) { |
||||||
|
method, err := abiStaking.MethodById(input) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
input = input[4:] // drop the method selector
|
||||||
|
args := map[string]interface{}{} // store into map
|
||||||
|
if err = method.Inputs.UnpackIntoMap(args, input); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
switch method.Name { |
||||||
|
case "Delegate": |
||||||
|
{ |
||||||
|
// in case of assembly call, a contract will delegate its own balance
|
||||||
|
// in case of assembly delegatecall, contract.Caller() is msg.sender
|
||||||
|
// which means an EOA can
|
||||||
|
// (1) deploy a contract which receives delegations and amounts
|
||||||
|
// (2) call the contract, which then performs the tx on behalf of the EOA
|
||||||
|
address, err := ValidateContractAddress(contractCaller, args, "delegatorAddress") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
validatorAddress, err := ParseAddressFromKey(args, "validatorAddress") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
amount, err := ParseBigIntFromKey(args, "amount") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
stakeMsg := &stakingTypes.Delegate{ |
||||||
|
DelegatorAddress: address, |
||||||
|
ValidatorAddress: validatorAddress, |
||||||
|
Amount: amount, |
||||||
|
} |
||||||
|
return stakeMsg, nil |
||||||
|
} |
||||||
|
case "Undelegate": |
||||||
|
{ |
||||||
|
// same validation as above
|
||||||
|
address, err := ValidateContractAddress(contractCaller, args, "delegatorAddress") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
validatorAddress, err := ParseAddressFromKey(args, "validatorAddress") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
// this type assertion is needed by Golang
|
||||||
|
amount, err := ParseBigIntFromKey(args, "amount") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
stakeMsg := &stakingTypes.Undelegate{ |
||||||
|
DelegatorAddress: address, |
||||||
|
ValidatorAddress: validatorAddress, |
||||||
|
Amount: amount, |
||||||
|
} |
||||||
|
return stakeMsg, nil |
||||||
|
} |
||||||
|
case "CollectRewards": |
||||||
|
{ |
||||||
|
// same validation as above
|
||||||
|
address, err := ValidateContractAddress(contractCaller, args, "delegatorAddress") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
stakeMsg := &stakingTypes.CollectRewards{ |
||||||
|
DelegatorAddress: address, |
||||||
|
} |
||||||
|
return stakeMsg, nil |
||||||
|
} |
||||||
|
case "Migrate": |
||||||
|
{ |
||||||
|
from, err := ValidateContractAddress(contractCaller, args, "from") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
to, err := ParseAddressFromKey(args, "to") |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
// no sanity check for migrating to same address, just do nothing
|
||||||
|
return &stakingTypes.MigrationMsg{ |
||||||
|
From: from, |
||||||
|
To: to, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
default: |
||||||
|
{ |
||||||
|
return nil, errors.New("[StakingPrecompile] Invalid method name from ABI selector") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// used to ensure caller == delegatorAddress
|
||||||
|
func ValidateContractAddress(contractCaller common.Address, args map[string]interface{}, key string) (common.Address, error) { |
||||||
|
address, err := ParseAddressFromKey(args, key) |
||||||
|
if err != nil { |
||||||
|
return common.Address{}, err |
||||||
|
} |
||||||
|
if !bytes.Equal(contractCaller.Bytes(), address.Bytes()) { |
||||||
|
return common.Address{}, errors.Errorf( |
||||||
|
"[StakingPrecompile] Address mismatch, expected %s have %s", |
||||||
|
contractCaller.String(), address.String(), |
||||||
|
) |
||||||
|
} else { |
||||||
|
return address, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// used for both delegatorAddress and validatorAddress
|
||||||
|
func ParseAddressFromKey(args map[string]interface{}, key string) (common.Address, error) { |
||||||
|
if address, ok := args[key].(common.Address); ok { |
||||||
|
return address, nil |
||||||
|
} else { |
||||||
|
return common.Address{}, errors.Errorf("Cannot parse address from %v", args[key]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// used for amounts
|
||||||
|
func ParseBigIntFromKey(args map[string]interface{}, key string) (*big.Int, error) { |
||||||
|
bigInt, ok := args[key].(*big.Int) |
||||||
|
if !ok { |
||||||
|
return nil, errors.Errorf( |
||||||
|
"Cannot parse BigInt from %v", args[key]) |
||||||
|
} else { |
||||||
|
return bigInt, nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,203 @@ |
|||||||
|
package staking |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"math/big" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/harmony-one/harmony/common/denominations" |
||||||
|
stakingTypes "github.com/harmony-one/harmony/staking/types" |
||||||
|
) |
||||||
|
|
||||||
|
func TestValidateContractAddress(t *testing.T) { |
||||||
|
input := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55} |
||||||
|
args := map[string]interface{}{} |
||||||
|
expectedError := errors.New("Cannot parse address from <nil>") |
||||||
|
if _, err := ValidateContractAddress(common.BytesToAddress(input), args, "ValidatorAddress"); err != nil { |
||||||
|
if expectedError.Error() != err.Error() { |
||||||
|
t.Errorf("Expected error %v, got %v", expectedError, err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
t.Errorf("Expected error %v, got result", expectedError) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestParseBigIntFromKey(t *testing.T) { |
||||||
|
args := map[string]interface{}{} |
||||||
|
expectedError := errors.New("Cannot parse BigInt from <nil>") |
||||||
|
if _, err := ParseBigIntFromKey(args, "PotentialBigInt"); err != nil { |
||||||
|
if expectedError.Error() != err.Error() { |
||||||
|
t.Errorf("Expected error %v, got %v", expectedError, err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
t.Errorf("Expected error %v, got result", expectedError) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type parseTest struct { |
||||||
|
input []byte |
||||||
|
name string |
||||||
|
expectedError error |
||||||
|
expected interface{} |
||||||
|
} |
||||||
|
|
||||||
|
var ParseStakeMsgTests = []parseTest{ |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 120}, |
||||||
|
expectedError: errors.New("no method with id: 0x6d6b2f78"), |
||||||
|
name: "badStakingKind", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{0, 0}, |
||||||
|
expectedError: errors.New("data too short (2 bytes) for abi method lookup"), |
||||||
|
name: "malformedInput", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55}, |
||||||
|
expected: &stakingTypes.CollectRewards{DelegatorAddress: common.HexToAddress("0x1337")}, |
||||||
|
name: "collectRewardsSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "collectRewardsAddressMismatch", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 31 require 32"), |
||||||
|
name: "collectRewardsInvalidABI", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expected: &stakingTypes.Delegate{ |
||||||
|
DelegatorAddress: common.HexToAddress("0x1337"), |
||||||
|
ValidatorAddress: common.HexToAddress("0x1338"), |
||||||
|
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(100)), |
||||||
|
}, |
||||||
|
name: "delegateSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"), |
||||||
|
name: "delegateInvalidABI", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{81, 11, 17, 187, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "delegateAddressMismatch", |
||||||
|
}, |
||||||
|
|
||||||
|
{ |
||||||
|
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expected: &stakingTypes.Undelegate{ |
||||||
|
DelegatorAddress: common.HexToAddress("0x1337"), |
||||||
|
ValidatorAddress: common.HexToAddress("0x1338"), |
||||||
|
Amount: new(big.Int).Mul(big.NewInt(denominations.One), big.NewInt(100)), |
||||||
|
}, |
||||||
|
name: "undelegateSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 95 require 96"), |
||||||
|
name: "undelegateInvalidABI", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{189, 168, 192, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 107, 199, 94, 45, 99, 16, 0, 0}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "undelegateAddressMismatch", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113}, |
||||||
|
expectedError: errors.New("abi: attempting to unmarshall an empty string while arguments are expected"), |
||||||
|
name: "yesMethodNoData", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{0, 0}, |
||||||
|
expectedError: errors.New("data too short (2 bytes) for abi method lookup"), |
||||||
|
name: "malformedInput", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56}, |
||||||
|
expected: &stakingTypes.MigrationMsg{ |
||||||
|
From: common.HexToAddress("0x1337"), |
||||||
|
To: common.HexToAddress("0x1338"), |
||||||
|
}, |
||||||
|
name: "migrationSuccess", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55}, |
||||||
|
expectedError: errors.New("[StakingPrecompile] Address mismatch, expected 0x0000000000000000000000000000000000001337 have 0x0000000000000000000000000000000000001338"), |
||||||
|
name: "migrationAddressMismatch", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 6, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55}, |
||||||
|
expectedError: errors.New("no method with id: 0x2a06bb71"), |
||||||
|
name: "migrationNoMatchingMethod", |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: []byte{42, 5, 187, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19}, |
||||||
|
expectedError: errors.New("abi: cannot marshal in to go type: length insufficient 63 require 64"), |
||||||
|
name: "migrationAddressMismatch", |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
func testParseStakeMsg(test parseTest, t *testing.T) { |
||||||
|
t.Run(fmt.Sprintf("%s", test.name), func(t *testing.T) { |
||||||
|
if res, err := ParseStakeMsg(common.HexToAddress("1337"), test.input); err != nil { |
||||||
|
if test.expectedError != nil { |
||||||
|
if test.expectedError.Error() != err.Error() { |
||||||
|
t.Errorf("Expected error %v, got %v", test.expectedError, err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if test.expectedError != nil { |
||||||
|
t.Errorf("Expected an error %v but instead got result %v", test.expectedError, res) |
||||||
|
} |
||||||
|
if test.expected != nil { |
||||||
|
if converted, ok := res.(*stakingTypes.Delegate); ok { |
||||||
|
convertedExp, ok := test.expected.(*stakingTypes.Delegate) |
||||||
|
if !ok { |
||||||
|
t.Errorf("Could not converted test.expected to *stakingTypes.Delegate") |
||||||
|
} else if !converted.Equals(*convertedExp) { |
||||||
|
t.Errorf("Expected %+v but got %+v", test.expected, converted) |
||||||
|
} |
||||||
|
} else if converted, ok := res.(*stakingTypes.Undelegate); ok { |
||||||
|
convertedExp, ok := test.expected.(*stakingTypes.Undelegate) |
||||||
|
if !ok { |
||||||
|
t.Errorf("Could not converted test.expected to *stakingTypes.Undelegate") |
||||||
|
} else if !converted.Equals(*convertedExp) { |
||||||
|
t.Errorf("Expected %+v but got %+v", test.expected, converted) |
||||||
|
} |
||||||
|
} else if converted, ok := res.(*stakingTypes.CollectRewards); ok { |
||||||
|
convertedExp, ok := test.expected.(*stakingTypes.CollectRewards) |
||||||
|
if !ok { |
||||||
|
t.Errorf("Could not converted test.expected to *stakingTypes.CollectRewards") |
||||||
|
} else if !converted.Equals(*convertedExp) { |
||||||
|
t.Errorf("Expected %+v but got %+v", test.expected, converted) |
||||||
|
} |
||||||
|
} else if converted, ok := res.(*stakingTypes.MigrationMsg); ok { |
||||||
|
convertedExp, ok := test.expected.(*stakingTypes.MigrationMsg) |
||||||
|
if !ok { |
||||||
|
t.Errorf("Could not converted test.expected to *stakingTypes.MigrationMsg") |
||||||
|
} else if !converted.Equals(*convertedExp) { |
||||||
|
t.Errorf("Expected %+v but got %+v", test.expected, converted) |
||||||
|
} |
||||||
|
} else { |
||||||
|
panic("Received unexpected result from ParseStakeMsg") |
||||||
|
} |
||||||
|
} else if res != nil { |
||||||
|
t.Errorf("Expected nil, got %v", res) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestParseStakeMsgs(t *testing.T) { |
||||||
|
for _, test := range ParseStakeMsgTests { |
||||||
|
testParseStakeMsg(test, t) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue