stargate: migrate types (#670)
* changelog v0.4.0 * stargate: types changes * msg and handler changes * validation * fixes * more fixes * more test fixes * changelog * changelog * lint * rm comment * lint * redundant if condition
This commit is contained in:
parent
64ada18b01
commit
9cbb4dcf6d
11
CHANGELOG.md
11
CHANGELOG.md
@ -39,6 +39,16 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
|||||||
|
|
||||||
### API Breaking
|
### API Breaking
|
||||||
|
|
||||||
|
* (evm) [\#670](https://github.com/cosmos/ethermint/pull/670) Migrate types to the ones defined by the protobuf messages, which are required for the stargate release.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* (evm) [\#674](https://github.com/cosmos/ethermint/issues/674) Reset all cache after account data has been committed in `EndBlock` to make sure every node state consistent
|
||||||
|
|
||||||
|
## [v0.4.0] - 2020-12-15
|
||||||
|
|
||||||
|
### API Breaking
|
||||||
|
|
||||||
* (evm) [\#661](https://github.com/cosmos/ethermint/pull/661) `Balance` field has been removed from the evm module's `GenesisState`.
|
* (evm) [\#661](https://github.com/cosmos/ethermint/pull/661) `Balance` field has been removed from the evm module's `GenesisState`.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@ -60,7 +70,6 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* (evm) [\#674](https://github.com/cosmos/ethermint/issues/674) Reset all cache after account data has been committed in `EndBlock` to make sure every node state consistent
|
|
||||||
* (evm) [\#661](https://github.com/cosmos/ethermint/pull/661) Set nonce to the EVM account on genesis initialization.
|
* (evm) [\#661](https://github.com/cosmos/ethermint/pull/661) Set nonce to the EVM account on genesis initialization.
|
||||||
* (rpc) [\#648](https://github.com/cosmos/ethermint/issues/648) Fix block cumulative gas used value.
|
* (rpc) [\#648](https://github.com/cosmos/ethermint/issues/648) Fix block cumulative gas used value.
|
||||||
* (evm) [\#621](https://github.com/cosmos/ethermint/issues/621) EVM `GenesisAccount` fields now share the same format as the auth module `Account`.
|
* (evm) [\#621](https://github.com/cosmos/ethermint/issues/621) EVM `GenesisAccount` fields now share the same format as the auth module `Account`.
|
||||||
|
@ -330,7 +330,7 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula
|
|||||||
// Charge sender for gas up to limit
|
// Charge sender for gas up to limit
|
||||||
if gasLimit != 0 {
|
if gasLimit != 0 {
|
||||||
// Cost calculates the fees paid to validators based on gas limit and price
|
// Cost calculates the fees paid to validators based on gas limit and price
|
||||||
cost := new(big.Int).Mul(msgEthTx.Data.Price, new(big.Int).SetUint64(gasLimit))
|
cost := new(big.Int).Mul(msgEthTx.Data.Price.BigInt(), new(big.Int).SetUint64(gasLimit))
|
||||||
|
|
||||||
evmDenom := egcd.evmKeeper.GetParams(ctx).EvmDenom
|
evmDenom := egcd.evmKeeper.GetParams(ctx).EvmDenom
|
||||||
|
|
||||||
|
@ -48,15 +48,15 @@ func NewTransaction(tx *evmtypes.MsgEthereumTx, txHash, blockHash common.Hash, b
|
|||||||
rpcTx := &Transaction{
|
rpcTx := &Transaction{
|
||||||
From: from,
|
From: from,
|
||||||
Gas: hexutil.Uint64(tx.Data.GasLimit),
|
Gas: hexutil.Uint64(tx.Data.GasLimit),
|
||||||
GasPrice: (*hexutil.Big)(tx.Data.Price),
|
GasPrice: (*hexutil.Big)(tx.Data.Price.BigInt()),
|
||||||
Hash: txHash,
|
Hash: txHash,
|
||||||
Input: hexutil.Bytes(tx.Data.Payload),
|
Input: hexutil.Bytes(tx.Data.Payload),
|
||||||
Nonce: hexutil.Uint64(tx.Data.AccountNonce),
|
Nonce: hexutil.Uint64(tx.Data.AccountNonce),
|
||||||
To: tx.To(),
|
To: tx.To(),
|
||||||
Value: (*hexutil.Big)(tx.Data.Amount),
|
Value: (*hexutil.Big)(tx.Data.Amount.BigInt()),
|
||||||
V: (*hexutil.Big)(tx.Data.V),
|
V: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.V)),
|
||||||
R: (*hexutil.Big)(tx.Data.R),
|
R: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.R)),
|
||||||
S: (*hexutil.Big)(tx.Data.S),
|
S: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.S)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if blockHash != (common.Hash{}) {
|
if blockHash != (common.Hash{}) {
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
package ethermint.v1;
|
|
||||||
|
|
||||||
import "third_party/proto/gogoproto/gogo.proto";
|
|
||||||
import "third_party/proto/cosmos-sdk/x/auth/types/types.proto";
|
|
||||||
|
|
||||||
option go_package = "github.com/cosmos/ethermint/types";
|
|
||||||
|
|
||||||
|
|
||||||
// EthAccount implements the auth.Account interface and embeds an
|
|
||||||
// auth.BaseAccount type. It is compatible with the auth.AccountKeeper.
|
|
||||||
message EthAccount {
|
|
||||||
option (gogoproto.goproto_getters) = false;
|
|
||||||
option (gogoproto.goproto_stringer) = false;
|
|
||||||
|
|
||||||
cosmos_sdk.x.auth.v1.BaseAccount base_account = 1 [
|
|
||||||
(gogoproto.embed) = true,
|
|
||||||
(gogoproto.moretags) = "yaml:\"base_account\""
|
|
||||||
];
|
|
||||||
bytes code_hash = 2 [
|
|
||||||
(gogoproto.moretags) = "yaml:\"code_hash\""
|
|
||||||
];
|
|
||||||
}
|
|
17
types/validation.go
Normal file
17
types/validation.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsEmptyHash returns true if the hash corresponds to an empty ethereum hex hash.
|
||||||
|
func IsEmptyHash(hash string) bool {
|
||||||
|
return bytes.Equal(ethcmn.HexToHash(hash).Bytes(), ethcmn.Hash{}.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZeroAddress returns true if the address corresponds to an empty ethereum hex address.
|
||||||
|
func IsZeroAddress(address string) bool {
|
||||||
|
return bytes.Equal(ethcmn.HexToAddress(address).Bytes(), ethcmn.Address{}.Bytes())
|
||||||
|
}
|
55
types/validation_test.go
Normal file
55
types/validation_test.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsEmptyHash(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
hash string
|
||||||
|
expEmpty bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty string", "", true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zero hash", ethcmn.Hash{}.String(), true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"non-empty hash", ethcmn.BytesToHash([]byte{1, 2, 3, 4}).String(), false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
require.Equal(t, tc.expEmpty, IsEmptyHash(tc.hash), tc.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsZeroAddress(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
address string
|
||||||
|
expEmpty bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty string", "", true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zero address", ethcmn.Address{}.String(), true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"non-empty address", ethcmn.BytesToAddress([]byte{1, 2, 3, 4}).String(), false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
require.Equal(t, tc.expEmpty, IsZeroAddress(tc.address), tc.name)
|
||||||
|
}
|
||||||
|
}
|
@ -9,15 +9,21 @@ import (
|
|||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
|
|
||||||
ethermint "github.com/cosmos/ethermint/types"
|
ethermint "github.com/cosmos/ethermint/types"
|
||||||
|
"github.com/cosmos/ethermint/x/evm/keeper"
|
||||||
"github.com/cosmos/ethermint/x/evm/types"
|
"github.com/cosmos/ethermint/x/evm/types"
|
||||||
|
|
||||||
abci "github.com/tendermint/tendermint/abci/types"
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitGenesis initializes genesis state based on exported genesis
|
// InitGenesis initializes genesis state based on exported genesis
|
||||||
func InitGenesis(ctx sdk.Context, k Keeper, accountKeeper types.AccountKeeper, data GenesisState) []abci.ValidatorUpdate { // nolint: interfacer
|
func InitGenesis(
|
||||||
k.SetParams(ctx, data.Params)
|
ctx sdk.Context,
|
||||||
|
k keeper.Keeper,
|
||||||
|
accountKeeper types.AccountKeeper, // nolint: interfacer
|
||||||
|
data GenesisState,
|
||||||
|
) []abci.ValidatorUpdate {
|
||||||
|
|
||||||
|
k.SetParams(ctx, data.Params)
|
||||||
evmDenom := data.Params.EvmDenom
|
evmDenom := data.Params.EvmDenom
|
||||||
|
|
||||||
for _, account := range data.Accounts {
|
for _, account := range data.Accounts {
|
||||||
@ -42,16 +48,16 @@ func InitGenesis(ctx sdk.Context, k Keeper, accountKeeper types.AccountKeeper, d
|
|||||||
|
|
||||||
k.SetNonce(ctx, address, acc.GetSequence())
|
k.SetNonce(ctx, address, acc.GetSequence())
|
||||||
k.SetBalance(ctx, address, evmBalance.BigInt())
|
k.SetBalance(ctx, address, evmBalance.BigInt())
|
||||||
k.SetCode(ctx, address, account.Code)
|
k.SetCode(ctx, address, ethcmn.Hex2Bytes(account.Code))
|
||||||
|
|
||||||
for _, storage := range account.Storage {
|
for _, storage := range account.Storage {
|
||||||
k.SetState(ctx, address, storage.Key, storage.Value)
|
k.SetState(ctx, address, ethcmn.HexToHash(storage.Key), ethcmn.HexToHash(storage.Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
for _, txLog := range data.TxsLogs {
|
for _, txLog := range data.TxsLogs {
|
||||||
if err = k.SetLogs(ctx, txLog.Hash, txLog.Logs); err != nil {
|
if err = k.SetLogs(ctx, ethcmn.HexToHash(txLog.Hash), txLog.Logs); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +81,7 @@ func InitGenesis(ctx sdk.Context, k Keeper, accountKeeper types.AccountKeeper, d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExportGenesis exports genesis state of the EVM module
|
// ExportGenesis exports genesis state of the EVM module
|
||||||
func ExportGenesis(ctx sdk.Context, k Keeper, ak types.AccountKeeper) GenesisState {
|
func ExportGenesis(ctx sdk.Context, k keeper.Keeper, ak types.AccountKeeper) GenesisState {
|
||||||
// nolint: prealloc
|
// nolint: prealloc
|
||||||
var ethGenAccounts []types.GenesisAccount
|
var ethGenAccounts []types.GenesisAccount
|
||||||
ak.IterateAccounts(ctx, func(account authexported.Account) bool {
|
ak.IterateAccounts(ctx, func(account authexported.Account) bool {
|
||||||
@ -94,7 +100,7 @@ func ExportGenesis(ctx sdk.Context, k Keeper, ak types.AccountKeeper) GenesisSta
|
|||||||
|
|
||||||
genAccount := types.GenesisAccount{
|
genAccount := types.GenesisAccount{
|
||||||
Address: addr.String(),
|
Address: addr.String(),
|
||||||
Code: k.GetCode(ctx, addr),
|
Code: ethcmn.Bytes2Hex(k.GetCode(ctx, addr)),
|
||||||
Storage: storage,
|
Storage: storage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ func (suite *EvmTestSuite) TestInitGenesis() {
|
|||||||
{
|
{
|
||||||
Address: address.String(),
|
Address: address.String(),
|
||||||
Storage: types.Storage{
|
Storage: types.Storage{
|
||||||
{Key: common.BytesToHash([]byte("key")), Value: common.BytesToHash([]byte("value"))},
|
{Key: common.BytesToHash([]byte("key")).String(), Value: common.BytesToHash([]byte("value")).String()},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -29,93 +29,16 @@ func NewHandler(k Keeper) sdk.Handler {
|
|||||||
|
|
||||||
// handleMsgEthereumTx handles an Ethereum specific tx
|
// handleMsgEthereumTx handles an Ethereum specific tx
|
||||||
func handleMsgEthereumTx(ctx sdk.Context, k Keeper, msg types.MsgEthereumTx) (*sdk.Result, error) {
|
func handleMsgEthereumTx(ctx sdk.Context, k Keeper, msg types.MsgEthereumTx) (*sdk.Result, error) {
|
||||||
// parse the chainID from a string to a base-10 integer
|
// execute state transition
|
||||||
chainIDEpoch, err := ethermint.ParseChainID(ctx.ChainID())
|
res, err := k.EthereumTx(ctx, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify signature and retrieve sender address
|
// log state transition result
|
||||||
sender, err := msg.VerifySig(chainIDEpoch)
|
k.Logger(ctx).Info(res.Log)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
txHash := tmtypes.Tx(ctx.TxBytes()).Hash()
|
return res, nil
|
||||||
ethHash := common.BytesToHash(txHash)
|
|
||||||
|
|
||||||
st := types.StateTransition{
|
|
||||||
AccountNonce: msg.Data.AccountNonce,
|
|
||||||
Price: msg.Data.Price,
|
|
||||||
GasLimit: msg.Data.GasLimit,
|
|
||||||
Recipient: msg.Data.Recipient,
|
|
||||||
Amount: msg.Data.Amount,
|
|
||||||
Payload: msg.Data.Payload,
|
|
||||||
Csdb: k.CommitStateDB.WithContext(ctx),
|
|
||||||
ChainID: chainIDEpoch,
|
|
||||||
TxHash: ðHash,
|
|
||||||
Sender: sender,
|
|
||||||
Simulate: ctx.IsCheckTx(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// since the txCount is used by the stateDB, and a simulated tx is run only on the node it's submitted to,
|
|
||||||
// then this will cause the txCount/stateDB of the node that ran the simulated tx to be different than the
|
|
||||||
// other nodes, causing a consensus error
|
|
||||||
if !st.Simulate {
|
|
||||||
// Prepare db for logs
|
|
||||||
blockHash := types.HashFromContext(ctx)
|
|
||||||
k.CommitStateDB.Prepare(ethHash, blockHash, k.TxCount)
|
|
||||||
k.TxCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
config, found := k.GetChainConfig(ctx)
|
|
||||||
if !found {
|
|
||||||
return nil, types.ErrChainConfigNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
executionResult, err := st.TransitionDb(ctx, config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !st.Simulate {
|
|
||||||
// update block bloom filter
|
|
||||||
k.Bloom.Or(k.Bloom, executionResult.Bloom)
|
|
||||||
|
|
||||||
// update transaction logs in KVStore
|
|
||||||
err = k.SetLogs(ctx, common.BytesToHash(txHash), executionResult.Logs)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// log successful execution
|
|
||||||
k.Logger(ctx).Info(executionResult.Result.Log)
|
|
||||||
|
|
||||||
ctx.EventManager().EmitEvents(sdk.Events{
|
|
||||||
sdk.NewEvent(
|
|
||||||
types.EventTypeEthereumTx,
|
|
||||||
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Data.Amount.String()),
|
|
||||||
),
|
|
||||||
sdk.NewEvent(
|
|
||||||
sdk.EventTypeMessage,
|
|
||||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
|
||||||
sdk.NewAttribute(sdk.AttributeKeySender, sender.String()),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
if msg.Data.Recipient != nil {
|
|
||||||
ctx.EventManager().EmitEvent(
|
|
||||||
sdk.NewEvent(
|
|
||||||
types.EventTypeEthereumTx,
|
|
||||||
sdk.NewAttribute(types.AttributeKeyRecipient, msg.Data.Recipient.String()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the events to the result
|
|
||||||
executionResult.Result.Events = ctx.EventManager().Events()
|
|
||||||
return executionResult.Result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleMsgEthermint handles an sdk.StdTx for an Ethereum state transition
|
// handleMsgEthermint handles an sdk.StdTx for an Ethereum state transition
|
||||||
|
@ -151,6 +151,7 @@ func (k Keeper) GetAllTxLogs(ctx sdk.Context) []types.TransactionLogs {
|
|||||||
// GetAccountStorage return state storage associated with an account
|
// GetAccountStorage return state storage associated with an account
|
||||||
func (k Keeper) GetAccountStorage(ctx sdk.Context, address common.Address) (types.Storage, error) {
|
func (k Keeper) GetAccountStorage(ctx sdk.Context, address common.Address) (types.Storage, error) {
|
||||||
storage := types.Storage{}
|
storage := types.Storage{}
|
||||||
|
|
||||||
err := k.ForEachStorage(ctx, address, func(key, value common.Hash) bool {
|
err := k.ForEachStorage(ctx, address, func(key, value common.Hash) bool {
|
||||||
storage = append(storage, types.NewState(key, value))
|
storage = append(storage, types.NewState(key, value))
|
||||||
return false
|
return false
|
||||||
@ -164,9 +165,9 @@ func (k Keeper) GetAccountStorage(ctx sdk.Context, address common.Address) (type
|
|||||||
|
|
||||||
// GetChainConfig gets block height from block consensus hash
|
// GetChainConfig gets block height from block consensus hash
|
||||||
func (k Keeper) GetChainConfig(ctx sdk.Context) (types.ChainConfig, bool) {
|
func (k Keeper) GetChainConfig(ctx sdk.Context) (types.ChainConfig, bool) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixChainConfig)
|
store := ctx.KVStore(k.storeKey)
|
||||||
// get from an empty key that's already prefixed by KeyPrefixChainConfig
|
|
||||||
bz := store.Get([]byte{})
|
bz := store.Get(types.KeyPrefixChainConfig)
|
||||||
if len(bz) == 0 {
|
if len(bz) == 0 {
|
||||||
return types.ChainConfig{}, false
|
return types.ChainConfig{}, false
|
||||||
}
|
}
|
||||||
@ -178,8 +179,7 @@ func (k Keeper) GetChainConfig(ctx sdk.Context) (types.ChainConfig, bool) {
|
|||||||
|
|
||||||
// SetChainConfig sets the mapping from block consensus hash to block height
|
// SetChainConfig sets the mapping from block consensus hash to block height
|
||||||
func (k Keeper) SetChainConfig(ctx sdk.Context, config types.ChainConfig) {
|
func (k Keeper) SetChainConfig(ctx sdk.Context, config types.ChainConfig) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefixChainConfig)
|
store := ctx.KVStore(k.storeKey)
|
||||||
bz := k.cdc.MustMarshalBinaryBare(config)
|
bz := k.cdc.MustMarshalBinaryBare(config)
|
||||||
// get to an empty key that's already prefixed by KeyPrefixChainConfig
|
store.Set(types.KeyPrefixChainConfig, bz)
|
||||||
store.Set([]byte{}, bz)
|
|
||||||
}
|
}
|
||||||
|
@ -98,10 +98,10 @@ func (suite *KeeperTestSuite) TestTransactionLogs() {
|
|||||||
txLogs := suite.app.EvmKeeper.GetAllTxLogs(suite.ctx)
|
txLogs := suite.app.EvmKeeper.GetAllTxLogs(suite.ctx)
|
||||||
suite.Require().Equal(2, len(txLogs))
|
suite.Require().Equal(2, len(txLogs))
|
||||||
|
|
||||||
suite.Require().Equal(ethcmn.Hash{}.String(), txLogs[0].Hash.String())
|
suite.Require().Equal(ethcmn.Hash{}.String(), txLogs[0].Hash)
|
||||||
suite.Require().Equal([]*ethtypes.Log{log2, log3}, txLogs[0].Logs)
|
suite.Require().Equal([]*ethtypes.Log{log2, log3}, txLogs[0].Logs)
|
||||||
|
|
||||||
suite.Require().Equal(ethHash.String(), txLogs[1].Hash.String())
|
suite.Require().Equal(ethHash.String(), txLogs[1].Hash)
|
||||||
suite.Require().Equal([]*ethtypes.Log{log}, txLogs[1].Logs)
|
suite.Require().Equal([]*ethtypes.Log{log}, txLogs[1].Logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
104
x/evm/keeper/msg_server.go
Normal file
104
x/evm/keeper/msg_server.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
tmtypes "github.com/tendermint/tendermint/types"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
ethermint "github.com/cosmos/ethermint/types"
|
||||||
|
"github.com/cosmos/ethermint/x/evm/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EthereumTx implements the Msg/EthereumTx gRPC method.
|
||||||
|
func (k Keeper) EthereumTx(ctx sdk.Context, msg types.MsgEthereumTx) (*sdk.Result, error) {
|
||||||
|
// parse the chainID from a string to a base-10 integer
|
||||||
|
chainIDEpoch, err := ethermint.ParseChainID(ctx.ChainID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signature and retrieve sender address
|
||||||
|
sender, err := msg.VerifySig(chainIDEpoch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipient *common.Address
|
||||||
|
if msg.Data.Recipient != nil {
|
||||||
|
addr := common.HexToAddress(msg.Data.Recipient.Address)
|
||||||
|
recipient = &addr
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash := tmtypes.Tx(ctx.TxBytes()).Hash()
|
||||||
|
ethHash := common.BytesToHash(txHash)
|
||||||
|
|
||||||
|
st := types.StateTransition{
|
||||||
|
AccountNonce: msg.Data.AccountNonce,
|
||||||
|
Price: msg.Data.Price.BigInt(),
|
||||||
|
GasLimit: msg.Data.GasLimit,
|
||||||
|
Recipient: recipient,
|
||||||
|
Amount: msg.Data.Amount.BigInt(),
|
||||||
|
Payload: msg.Data.Payload,
|
||||||
|
Csdb: k.CommitStateDB.WithContext(ctx),
|
||||||
|
ChainID: chainIDEpoch,
|
||||||
|
TxHash: ðHash,
|
||||||
|
Sender: sender,
|
||||||
|
Simulate: ctx.IsCheckTx(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// since the txCount is used by the stateDB, and a simulated tx is run only on the node it's submitted to,
|
||||||
|
// then this will cause the txCount/stateDB of the node that ran the simulated tx to be different than the
|
||||||
|
// other nodes, causing a consensus error
|
||||||
|
if !st.Simulate {
|
||||||
|
// Prepare db for logs
|
||||||
|
blockHash := types.HashFromContext(ctx)
|
||||||
|
k.CommitStateDB.Prepare(ethHash, blockHash, k.TxCount)
|
||||||
|
k.TxCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
config, found := k.GetChainConfig(ctx)
|
||||||
|
if !found {
|
||||||
|
return nil, types.ErrChainConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
executionResult, err := st.TransitionDb(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !st.Simulate {
|
||||||
|
// update block bloom filter
|
||||||
|
k.Bloom.Or(k.Bloom, executionResult.Bloom)
|
||||||
|
|
||||||
|
// update transaction logs in KVStore
|
||||||
|
err = k.SetLogs(ctx, common.BytesToHash(txHash), executionResult.Logs)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvents(sdk.Events{
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeEthereumTx,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Data.Amount.String()),
|
||||||
|
),
|
||||||
|
sdk.NewEvent(
|
||||||
|
sdk.EventTypeMessage,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeySender, sender.String()),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
if msg.Data.Recipient != nil {
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeEthereumTx,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyRecipient, msg.Data.Recipient.Address),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
executionResult.Result.Events = ctx.EventManager().Events()
|
||||||
|
return executionResult.Result, nil
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package keeper
|
package keeper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -39,8 +38,6 @@ func NewQuerier(keeper Keeper) sdk.Querier {
|
|||||||
return queryLogs(ctx, keeper)
|
return queryLogs(ctx, keeper)
|
||||||
case types.QueryAccount:
|
case types.QueryAccount:
|
||||||
return queryAccount(ctx, path, keeper)
|
return queryAccount(ctx, path, keeper)
|
||||||
case types.QueryExportAccount:
|
|
||||||
return queryExportAccount(ctx, path, keeper)
|
|
||||||
default:
|
default:
|
||||||
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown query endpoint")
|
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown query endpoint")
|
||||||
}
|
}
|
||||||
@ -183,31 +180,3 @@ func queryAccount(ctx sdk.Context, path []string, keeper Keeper) ([]byte, error)
|
|||||||
}
|
}
|
||||||
return bz, nil
|
return bz, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryExportAccount(ctx sdk.Context, path []string, keeper Keeper) ([]byte, error) {
|
|
||||||
hexAddress := path[1]
|
|
||||||
addr := ethcmn.HexToAddress(hexAddress)
|
|
||||||
|
|
||||||
var storage types.Storage
|
|
||||||
err := keeper.ForEachStorage(ctx, addr, func(key, value ethcmn.Hash) bool {
|
|
||||||
storage = append(storage, types.NewState(key, value))
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := types.GenesisAccount{
|
|
||||||
Address: hexAddress,
|
|
||||||
Code: keeper.GetCode(ctx, addr),
|
|
||||||
Storage: storage,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: codec.MarshalJSONIndent doesn't call the String() method of types properly
|
|
||||||
bz, err := json.MarshalIndent(res, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return bz, nil
|
|
||||||
}
|
|
||||||
|
@ -34,7 +34,6 @@ func (suite *KeeperTestSuite) TestQuerier() {
|
|||||||
}, true},
|
}, true},
|
||||||
{"logs", []string{types.QueryLogs, "0x0"}, func() {}, true},
|
{"logs", []string{types.QueryLogs, "0x0"}, func() {}, true},
|
||||||
{"account", []string{types.QueryAccount, "0x0"}, func() {}, true},
|
{"account", []string{types.QueryAccount, "0x0"}, func() {}, true},
|
||||||
{"exportAccount", []string{types.QueryExportAccount, "0x0"}, func() {}, true},
|
|
||||||
{"unknown request", []string{"other"}, func() {}, false},
|
{"unknown request", []string{"other"}, func() {}, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,7 +582,7 @@ func (suite *KeeperTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
name string
|
name string
|
||||||
malleate func()
|
malleate func()
|
||||||
callback func(key, value ethcmn.Hash) (stop bool)
|
callback func(key, value ethcmn.Hash) (stop bool)
|
||||||
expValues []ethcmn.Hash
|
expValues []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"aggregate state",
|
"aggregate state",
|
||||||
@ -595,12 +595,12 @@ func (suite *KeeperTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
storage = append(storage, types.NewState(key, value))
|
storage = append(storage, types.NewState(key, value))
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
[]ethcmn.Hash{
|
[]string{
|
||||||
ethcmn.BytesToHash([]byte("value0")),
|
ethcmn.BytesToHash([]byte("value0")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value1")),
|
ethcmn.BytesToHash([]byte("value1")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value2")),
|
ethcmn.BytesToHash([]byte("value2")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value3")),
|
ethcmn.BytesToHash([]byte("value3")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value4")),
|
ethcmn.BytesToHash([]byte("value4")).String(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -616,8 +616,8 @@ func (suite *KeeperTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
[]ethcmn.Hash{
|
[]string{
|
||||||
ethcmn.BytesToHash([]byte("filtervalue")),
|
ethcmn.BytesToHash([]byte("filtervalue")).String(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -632,7 +632,7 @@ func (suite *KeeperTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
suite.Require().NoError(err)
|
suite.Require().NoError(err)
|
||||||
suite.Require().Equal(len(tc.expValues), len(storage), fmt.Sprintf("Expected values:\n%v\nStorage Values\n%v", tc.expValues, storage))
|
suite.Require().Equal(len(tc.expValues), len(storage), fmt.Sprintf("Expected values:\n%v\nStorage Values\n%v", tc.expValues, storage))
|
||||||
|
|
||||||
vals := make([]ethcmn.Hash, len(storage))
|
vals := make([]string, len(storage))
|
||||||
for i := range storage {
|
for i := range storage {
|
||||||
vals[i] = storage[i].Value
|
vals[i] = storage[i].Value
|
||||||
}
|
}
|
||||||
|
@ -151,12 +151,6 @@ func validateHash(hex string) error {
|
|||||||
return sdkerrors.Wrapf(ErrInvalidChainConfig, "hash cannot be blank")
|
return sdkerrors.Wrapf(ErrInvalidChainConfig, "hash cannot be blank")
|
||||||
}
|
}
|
||||||
|
|
||||||
bz := common.FromHex(hex)
|
|
||||||
lenHex := len(bz)
|
|
||||||
if lenHex > 0 && lenHex != common.HashLength {
|
|
||||||
return sdkerrors.Wrapf(ErrInvalidChainConfig, "invalid hash length, expected %d, got %d", common.HashLength, lenHex)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,8 +4,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
ethermint "github.com/cosmos/ethermint/types"
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -22,19 +23,19 @@ type (
|
|||||||
// storage type and that it doesn't contain the private key field.
|
// storage type and that it doesn't contain the private key field.
|
||||||
// NOTE: balance is omitted as it is imported from the auth account balance.
|
// NOTE: balance is omitted as it is imported from the auth account balance.
|
||||||
GenesisAccount struct {
|
GenesisAccount struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Code hexutil.Bytes `json:"code,omitempty"`
|
Code string `json:"code,omitempty"`
|
||||||
Storage Storage `json:"storage,omitempty"`
|
Storage Storage `json:"storage,omitempty"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validate performs a basic validation of a GenesisAccount fields.
|
// Validate performs a basic validation of a GenesisAccount fields.
|
||||||
func (ga GenesisAccount) Validate() error {
|
func (ga GenesisAccount) Validate() error {
|
||||||
if ga.Address == (ethcmn.Address{}.String()) {
|
if ethermint.IsZeroAddress(ga.Address) {
|
||||||
return fmt.Errorf("address cannot be the zero address %s", ga.Address)
|
return fmt.Errorf("address cannot be the zero address %s", ga.Address)
|
||||||
}
|
}
|
||||||
if ga.Code != nil && len(ga.Code) == 0 {
|
if len(ethcmn.Hex2Bytes(ga.Code)) == 0 {
|
||||||
return errors.New("code bytes cannot be empty")
|
return errors.New("code cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ga.Storage.Validate()
|
return ga.Storage.Validate()
|
||||||
@ -67,15 +68,15 @@ func (gs GenesisState) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tx := range gs.TxsLogs {
|
for _, tx := range gs.TxsLogs {
|
||||||
if seenTxs[tx.Hash.String()] {
|
if seenTxs[tx.Hash] {
|
||||||
return fmt.Errorf("duplicated logs from transaction %s", tx.Hash.String())
|
return fmt.Errorf("duplicated logs from transaction %s", tx.Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Validate(); err != nil {
|
if err := tx.Validate(); err != nil {
|
||||||
return fmt.Errorf("invalid logs from transaction %s: %w", tx.Hash.String(), err)
|
return fmt.Errorf("invalid logs from transaction %s: %w", tx.Hash, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
seenTxs[tx.Hash.String()] = true
|
seenTxs[tx.Hash] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gs.ChainConfig.Validate(); err != nil {
|
if err := gs.ChainConfig.Validate(); err != nil {
|
||||||
|
@ -3,18 +3,36 @@ package types
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
|
||||||
|
|
||||||
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
|
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var address = ethcmn.BytesToAddress([]byte{1, 2, 3, 4, 5})
|
type GenesisTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
func TestValidateGenesisAccount(t *testing.T) {
|
address ethcmn.Address
|
||||||
|
hash ethcmn.Hash
|
||||||
|
code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *GenesisTestSuite) SetupTest() {
|
||||||
|
priv, err := ethsecp256k1.GenerateKey()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
suite.address = ethcmn.BytesToAddress(priv.PubKey().Address().Bytes())
|
||||||
|
suite.hash = ethcmn.BytesToHash([]byte("hash"))
|
||||||
|
suite.code = ethcmn.Bytes2Hex([]byte{1, 2, 3})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenesisTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(GenesisTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *GenesisTestSuite) TestValidateGenesisAccount() {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
genesisAccount GenesisAccount
|
genesisAccount GenesisAccount
|
||||||
@ -23,10 +41,10 @@ func TestValidateGenesisAccount(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"valid genesis account",
|
"valid genesis account",
|
||||||
GenesisAccount{
|
GenesisAccount{
|
||||||
Address: address.String(),
|
Address: suite.address.String(),
|
||||||
Code: []byte{1, 2, 3},
|
Code: suite.code,
|
||||||
Storage: Storage{
|
Storage: Storage{
|
||||||
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
NewState(suite.hash, suite.hash),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
@ -41,8 +59,8 @@ func TestValidateGenesisAccount(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"empty code bytes",
|
"empty code bytes",
|
||||||
GenesisAccount{
|
GenesisAccount{
|
||||||
Address: address.String(),
|
Address: suite.address.String(),
|
||||||
Code: []byte{},
|
Code: "",
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
@ -52,18 +70,14 @@ func TestValidateGenesisAccount(t *testing.T) {
|
|||||||
tc := tc
|
tc := tc
|
||||||
err := tc.genesisAccount.Validate()
|
err := tc.genesisAccount.Validate()
|
||||||
if tc.expPass {
|
if tc.expPass {
|
||||||
require.NoError(t, err, tc.name)
|
suite.Require().NoError(err, tc.name)
|
||||||
} else {
|
} else {
|
||||||
require.Error(t, err, tc.name)
|
suite.Require().Error(err, tc.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateGenesis(t *testing.T) {
|
func (suite *GenesisTestSuite) TestValidateGenesis() {
|
||||||
priv, err := ethsecp256k1.GenerateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
addr := ethcrypto.PubkeyToAddress(priv.ToECDSA().PublicKey)
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
genState GenesisState
|
genState GenesisState
|
||||||
@ -79,25 +93,25 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
genState: GenesisState{
|
genState: GenesisState{
|
||||||
Accounts: []GenesisAccount{
|
Accounts: []GenesisAccount{
|
||||||
{
|
{
|
||||||
Address: address.String(),
|
Address: suite.address.String(),
|
||||||
Code: []byte{1, 2, 3},
|
Code: suite.code,
|
||||||
Storage: Storage{
|
Storage: Storage{
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: suite.hash.String()},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TxsLogs: []TransactionLogs{
|
TxsLogs: []TransactionLogs{
|
||||||
{
|
{
|
||||||
Hash: ethcmn.BytesToHash([]byte("tx_hash")),
|
Hash: suite.hash.String(),
|
||||||
Logs: []*ethtypes.Log{
|
Logs: []*ethtypes.Log{
|
||||||
{
|
{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
Topics: []ethcmn.Hash{suite.hash},
|
||||||
Data: []byte("data"),
|
Data: []byte("data"),
|
||||||
BlockNumber: 1,
|
BlockNumber: 1,
|
||||||
TxHash: ethcmn.BytesToHash([]byte("tx_hash")),
|
TxHash: suite.hash,
|
||||||
TxIndex: 1,
|
TxIndex: 1,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Removed: false,
|
Removed: false,
|
||||||
},
|
},
|
||||||
@ -130,17 +144,17 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
genState: GenesisState{
|
genState: GenesisState{
|
||||||
Accounts: []GenesisAccount{
|
Accounts: []GenesisAccount{
|
||||||
{
|
{
|
||||||
Address: address.String(),
|
Address: suite.address.String(),
|
||||||
Code: []byte{1, 2, 3},
|
Code: suite.code,
|
||||||
Storage: Storage{
|
Storage: Storage{
|
||||||
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
NewState(suite.hash, suite.hash),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Address: address.String(),
|
Address: suite.address.String(),
|
||||||
Code: []byte{1, 2, 3},
|
Code: suite.code,
|
||||||
Storage: Storage{
|
Storage: Storage{
|
||||||
NewState(ethcmn.BytesToHash([]byte{1, 2, 3}), ethcmn.BytesToHash([]byte{1, 2, 3})),
|
NewState(suite.hash, suite.hash),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -152,41 +166,41 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
genState: GenesisState{
|
genState: GenesisState{
|
||||||
Accounts: []GenesisAccount{
|
Accounts: []GenesisAccount{
|
||||||
{
|
{
|
||||||
Address: address.String(),
|
Address: suite.address.String(),
|
||||||
Code: []byte{1, 2, 3},
|
Code: suite.code,
|
||||||
Storage: Storage{
|
Storage: Storage{
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: suite.hash.String()},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TxsLogs: []TransactionLogs{
|
TxsLogs: []TransactionLogs{
|
||||||
{
|
{
|
||||||
Hash: ethcmn.BytesToHash([]byte("tx_hash")),
|
Hash: suite.hash.String(),
|
||||||
Logs: []*ethtypes.Log{
|
Logs: []*ethtypes.Log{
|
||||||
{
|
{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
Topics: []ethcmn.Hash{suite.hash},
|
||||||
Data: []byte("data"),
|
Data: []byte("data"),
|
||||||
BlockNumber: 1,
|
BlockNumber: 1,
|
||||||
TxHash: ethcmn.BytesToHash([]byte("tx_hash")),
|
TxHash: suite.hash,
|
||||||
TxIndex: 1,
|
TxIndex: 1,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Removed: false,
|
Removed: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Hash: ethcmn.BytesToHash([]byte("tx_hash")),
|
Hash: suite.hash.String(),
|
||||||
Logs: []*ethtypes.Log{
|
Logs: []*ethtypes.Log{
|
||||||
{
|
{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
Topics: []ethcmn.Hash{suite.hash},
|
||||||
Data: []byte("data"),
|
Data: []byte("data"),
|
||||||
BlockNumber: 1,
|
BlockNumber: 1,
|
||||||
TxHash: ethcmn.BytesToHash([]byte("tx_hash")),
|
TxHash: suite.hash,
|
||||||
TxIndex: 1,
|
TxIndex: 1,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Removed: false,
|
Removed: false,
|
||||||
},
|
},
|
||||||
@ -201,10 +215,10 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
genState: GenesisState{
|
genState: GenesisState{
|
||||||
Accounts: []GenesisAccount{
|
Accounts: []GenesisAccount{
|
||||||
{
|
{
|
||||||
Address: address.String(),
|
Address: suite.address.String(),
|
||||||
Code: []byte{1, 2, 3},
|
Code: suite.code,
|
||||||
Storage: Storage{
|
Storage: Storage{
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: suite.hash.String()},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -234,9 +248,9 @@ func TestValidateGenesis(t *testing.T) {
|
|||||||
tc := tc
|
tc := tc
|
||||||
err := tc.genState.Validate()
|
err := tc.genState.Validate()
|
||||||
if tc.expPass {
|
if tc.expPass {
|
||||||
require.NoError(t, err, tc.name)
|
suite.Require().NoError(err, tc.name)
|
||||||
} else {
|
} else {
|
||||||
require.Error(t, err, tc.name)
|
suite.Require().Error(err, tc.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
ethermint "github.com/cosmos/ethermint/types"
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||||
)
|
)
|
||||||
@ -13,14 +13,14 @@ import (
|
|||||||
// with a given hash. It it used for import/export data as transactions are not persisted
|
// with a given hash. It it used for import/export data as transactions are not persisted
|
||||||
// on blockchain state after an upgrade.
|
// on blockchain state after an upgrade.
|
||||||
type TransactionLogs struct {
|
type TransactionLogs struct {
|
||||||
Hash ethcmn.Hash `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Logs []*ethtypes.Log `json:"logs"`
|
Logs []*ethtypes.Log `json:"logs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTransactionLogs creates a new NewTransactionLogs instance.
|
// NewTransactionLogs creates a new NewTransactionLogs instance.
|
||||||
func NewTransactionLogs(hash ethcmn.Hash, logs []*ethtypes.Log) TransactionLogs {
|
func NewTransactionLogs(hash ethcmn.Hash, logs []*ethtypes.Log) TransactionLogs { // nolint: interfacer
|
||||||
return TransactionLogs{
|
return TransactionLogs{
|
||||||
Hash: hash,
|
Hash: hash.String(),
|
||||||
Logs: logs,
|
Logs: logs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,16 +39,16 @@ func UnmarshalLogs(in []byte) ([]*ethtypes.Log, error) {
|
|||||||
|
|
||||||
// Validate performs a basic validation of a GenesisAccount fields.
|
// Validate performs a basic validation of a GenesisAccount fields.
|
||||||
func (tx TransactionLogs) Validate() error {
|
func (tx TransactionLogs) Validate() error {
|
||||||
if bytes.Equal(tx.Hash.Bytes(), ethcmn.Hash{}.Bytes()) {
|
if ethermint.IsEmptyHash(tx.Hash) {
|
||||||
return fmt.Errorf("hash cannot be the empty %s", tx.Hash.String())
|
return fmt.Errorf("hash cannot be the empty %s", tx.Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, log := range tx.Logs {
|
for i, log := range tx.Logs {
|
||||||
if err := ValidateLog(log); err != nil {
|
if err := ValidateLog(log); err != nil {
|
||||||
return fmt.Errorf("invalid log %d: %w", i, err)
|
return fmt.Errorf("invalid log %d: %w", i, err)
|
||||||
}
|
}
|
||||||
if !bytes.Equal(log.TxHash.Bytes(), tx.Hash.Bytes()) {
|
if log.TxHash.String() != tx.Hash {
|
||||||
return fmt.Errorf("log tx hash mismatch (%s ≠ %s)", log.TxHash.String(), tx.Hash.String())
|
return fmt.Errorf("log tx hash mismatch (%s ≠ %s)", log.TxHash.String(), tx.Hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -59,16 +59,16 @@ func ValidateLog(log *ethtypes.Log) error {
|
|||||||
if log == nil {
|
if log == nil {
|
||||||
return errors.New("log cannot be nil")
|
return errors.New("log cannot be nil")
|
||||||
}
|
}
|
||||||
if bytes.Equal(log.Address.Bytes(), ethcmn.Address{}.Bytes()) {
|
if ethermint.IsZeroAddress(log.Address.String()) {
|
||||||
return fmt.Errorf("log address cannot be empty %s", log.Address.String())
|
return fmt.Errorf("log address cannot be empty %s", log.Address.String())
|
||||||
}
|
}
|
||||||
if bytes.Equal(log.BlockHash.Bytes(), ethcmn.Hash{}.Bytes()) {
|
if ethermint.IsEmptyHash(log.BlockHash.String()) {
|
||||||
return fmt.Errorf("block hash cannot be the empty %s", log.BlockHash.String())
|
return fmt.Errorf("block hash cannot be the empty %s", log.BlockHash.String())
|
||||||
}
|
}
|
||||||
if log.BlockNumber == 0 {
|
if log.BlockNumber == 0 {
|
||||||
return errors.New("block number cannot be zero")
|
return errors.New("block number cannot be zero")
|
||||||
}
|
}
|
||||||
if bytes.Equal(log.TxHash.Bytes(), ethcmn.Hash{}.Bytes()) {
|
if ethermint.IsEmptyHash(log.TxHash.String()) {
|
||||||
return fmt.Errorf("tx hash cannot be the empty %s", log.TxHash.String())
|
return fmt.Errorf("tx hash cannot be the empty %s", log.TxHash.String())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,21 +1,11 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTransactionLogsValidate(t *testing.T) {
|
func (suite *GenesisTestSuite) TestTransactionLogsValidate() {
|
||||||
priv, err := ethsecp256k1.GenerateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
addr := ethcrypto.PubkeyToAddress(priv.ToECDSA().PublicKey)
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
txLogs TransactionLogs
|
txLogs TransactionLogs
|
||||||
@ -24,16 +14,16 @@ func TestTransactionLogsValidate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"valid log",
|
"valid log",
|
||||||
TransactionLogs{
|
TransactionLogs{
|
||||||
Hash: ethcmn.BytesToHash([]byte("tx_hash")),
|
Hash: suite.hash.String(),
|
||||||
Logs: []*ethtypes.Log{
|
Logs: []*ethtypes.Log{
|
||||||
{
|
{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
||||||
Data: []byte("data"),
|
Data: []byte("data"),
|
||||||
BlockNumber: 1,
|
BlockNumber: 1,
|
||||||
TxHash: ethcmn.BytesToHash([]byte("tx_hash")),
|
TxHash: suite.hash,
|
||||||
TxIndex: 1,
|
TxIndex: 1,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Removed: false,
|
Removed: false,
|
||||||
},
|
},
|
||||||
@ -44,14 +34,14 @@ func TestTransactionLogsValidate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"empty hash",
|
"empty hash",
|
||||||
TransactionLogs{
|
TransactionLogs{
|
||||||
Hash: ethcmn.Hash{},
|
Hash: ethcmn.Hash{}.String(),
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"invalid log",
|
"invalid log",
|
||||||
TransactionLogs{
|
TransactionLogs{
|
||||||
Hash: ethcmn.BytesToHash([]byte("tx_hash")),
|
Hash: suite.hash.String(),
|
||||||
Logs: []*ethtypes.Log{nil},
|
Logs: []*ethtypes.Log{nil},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -59,16 +49,16 @@ func TestTransactionLogsValidate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"hash mismatch log",
|
"hash mismatch log",
|
||||||
TransactionLogs{
|
TransactionLogs{
|
||||||
Hash: ethcmn.BytesToHash([]byte("tx_hash")),
|
Hash: suite.hash.String(),
|
||||||
Logs: []*ethtypes.Log{
|
Logs: []*ethtypes.Log{
|
||||||
{
|
{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
||||||
Data: []byte("data"),
|
Data: []byte("data"),
|
||||||
BlockNumber: 1,
|
BlockNumber: 1,
|
||||||
TxHash: ethcmn.BytesToHash([]byte("other_hash")),
|
TxHash: ethcmn.BytesToHash([]byte("other_hash")),
|
||||||
TxIndex: 1,
|
TxIndex: 1,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Removed: false,
|
Removed: false,
|
||||||
},
|
},
|
||||||
@ -82,18 +72,14 @@ func TestTransactionLogsValidate(t *testing.T) {
|
|||||||
tc := tc
|
tc := tc
|
||||||
err := tc.txLogs.Validate()
|
err := tc.txLogs.Validate()
|
||||||
if tc.expPass {
|
if tc.expPass {
|
||||||
require.NoError(t, err, tc.name)
|
suite.Require().NoError(err, tc.name)
|
||||||
} else {
|
} else {
|
||||||
require.Error(t, err, tc.name)
|
suite.Require().Error(err, tc.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateLog(t *testing.T) {
|
func (suite *GenesisTestSuite) TestValidateLog() {
|
||||||
priv, err := ethsecp256k1.GenerateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
addr := ethcrypto.PubkeyToAddress(priv.ToECDSA().PublicKey)
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
log *ethtypes.Log
|
log *ethtypes.Log
|
||||||
@ -102,13 +88,13 @@ func TestValidateLog(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"valid log",
|
"valid log",
|
||||||
ðtypes.Log{
|
ðtypes.Log{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
Topics: []ethcmn.Hash{ethcmn.BytesToHash([]byte("topic"))},
|
||||||
Data: []byte("data"),
|
Data: []byte("data"),
|
||||||
BlockNumber: 1,
|
BlockNumber: 1,
|
||||||
TxHash: ethcmn.BytesToHash([]byte("tx_hash")),
|
TxHash: suite.hash,
|
||||||
TxIndex: 1,
|
TxIndex: 1,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Removed: false,
|
Removed: false,
|
||||||
},
|
},
|
||||||
@ -127,7 +113,7 @@ func TestValidateLog(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"empty block hash",
|
"empty block hash",
|
||||||
ðtypes.Log{
|
ðtypes.Log{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
BlockHash: ethcmn.Hash{},
|
BlockHash: ethcmn.Hash{},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -135,8 +121,8 @@ func TestValidateLog(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"zero block number",
|
"zero block number",
|
||||||
ðtypes.Log{
|
ðtypes.Log{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
BlockNumber: 0,
|
BlockNumber: 0,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
@ -144,8 +130,8 @@ func TestValidateLog(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"empty tx hash",
|
"empty tx hash",
|
||||||
ðtypes.Log{
|
ðtypes.Log{
|
||||||
Address: addr,
|
Address: suite.address,
|
||||||
BlockHash: ethcmn.BytesToHash([]byte("block_hash")),
|
BlockHash: suite.hash,
|
||||||
BlockNumber: 1,
|
BlockNumber: 1,
|
||||||
TxHash: ethcmn.Hash{},
|
TxHash: ethcmn.Hash{},
|
||||||
},
|
},
|
||||||
@ -157,9 +143,9 @@ func TestValidateLog(t *testing.T) {
|
|||||||
tc := tc
|
tc := tc
|
||||||
err := ValidateLog(tc.log)
|
err := ValidateLog(tc.log)
|
||||||
if tc.expPass {
|
if tc.expPass {
|
||||||
require.NoError(t, err, tc.name)
|
suite.Require().NoError(err, tc.name)
|
||||||
} else {
|
} else {
|
||||||
require.Error(t, err, tc.name)
|
suite.Require().Error(err, tc.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/cosmos/ethermint/types"
|
"github.com/cosmos/ethermint/types"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
@ -147,37 +148,43 @@ func NewMsgEthereumTxContract(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newMsgEthereumTx(
|
func newMsgEthereumTx(
|
||||||
nonce uint64, to *ethcmn.Address, amount *big.Int,
|
nonce uint64, to *ethcmn.Address, amount *big.Int, // nolint: interfacer
|
||||||
gasLimit uint64, gasPrice *big.Int, payload []byte,
|
gasLimit uint64, gasPrice *big.Int, payload []byte,
|
||||||
) MsgEthereumTx {
|
) MsgEthereumTx {
|
||||||
if len(payload) > 0 {
|
if len(payload) > 0 {
|
||||||
payload = ethcmn.CopyBytes(payload)
|
payload = ethcmn.CopyBytes(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var recipient *Recipient
|
||||||
|
if to != nil {
|
||||||
|
recipient = &Recipient{Address: to.String()}
|
||||||
|
}
|
||||||
|
|
||||||
txData := TxData{
|
txData := TxData{
|
||||||
AccountNonce: nonce,
|
AccountNonce: nonce,
|
||||||
Recipient: to,
|
Recipient: recipient,
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
GasLimit: gasLimit,
|
GasLimit: gasLimit,
|
||||||
Amount: new(big.Int),
|
Amount: sdk.ZeroInt(),
|
||||||
Price: new(big.Int),
|
Price: sdk.ZeroInt(),
|
||||||
V: new(big.Int),
|
V: []byte{},
|
||||||
R: new(big.Int),
|
R: []byte{},
|
||||||
S: new(big.Int),
|
S: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if amount != nil {
|
if amount != nil {
|
||||||
txData.Amount.Set(amount)
|
txData.Amount = sdk.NewIntFromBigInt(amount)
|
||||||
}
|
}
|
||||||
if gasPrice != nil {
|
if gasPrice != nil {
|
||||||
txData.Price.Set(gasPrice)
|
txData.Price = sdk.NewIntFromBigInt(gasPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
return MsgEthereumTx{Data: txData}
|
return MsgEthereumTx{Data: txData}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg MsgEthereumTx) String() string {
|
func (msg MsgEthereumTx) String() string {
|
||||||
return msg.Data.String()
|
out, _ := yaml.Marshal(msg.Data)
|
||||||
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route returns the route value of an MsgEthereumTx.
|
// Route returns the route value of an MsgEthereumTx.
|
||||||
@ -189,16 +196,16 @@ func (msg MsgEthereumTx) Type() string { return TypeMsgEthereumTx }
|
|||||||
// ValidateBasic implements the sdk.Msg interface. It performs basic validation
|
// ValidateBasic implements the sdk.Msg interface. It performs basic validation
|
||||||
// checks of a Transaction. If returns an error if validation fails.
|
// checks of a Transaction. If returns an error if validation fails.
|
||||||
func (msg MsgEthereumTx) ValidateBasic() error {
|
func (msg MsgEthereumTx) ValidateBasic() error {
|
||||||
if msg.Data.Price.Cmp(big.NewInt(0)) == 0 {
|
if msg.Data.Price.IsZero() {
|
||||||
return sdkerrors.Wrapf(types.ErrInvalidValue, "gas price cannot be 0")
|
return sdkerrors.Wrapf(types.ErrInvalidValue, "gas price cannot be 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.Data.Price.Sign() == -1 {
|
if msg.Data.Price.IsNegative() {
|
||||||
return sdkerrors.Wrapf(types.ErrInvalidValue, "gas price cannot be negative %s", msg.Data.Price)
|
return sdkerrors.Wrapf(types.ErrInvalidValue, "gas price cannot be negative %s", msg.Data.Price)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Amount can be 0
|
// Amount can be 0
|
||||||
if msg.Data.Amount.Sign() == -1 {
|
if msg.Data.Amount.IsNegative() {
|
||||||
return sdkerrors.Wrapf(types.ErrInvalidValue, "amount cannot be negative %s", msg.Data.Amount)
|
return sdkerrors.Wrapf(types.ErrInvalidValue, "amount cannot be negative %s", msg.Data.Amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +215,12 @@ func (msg MsgEthereumTx) ValidateBasic() error {
|
|||||||
// To returns the recipient address of the transaction. It returns nil if the
|
// To returns the recipient address of the transaction. It returns nil if the
|
||||||
// transaction is a contract creation.
|
// transaction is a contract creation.
|
||||||
func (msg MsgEthereumTx) To() *ethcmn.Address {
|
func (msg MsgEthereumTx) To() *ethcmn.Address {
|
||||||
return msg.Data.Recipient
|
if msg.Data.Recipient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
recipient := ethcmn.HexToAddress(msg.Data.Recipient.Address)
|
||||||
|
return &recipient
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMsgs returns a single MsgEthereumTx as an sdk.Msg.
|
// GetMsgs returns a single MsgEthereumTx as an sdk.Msg.
|
||||||
@ -242,10 +254,10 @@ func (msg MsgEthereumTx) GetSignBytes() []byte {
|
|||||||
func (msg MsgEthereumTx) RLPSignBytes(chainID *big.Int) ethcmn.Hash {
|
func (msg MsgEthereumTx) RLPSignBytes(chainID *big.Int) ethcmn.Hash {
|
||||||
return rlpHash([]interface{}{
|
return rlpHash([]interface{}{
|
||||||
msg.Data.AccountNonce,
|
msg.Data.AccountNonce,
|
||||||
msg.Data.Price,
|
msg.Data.Price.BigInt(),
|
||||||
msg.Data.GasLimit,
|
msg.Data.GasLimit,
|
||||||
msg.Data.Recipient,
|
msg.To(),
|
||||||
msg.Data.Amount,
|
msg.Data.Amount.BigInt(),
|
||||||
msg.Data.Payload,
|
msg.Data.Payload,
|
||||||
chainID, uint(0), uint(0),
|
chainID, uint(0), uint(0),
|
||||||
})
|
})
|
||||||
@ -253,7 +265,39 @@ func (msg MsgEthereumTx) RLPSignBytes(chainID *big.Int) ethcmn.Hash {
|
|||||||
|
|
||||||
// EncodeRLP implements the rlp.Encoder interface.
|
// EncodeRLP implements the rlp.Encoder interface.
|
||||||
func (msg *MsgEthereumTx) EncodeRLP(w io.Writer) error {
|
func (msg *MsgEthereumTx) EncodeRLP(w io.Writer) error {
|
||||||
return rlp.Encode(w, &msg.Data)
|
var hash ethcmn.Hash
|
||||||
|
if len(msg.Data.Hash) > 0 {
|
||||||
|
hash = ethcmn.HexToHash(msg.Data.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
AccountNonce uint64
|
||||||
|
Price *big.Int `json:"gasPrice"`
|
||||||
|
GasLimit uint64 `json:"gas"`
|
||||||
|
Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
|
||||||
|
Amount *big.Int `json:"value"`
|
||||||
|
Payload []byte `json:"input"`
|
||||||
|
|
||||||
|
// signature values
|
||||||
|
V *big.Int `json:"v"`
|
||||||
|
R *big.Int `json:"r"`
|
||||||
|
S *big.Int `json:"s"`
|
||||||
|
|
||||||
|
// hash is only used when marshaling to JSON
|
||||||
|
Hash *ethcmn.Hash `json:"hash" rlp:"-"`
|
||||||
|
}{
|
||||||
|
AccountNonce: msg.Data.AccountNonce,
|
||||||
|
Price: msg.Data.Price.BigInt(),
|
||||||
|
GasLimit: msg.Data.GasLimit,
|
||||||
|
Recipient: msg.To(),
|
||||||
|
Amount: msg.Data.Amount.BigInt(),
|
||||||
|
Payload: msg.Data.Payload,
|
||||||
|
V: new(big.Int).SetBytes(msg.Data.V),
|
||||||
|
R: new(big.Int).SetBytes(msg.Data.R),
|
||||||
|
S: new(big.Int).SetBytes(msg.Data.S),
|
||||||
|
Hash: &hash,
|
||||||
|
}
|
||||||
|
return rlp.Encode(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeRLP implements the rlp.Decoder interface.
|
// DecodeRLP implements the rlp.Decoder interface.
|
||||||
@ -264,10 +308,50 @@ func (msg *MsgEthereumTx) DecodeRLP(s *rlp.Stream) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Decode(&msg.Data); err != nil {
|
var data struct {
|
||||||
|
AccountNonce uint64
|
||||||
|
Price *big.Int `json:"gasPrice"`
|
||||||
|
GasLimit uint64 `json:"gas"`
|
||||||
|
Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
|
||||||
|
Amount *big.Int `json:"value"`
|
||||||
|
Payload []byte `json:"input"`
|
||||||
|
|
||||||
|
// signature values
|
||||||
|
V *big.Int `json:"v"`
|
||||||
|
R *big.Int `json:"r"`
|
||||||
|
S *big.Int `json:"s"`
|
||||||
|
|
||||||
|
// hash is only used when marshaling to JSON
|
||||||
|
Hash *ethcmn.Hash `json:"hash" rlp:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Decode(&data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hash string
|
||||||
|
if data.Hash != nil {
|
||||||
|
hash = data.Hash.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipient *Recipient
|
||||||
|
if data.Recipient != nil {
|
||||||
|
recipient = &Recipient{Address: data.Recipient.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Data = TxData{
|
||||||
|
AccountNonce: data.AccountNonce,
|
||||||
|
Price: sdk.NewIntFromBigInt(data.Price),
|
||||||
|
GasLimit: data.GasLimit,
|
||||||
|
Recipient: recipient,
|
||||||
|
Amount: sdk.NewIntFromBigInt(data.Amount),
|
||||||
|
Payload: data.Payload,
|
||||||
|
V: data.V.Bytes(),
|
||||||
|
R: data.R.Bytes(),
|
||||||
|
S: data.S.Bytes(),
|
||||||
|
Hash: hash,
|
||||||
|
}
|
||||||
|
|
||||||
msg.size.Store(ethcmn.StorageSize(rlp.ListSize(size)))
|
msg.size.Store(ethcmn.StorageSize(rlp.ListSize(size)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -302,15 +386,16 @@ func (msg *MsgEthereumTx) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) error {
|
|||||||
v.Add(v, chainIDMul)
|
v.Add(v, chainIDMul)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Data.V = v
|
msg.Data.V = v.Bytes()
|
||||||
msg.Data.R = r
|
msg.Data.R = r.Bytes()
|
||||||
msg.Data.S = s
|
msg.Data.S = s.Bytes()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifySig attempts to verify a Transaction's signature for a given chainID.
|
// VerifySig attempts to verify a Transaction's signature for a given chainID.
|
||||||
// A derived address is returned upon success or an error if recovery fails.
|
// A derived address is returned upon success or an error if recovery fails.
|
||||||
func (msg *MsgEthereumTx) VerifySig(chainID *big.Int) (ethcmn.Address, error) {
|
func (msg *MsgEthereumTx) VerifySig(chainID *big.Int) (ethcmn.Address, error) {
|
||||||
|
v, r, s := msg.RawSignatureValues()
|
||||||
signer := ethtypes.NewEIP155Signer(chainID)
|
signer := ethtypes.NewEIP155Signer(chainID)
|
||||||
|
|
||||||
if sc := msg.from.Load(); sc != nil {
|
if sc := msg.from.Load(); sc != nil {
|
||||||
@ -328,11 +413,11 @@ func (msg *MsgEthereumTx) VerifySig(chainID *big.Int) (ethcmn.Address, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
|
chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
|
||||||
V := new(big.Int).Sub(msg.Data.V, chainIDMul)
|
V := new(big.Int).Sub(v, chainIDMul)
|
||||||
V.Sub(V, big8)
|
V.Sub(V, big8)
|
||||||
|
|
||||||
sigHash := msg.RLPSignBytes(chainID)
|
sigHash := msg.RLPSignBytes(chainID)
|
||||||
sender, err := recoverEthSig(msg.Data.R, msg.Data.S, V, sigHash)
|
sender, err := recoverEthSig(r, s, V, sigHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ethcmn.Address{}, err
|
return ethcmn.Address{}, err
|
||||||
}
|
}
|
||||||
@ -348,25 +433,30 @@ func (msg MsgEthereumTx) GetGas() uint64 {
|
|||||||
|
|
||||||
// Fee returns gasprice * gaslimit.
|
// Fee returns gasprice * gaslimit.
|
||||||
func (msg MsgEthereumTx) Fee() *big.Int {
|
func (msg MsgEthereumTx) Fee() *big.Int {
|
||||||
return new(big.Int).Mul(msg.Data.Price, new(big.Int).SetUint64(msg.Data.GasLimit))
|
gasPrice := msg.Data.Price.BigInt()
|
||||||
|
gasLimit := new(big.Int).SetUint64(msg.Data.GasLimit)
|
||||||
|
return new(big.Int).Mul(gasPrice, gasLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChainID returns which chain id this transaction was signed for (if at all)
|
// ChainID returns which chain id this transaction was signed for (if at all)
|
||||||
func (msg *MsgEthereumTx) ChainID() *big.Int {
|
func (msg *MsgEthereumTx) ChainID() *big.Int {
|
||||||
return deriveChainID(msg.Data.V)
|
v := new(big.Int).SetBytes(msg.Data.V)
|
||||||
|
return deriveChainID(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cost returns amount + gasprice * gaslimit.
|
// Cost returns amount + gasprice * gaslimit.
|
||||||
func (msg MsgEthereumTx) Cost() *big.Int {
|
func (msg MsgEthereumTx) Cost() *big.Int {
|
||||||
total := msg.Fee()
|
total := msg.Fee()
|
||||||
total.Add(total, msg.Data.Amount)
|
total.Add(total, msg.Data.Amount.BigInt())
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawSignatureValues returns the V, R, S signature values of the transaction.
|
// RawSignatureValues returns the V, R, S signature values of the transaction.
|
||||||
// The return values should not be modified by the caller.
|
// The return values should not be modified by the caller.
|
||||||
func (msg MsgEthereumTx) RawSignatureValues() (v, r, s *big.Int) {
|
func (msg MsgEthereumTx) RawSignatureValues() (v, r, s *big.Int) {
|
||||||
return msg.Data.V, msg.Data.R, msg.Data.S
|
return new(big.Int).SetBytes(msg.Data.V),
|
||||||
|
new(big.Int).SetBytes(msg.Data.R),
|
||||||
|
new(big.Int).SetBytes(msg.Data.S)
|
||||||
}
|
}
|
||||||
|
|
||||||
// From loads the ethereum sender address from the sigcache and returns an
|
// From loads the ethereum sender address from the sigcache and returns an
|
||||||
|
@ -92,7 +92,8 @@ func TestMsgEthereumTx(t *testing.T) {
|
|||||||
|
|
||||||
msg := NewMsgEthereumTx(0, &addr, nil, 100000, nil, []byte("test"))
|
msg := NewMsgEthereumTx(0, &addr, nil, 100000, nil, []byte("test"))
|
||||||
require.NotNil(t, msg)
|
require.NotNil(t, msg)
|
||||||
require.Equal(t, *msg.Data.Recipient, addr)
|
require.NotNil(t, msg.Data.Recipient)
|
||||||
|
require.Equal(t, msg.Data.Recipient.Address, addr.String())
|
||||||
require.Equal(t, msg.Route(), RouterKey)
|
require.Equal(t, msg.Route(), RouterKey)
|
||||||
require.Equal(t, msg.Type(), TypeMsgEthereumTx)
|
require.Equal(t, msg.Type(), TypeMsgEthereumTx)
|
||||||
require.NotNil(t, msg.To())
|
require.NotNil(t, msg.To())
|
||||||
@ -102,7 +103,7 @@ func TestMsgEthereumTx(t *testing.T) {
|
|||||||
|
|
||||||
msg = NewMsgEthereumTxContract(0, nil, 100000, nil, []byte("test"))
|
msg = NewMsgEthereumTxContract(0, nil, 100000, nil, []byte("test"))
|
||||||
require.NotNil(t, msg)
|
require.NotNil(t, msg)
|
||||||
require.Nil(t, msg.Data.Recipient)
|
require.Empty(t, msg.Data.Recipient)
|
||||||
require.Nil(t, msg.To())
|
require.Nil(t, msg.To())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,11 +41,11 @@ type Params struct {
|
|||||||
// EnableCall toggles state transitions that use the vm.Call function
|
// EnableCall toggles state transitions that use the vm.Call function
|
||||||
EnableCall bool `json:"enable_call" yaml:"enable_call"`
|
EnableCall bool `json:"enable_call" yaml:"enable_call"`
|
||||||
// ExtraEIPs defines the additional EIPs for the vm.Config
|
// ExtraEIPs defines the additional EIPs for the vm.Config
|
||||||
ExtraEIPs []int `json:"extra_eips" yaml:"extra_eips"`
|
ExtraEIPs []int64 `json:"extra_eips" yaml:"extra_eips"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewParams creates a new Params instance
|
// NewParams creates a new Params instance
|
||||||
func NewParams(evmDenom string, enableCreate, enableCall bool, extraEIPs ...int) Params {
|
func NewParams(evmDenom string, enableCreate, enableCall bool, extraEIPs ...int64) Params {
|
||||||
return Params{
|
return Params{
|
||||||
EvmDenom: evmDenom,
|
EvmDenom: evmDenom,
|
||||||
EnableCreate: enableCreate,
|
EnableCreate: enableCreate,
|
||||||
@ -60,7 +60,7 @@ func DefaultParams() Params {
|
|||||||
EvmDenom: ethermint.AttoPhoton,
|
EvmDenom: ethermint.AttoPhoton,
|
||||||
EnableCreate: true,
|
EnableCreate: true,
|
||||||
EnableCall: true,
|
EnableCall: true,
|
||||||
ExtraEIPs: []int(nil), // TODO: define default values
|
ExtraEIPs: []int64(nil), // TODO: define default values
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,13 +107,13 @@ func validateBool(i interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateEIPs(i interface{}) error {
|
func validateEIPs(i interface{}) error {
|
||||||
eips, ok := i.([]int)
|
eips, ok := i.([]int64)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid EIP slice type: %T", i)
|
return fmt.Errorf("invalid EIP slice type: %T", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, eip := range eips {
|
for _, eip := range eips {
|
||||||
if !vm.ValidEip(eip) {
|
if !vm.ValidEip(int(eip)) {
|
||||||
return fmt.Errorf("EIP %d is not activateable", eip)
|
return fmt.Errorf("EIP %d is not activateable", eip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func TestParamsValidate(t *testing.T) {
|
|||||||
"invalid eip",
|
"invalid eip",
|
||||||
Params{
|
Params{
|
||||||
EvmDenom: "stake",
|
EvmDenom: "stake",
|
||||||
ExtraEIPs: []int{1},
|
ExtraEIPs: []int64{1},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
@ -57,7 +57,7 @@ func TestParamsValidatePriv(t *testing.T) {
|
|||||||
require.Error(t, validateBool(""))
|
require.Error(t, validateBool(""))
|
||||||
require.NoError(t, validateBool(true))
|
require.NoError(t, validateBool(true))
|
||||||
require.Error(t, validateEIPs(""))
|
require.Error(t, validateEIPs(""))
|
||||||
require.NoError(t, validateEIPs([]int{1884}))
|
require.NoError(t, validateEIPs([]int64{1884}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParams_String(t *testing.T) {
|
func TestParams_String(t *testing.T) {
|
||||||
|
@ -18,7 +18,6 @@ const (
|
|||||||
QueryBloom = "bloom"
|
QueryBloom = "bloom"
|
||||||
QueryLogs = "logs"
|
QueryLogs = "logs"
|
||||||
QueryAccount = "account"
|
QueryAccount = "account"
|
||||||
QueryExportAccount = "exportAccount"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// QueryResBalance is response type for balance query
|
// QueryResBalance is response type for balance query
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
|
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
|
||||||
|
|
||||||
"github.com/cosmos/ethermint/types"
|
ethermint "github.com/cosmos/ethermint/types"
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
ethstate "github.com/ethereum/go-ethereum/core/state"
|
ethstate "github.com/ethereum/go-ethereum/core/state"
|
||||||
@ -53,7 +53,7 @@ type StateObject interface {
|
|||||||
// Account values can be accessed and modified through the object.
|
// Account values can be accessed and modified through the object.
|
||||||
// Finally, call CommitTrie to write the modified storage trie into a database.
|
// Finally, call CommitTrie to write the modified storage trie into a database.
|
||||||
type stateObject struct {
|
type stateObject struct {
|
||||||
code types.Code // contract bytecode, which gets set when code is loaded
|
code ethermint.Code // contract bytecode, which gets set when code is loaded
|
||||||
// State objects are used by the consensus core and VM which are
|
// State objects are used by the consensus core and VM which are
|
||||||
// unable to deal with database-level errors. Any error that occurs
|
// unable to deal with database-level errors. Any error that occurs
|
||||||
// during a database read is memoized here and will eventually be returned
|
// during a database read is memoized here and will eventually be returned
|
||||||
@ -64,7 +64,7 @@ type stateObject struct {
|
|||||||
// DB error
|
// DB error
|
||||||
dbErr error
|
dbErr error
|
||||||
stateDB *CommitStateDB
|
stateDB *CommitStateDB
|
||||||
account *types.EthAccount
|
account *ethermint.EthAccount
|
||||||
|
|
||||||
keyToOriginStorageIndex map[ethcmn.Hash]int
|
keyToOriginStorageIndex map[ethcmn.Hash]int
|
||||||
keyToDirtyStorageIndex map[ethcmn.Hash]int
|
keyToDirtyStorageIndex map[ethcmn.Hash]int
|
||||||
@ -82,7 +82,7 @@ type stateObject struct {
|
|||||||
|
|
||||||
func newStateObject(db *CommitStateDB, accProto authexported.Account) *stateObject {
|
func newStateObject(db *CommitStateDB, accProto authexported.Account) *stateObject {
|
||||||
// func newStateObject(db *CommitStateDB, accProto authexported.Account, balance sdk.Int) *stateObject {
|
// func newStateObject(db *CommitStateDB, accProto authexported.Account, balance sdk.Int) *stateObject {
|
||||||
ethermintAccount, ok := accProto.(*types.EthAccount)
|
ethermintAccount, ok := accProto.(*ethermint.EthAccount)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("invalid account type for state object: %T", accProto))
|
panic(fmt.Sprintf("invalid account type for state object: %T", accProto))
|
||||||
}
|
}
|
||||||
@ -132,7 +132,7 @@ func (so *stateObject) SetState(db ethstate.Database, key, value ethcmn.Hash) {
|
|||||||
func (so *stateObject) setState(key, value ethcmn.Hash) {
|
func (so *stateObject) setState(key, value ethcmn.Hash) {
|
||||||
idx, ok := so.keyToDirtyStorageIndex[key]
|
idx, ok := so.keyToDirtyStorageIndex[key]
|
||||||
if ok {
|
if ok {
|
||||||
so.dirtyStorage[idx].Value = value
|
so.dirtyStorage[idx].Value = value.String()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,21 +248,24 @@ func (so *stateObject) commitState() {
|
|||||||
for _, state := range so.dirtyStorage {
|
for _, state := range so.dirtyStorage {
|
||||||
// NOTE: key is already prefixed from GetStorageByAddressKey
|
// NOTE: key is already prefixed from GetStorageByAddressKey
|
||||||
|
|
||||||
|
key := ethcmn.HexToHash(state.Key)
|
||||||
|
value := ethcmn.HexToHash(state.Value)
|
||||||
|
|
||||||
// delete empty values from the store
|
// delete empty values from the store
|
||||||
if (state.Value == ethcmn.Hash{}) {
|
if ethermint.IsEmptyHash(state.Value) {
|
||||||
store.Delete(state.Key.Bytes())
|
store.Delete(key.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(so.keyToDirtyStorageIndex, state.Key)
|
delete(so.keyToDirtyStorageIndex, key)
|
||||||
|
|
||||||
// skip no-op changes, persist actual changes
|
// skip no-op changes, persist actual changes
|
||||||
idx, ok := so.keyToOriginStorageIndex[state.Key]
|
idx, ok := so.keyToOriginStorageIndex[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.Value == ethcmn.Hash{}) {
|
if ethermint.IsEmptyHash(state.Value) {
|
||||||
delete(so.keyToOriginStorageIndex, state.Key)
|
delete(so.keyToOriginStorageIndex, key)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +274,7 @@ func (so *stateObject) commitState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
so.originStorage[idx].Value = state.Value
|
so.originStorage[idx].Value = state.Value
|
||||||
store.Set(state.Key.Bytes(), state.Value.Bytes())
|
store.Set(key.Bytes(), value.Bytes())
|
||||||
}
|
}
|
||||||
// clean storage as all entries are dirty
|
// clean storage as all entries are dirty
|
||||||
so.dirtyStorage = Storage{}
|
so.dirtyStorage = Storage{}
|
||||||
@ -348,7 +351,8 @@ func (so *stateObject) GetState(db ethstate.Database, key ethcmn.Hash) ethcmn.Ha
|
|||||||
// if we have a dirty value for this state entry, return it
|
// if we have a dirty value for this state entry, return it
|
||||||
idx, dirty := so.keyToDirtyStorageIndex[prefixKey]
|
idx, dirty := so.keyToDirtyStorageIndex[prefixKey]
|
||||||
if dirty {
|
if dirty {
|
||||||
return so.dirtyStorage[idx].Value
|
value := ethcmn.HexToHash(so.dirtyStorage[idx].Value)
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise return the entry's original value
|
// otherwise return the entry's original value
|
||||||
@ -365,23 +369,26 @@ func (so *stateObject) GetCommittedState(_ ethstate.Database, key ethcmn.Hash) e
|
|||||||
// if we have the original value cached, return that
|
// if we have the original value cached, return that
|
||||||
idx, cached := so.keyToOriginStorageIndex[prefixKey]
|
idx, cached := so.keyToOriginStorageIndex[prefixKey]
|
||||||
if cached {
|
if cached {
|
||||||
return so.originStorage[idx].Value
|
value := ethcmn.HexToHash(so.originStorage[idx].Value)
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise load the value from the KVStore
|
// otherwise load the value from the KVStore
|
||||||
state := NewState(prefixKey, ethcmn.Hash{})
|
state := NewState(prefixKey, ethcmn.Hash{})
|
||||||
|
value := ethcmn.Hash{}
|
||||||
|
|
||||||
ctx := so.stateDB.ctx
|
ctx := so.stateDB.ctx
|
||||||
store := prefix.NewStore(ctx.KVStore(so.stateDB.storeKey), AddressStoragePrefix(so.Address()))
|
store := prefix.NewStore(ctx.KVStore(so.stateDB.storeKey), AddressStoragePrefix(so.Address()))
|
||||||
rawValue := store.Get(prefixKey.Bytes())
|
rawValue := store.Get(prefixKey.Bytes())
|
||||||
|
|
||||||
if len(rawValue) > 0 {
|
if len(rawValue) > 0 {
|
||||||
state.Value.SetBytes(rawValue)
|
value.SetBytes(rawValue)
|
||||||
|
state.Value = value.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
so.originStorage = append(so.originStorage, state)
|
so.originStorage = append(so.originStorage, state)
|
||||||
so.keyToOriginStorageIndex[prefixKey] = len(so.originStorage) - 1
|
so.keyToOriginStorageIndex[prefixKey] = len(so.originStorage) - 1
|
||||||
return state.Value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -77,7 +77,7 @@ func (st StateTransition) newEVM(
|
|||||||
gasLimit uint64,
|
gasLimit uint64,
|
||||||
gasPrice *big.Int,
|
gasPrice *big.Int,
|
||||||
config ChainConfig,
|
config ChainConfig,
|
||||||
extraEIPs []int,
|
extraEIPs []int64,
|
||||||
) *vm.EVM {
|
) *vm.EVM {
|
||||||
// Create contexts for evm
|
// Create contexts for evm
|
||||||
|
|
||||||
@ -97,8 +97,13 @@ func (st StateTransition) newEVM(
|
|||||||
GasPrice: gasPrice,
|
GasPrice: gasPrice,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eips := make([]int, len(extraEIPs))
|
||||||
|
for i, eip := range extraEIPs {
|
||||||
|
eips[i] = int(eip)
|
||||||
|
}
|
||||||
|
|
||||||
vmConfig := vm.Config{
|
vmConfig := vm.Config{
|
||||||
ExtraEips: extraEIPs,
|
ExtraEips: eips,
|
||||||
}
|
}
|
||||||
|
|
||||||
return vm.NewEVM(blockCtx, txCtx, csdb, config.EthereumConfig(st.ChainID), vmConfig)
|
return vm.NewEVM(blockCtx, txCtx, csdb, config.EthereumConfig(st.ChainID), vmConfig)
|
||||||
|
@ -834,7 +834,7 @@ func (csdb *CommitStateDB) ForEachStorage(addr ethcmn.Address, cb func(key, valu
|
|||||||
|
|
||||||
if idx, dirty := so.keyToDirtyStorageIndex[key]; dirty {
|
if idx, dirty := so.keyToDirtyStorageIndex[key]; dirty {
|
||||||
// check if iteration stops
|
// check if iteration stops
|
||||||
if cb(key, so.dirtyStorage[idx].Value) {
|
if cb(key, ethcmn.HexToHash(so.dirtyStorage[idx].Value)) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -646,7 +646,7 @@ func (suite *StateDBTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
name string
|
name string
|
||||||
malleate func()
|
malleate func()
|
||||||
callback func(key, value ethcmn.Hash) (stop bool)
|
callback func(key, value ethcmn.Hash) (stop bool)
|
||||||
expValues []ethcmn.Hash
|
expValues []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"aggregate state",
|
"aggregate state",
|
||||||
@ -659,12 +659,12 @@ func (suite *StateDBTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
storage = append(storage, types.NewState(key, value))
|
storage = append(storage, types.NewState(key, value))
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
[]ethcmn.Hash{
|
[]string{
|
||||||
ethcmn.BytesToHash([]byte("value0")),
|
ethcmn.BytesToHash([]byte("value0")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value1")),
|
ethcmn.BytesToHash([]byte("value1")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value2")),
|
ethcmn.BytesToHash([]byte("value2")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value3")),
|
ethcmn.BytesToHash([]byte("value3")).String(),
|
||||||
ethcmn.BytesToHash([]byte("value4")),
|
ethcmn.BytesToHash([]byte("value4")).String(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -680,8 +680,8 @@ func (suite *StateDBTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
[]ethcmn.Hash{
|
[]string{
|
||||||
ethcmn.BytesToHash([]byte("filtervalue")),
|
ethcmn.BytesToHash([]byte("filtervalue")).String(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -696,7 +696,7 @@ func (suite *StateDBTestSuite) TestCommitStateDB_ForEachStorage() {
|
|||||||
suite.Require().NoError(err)
|
suite.Require().NoError(err)
|
||||||
suite.Require().Equal(len(tc.expValues), len(storage), fmt.Sprintf("Expected values:\n%v\nStorage Values\n%v", tc.expValues, storage))
|
suite.Require().Equal(len(tc.expValues), len(storage), fmt.Sprintf("Expected values:\n%v\nStorage Values\n%v", tc.expValues, storage))
|
||||||
|
|
||||||
vals := make([]ethcmn.Hash, len(storage))
|
vals := make([]string, len(storage))
|
||||||
for i := range storage {
|
for i := range storage {
|
||||||
vals[i] = storage[i].Value
|
vals[i] = storage[i].Value
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
|
ethermint "github.com/cosmos/ethermint/types"
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,15 +18,15 @@ type Storage []State
|
|||||||
func (s Storage) Validate() error {
|
func (s Storage) Validate() error {
|
||||||
seenStorage := make(map[string]bool)
|
seenStorage := make(map[string]bool)
|
||||||
for i, state := range s {
|
for i, state := range s {
|
||||||
if seenStorage[state.Key.String()] {
|
if seenStorage[state.Key] {
|
||||||
return sdkerrors.Wrapf(ErrInvalidState, "duplicate state key %d", i)
|
return sdkerrors.Wrapf(ErrInvalidState, "duplicate state key %d: %s", i, state.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := state.Validate(); err != nil {
|
if err := state.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
seenStorage[state.Key.String()] = true
|
seenStorage[state.Key] = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -34,7 +35,7 @@ func (s Storage) Validate() error {
|
|||||||
func (s Storage) String() string {
|
func (s Storage) String() string {
|
||||||
var str string
|
var str string
|
||||||
for _, state := range s {
|
for _, state := range s {
|
||||||
str += fmt.Sprintf("%s: %s\n", state.Key.String(), state.Value.String())
|
str += fmt.Sprintf("%s: %s\n", state.Key, state.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return str
|
return str
|
||||||
@ -50,13 +51,13 @@ func (s Storage) Copy() Storage {
|
|||||||
|
|
||||||
// State represents a single Storage key value pair item.
|
// State represents a single Storage key value pair item.
|
||||||
type State struct {
|
type State struct {
|
||||||
Key ethcmn.Hash `json:"key"`
|
Key string `json:"key"`
|
||||||
Value ethcmn.Hash `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate performs a basic validation of the State fields.
|
// Validate performs a basic validation of the State fields.
|
||||||
func (s State) Validate() error {
|
func (s State) Validate() error {
|
||||||
if bytes.Equal(s.Key.Bytes(), ethcmn.Hash{}.Bytes()) {
|
if ethermint.IsEmptyHash(s.Key) {
|
||||||
return sdkerrors.Wrap(ErrInvalidState, "state key hash cannot be empty")
|
return sdkerrors.Wrap(ErrInvalidState, "state key hash cannot be empty")
|
||||||
}
|
}
|
||||||
// NOTE: state value can be empty
|
// NOTE: state value can be empty
|
||||||
@ -64,9 +65,9 @@ func (s State) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewState creates a new State instance
|
// NewState creates a new State instance
|
||||||
func NewState(key, value ethcmn.Hash) State {
|
func NewState(key, value ethcmn.Hash) State { // nolint: interfacer
|
||||||
return State{
|
return State{
|
||||||
Key: key,
|
Key: key.String(),
|
||||||
Value: value,
|
Value: value.String(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,9 @@ package types
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStorageValidate(t *testing.T) {
|
func TestStorageValidate(t *testing.T) {
|
||||||
@ -23,15 +24,15 @@ func TestStorageValidate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"empty storage key bytes",
|
"empty storage key bytes",
|
||||||
Storage{
|
Storage{
|
||||||
{Key: ethcmn.Hash{}},
|
{Key: ethcmn.Hash{}.String()},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duplicated storage key",
|
"duplicated storage key",
|
||||||
Storage{
|
Storage{
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: ethcmn.BytesToHash([]byte{1, 2, 3}).String()},
|
||||||
{Key: ethcmn.BytesToHash([]byte{1, 2, 3})},
|
{Key: ethcmn.BytesToHash([]byte{1, 2, 3}).String()},
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
@ -62,7 +63,7 @@ func TestStorageCopy(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"empty storage key value bytes",
|
"empty storage key value bytes",
|
||||||
Storage{
|
Storage{
|
||||||
{Key: ethcmn.Hash{}, Value: ethcmn.Hash{}},
|
{Key: ethcmn.Hash{}.String(), Value: ethcmn.Hash{}.String()},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,177 +1,29 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/cosmos/ethermint/utils"
|
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Recipient is a wrapper of the
|
||||||
|
type Recipient struct {
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
// TxData implements the Ethereum transaction data structure. It is used
|
// TxData implements the Ethereum transaction data structure. It is used
|
||||||
// solely as intended in Ethereum abiding by the protocol.
|
// solely as intended in Ethereum abiding by the protocol.
|
||||||
type TxData struct {
|
type TxData struct {
|
||||||
AccountNonce uint64 `json:"nonce"`
|
AccountNonce uint64 `json:"nonce"`
|
||||||
Price *big.Int `json:"gasPrice"`
|
Price sdk.Int `json:"gasPrice"`
|
||||||
GasLimit uint64 `json:"gas"`
|
GasLimit uint64 `json:"gas"`
|
||||||
Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
|
Recipient *Recipient `json:"to" rlp:"nil"` // nil means contract creation
|
||||||
Amount *big.Int `json:"value"`
|
Amount sdk.Int `json:"value"`
|
||||||
Payload []byte `json:"input"`
|
Payload []byte `json:"input"`
|
||||||
|
|
||||||
// signature values
|
// signature values
|
||||||
V *big.Int `json:"v"`
|
V []byte `json:"v"`
|
||||||
R *big.Int `json:"r"`
|
R []byte `json:"r"`
|
||||||
S *big.Int `json:"s"`
|
S []byte `json:"s"`
|
||||||
|
|
||||||
// hash is only used when marshaling to JSON
|
// hash is only used when marshaling to JSON
|
||||||
Hash *ethcmn.Hash `json:"hash" rlp:"-"`
|
Hash string `json:"hash" rlp:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodableTxData implements the Ethereum transaction data structure. It is used
|
|
||||||
// solely as intended in Ethereum abiding by the protocol.
|
|
||||||
type encodableTxData struct {
|
|
||||||
AccountNonce uint64 `json:"nonce"`
|
|
||||||
Price string `json:"gasPrice"`
|
|
||||||
GasLimit uint64 `json:"gas"`
|
|
||||||
Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
|
|
||||||
Amount string `json:"value"`
|
|
||||||
Payload []byte `json:"input"`
|
|
||||||
|
|
||||||
// signature values
|
|
||||||
V string `json:"v"`
|
|
||||||
R string `json:"r"`
|
|
||||||
S string `json:"s"`
|
|
||||||
|
|
||||||
// hash is only used when marshaling to JSON
|
|
||||||
Hash *ethcmn.Hash `json:"hash" rlp:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (td TxData) String() string {
|
|
||||||
if td.Recipient != nil {
|
|
||||||
return fmt.Sprintf("nonce=%d price=%s gasLimit=%d recipient=%s amount=%s data=0x%x v=%s r=%s s=%s",
|
|
||||||
td.AccountNonce, td.Price, td.GasLimit, td.Recipient.Hex(), td.Amount, td.Payload, td.V, td.R, td.S)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("nonce=%d price=%s gasLimit=%d recipient=nil amount=%s data=0x%x v=%s r=%s s=%s",
|
|
||||||
td.AccountNonce, td.Price, td.GasLimit, td.Amount, td.Payload, td.V, td.R, td.S)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalAmino defines custom encoding scheme for TxData
|
|
||||||
func (td TxData) MarshalAmino() ([]byte, error) {
|
|
||||||
gasPrice, err := utils.MarshalBigInt(td.Price)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
amount, err := utils.MarshalBigInt(td.Amount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := utils.MarshalBigInt(td.V)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := utils.MarshalBigInt(td.R)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := utils.MarshalBigInt(td.S)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
e := encodableTxData{
|
|
||||||
AccountNonce: td.AccountNonce,
|
|
||||||
Price: gasPrice,
|
|
||||||
GasLimit: td.GasLimit,
|
|
||||||
Recipient: td.Recipient,
|
|
||||||
Amount: amount,
|
|
||||||
Payload: td.Payload,
|
|
||||||
V: v,
|
|
||||||
R: r,
|
|
||||||
S: s,
|
|
||||||
Hash: td.Hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
return ModuleCdc.MarshalBinaryBare(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalAmino defines custom decoding scheme for TxData
|
|
||||||
func (td *TxData) UnmarshalAmino(data []byte) error {
|
|
||||||
var e encodableTxData
|
|
||||||
err := ModuleCdc.UnmarshalBinaryBare(data, &e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
td.AccountNonce = e.AccountNonce
|
|
||||||
td.GasLimit = e.GasLimit
|
|
||||||
td.Recipient = e.Recipient
|
|
||||||
td.Payload = e.Payload
|
|
||||||
td.Hash = e.Hash
|
|
||||||
|
|
||||||
price, err := utils.UnmarshalBigInt(e.Price)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if td.Price != nil {
|
|
||||||
td.Price.Set(price)
|
|
||||||
} else {
|
|
||||||
td.Price = price
|
|
||||||
}
|
|
||||||
|
|
||||||
amt, err := utils.UnmarshalBigInt(e.Amount)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if td.Amount != nil {
|
|
||||||
td.Amount.Set(amt)
|
|
||||||
} else {
|
|
||||||
td.Amount = amt
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := utils.UnmarshalBigInt(e.V)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if td.V != nil {
|
|
||||||
td.V.Set(v)
|
|
||||||
} else {
|
|
||||||
td.V = v
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := utils.UnmarshalBigInt(e.R)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if td.R != nil {
|
|
||||||
td.R.Set(r)
|
|
||||||
} else {
|
|
||||||
td.R = r
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := utils.UnmarshalBigInt(e.S)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if td.S != nil {
|
|
||||||
td.S.Set(s)
|
|
||||||
} else {
|
|
||||||
td.S = s
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement JSON marshaling/ unmarshaling for this type
|
|
||||||
|
|
||||||
// TODO: Implement YAML marshaling/ unmarshaling for this type
|
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarshalAndUnmarshalData(t *testing.T) {
|
|
||||||
addr := GenerateEthAddress()
|
|
||||||
hash := ethcmn.BigToHash(big.NewInt(2))
|
|
||||||
|
|
||||||
txData := TxData{
|
|
||||||
AccountNonce: 2,
|
|
||||||
Price: big.NewInt(3),
|
|
||||||
GasLimit: 1,
|
|
||||||
Recipient: &addr,
|
|
||||||
Amount: big.NewInt(4),
|
|
||||||
Payload: []byte("test"),
|
|
||||||
V: big.NewInt(5),
|
|
||||||
R: big.NewInt(6),
|
|
||||||
S: big.NewInt(7),
|
|
||||||
Hash: &hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
bz, err := txData.MarshalAmino()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, bz)
|
|
||||||
|
|
||||||
var txData2 TxData
|
|
||||||
err = txData2.UnmarshalAmino(bz)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, txData, txData2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgEthereumTxAmino(t *testing.T) {
|
|
||||||
addr := GenerateEthAddress()
|
|
||||||
msg := NewMsgEthereumTx(5, &addr, big.NewInt(1), 100000, big.NewInt(3), []byte("test"))
|
|
||||||
|
|
||||||
msg.Data.V = big.NewInt(1)
|
|
||||||
msg.Data.R = big.NewInt(2)
|
|
||||||
msg.Data.S = big.NewInt(3)
|
|
||||||
|
|
||||||
raw, err := ModuleCdc.MarshalBinaryBare(msg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var msg2 MsgEthereumTx
|
|
||||||
|
|
||||||
err = ModuleCdc.UnmarshalBinaryBare(raw, &msg2)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, msg, msg2)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user