From 92dc7d9a59f5030e86bc39f29ebcb04f5b486774 Mon Sep 17 00:00:00 2001 From: David Ansermino Date: Thu, 25 Jul 2019 16:38:55 -0400 Subject: [PATCH] Basic RPC and CLI Queries (#77) - Adds ethermint query command (`emintcli query ethermint `) - Supports block number, storage, code, balance lookups - Implements RPC API methods `eth_blockNumber`, `eth_getStorageAt`, `eth_getBalance`, and `eth_getCode` - Adds tester utility for RPC calls - Adheres to go test format, but should not be run with regular suite - Requires daemon and RPC server to be running - Excluded from `make test`, available with `make test-rpc` - Implemented AppModule interface and added EVM module to app - Required for routing - Implements `InitGenesis` (`x/evm/genesis.go`) and stubs `ExportGenesis` - Modifies GenesisAccount to match expected format --- Makefile | 5 +- app/ethermint.go | 6 +- cmd/emintcli/main.go | 23 ++++++- go.mod | 1 + go.sum | 10 --- rpc/apis_test.go | 77 ----------------------- rpc/eth_api.go | 45 +++++++++++-- rpc/rpc.go | 38 ----------- rpc/rpc_test.go | 94 --------------------------- rpc/tester/tester_test.go | 129 ++++++++++++++++++++++++++++++++++++++ rpc/web3_api.go | 2 +- x/evm/client/cli/query.go | 96 ++++++++++++++++++++++++++++ x/evm/genesis.go | 36 ++++++----- x/evm/keeper.go | 20 +++++- x/evm/module.go | 60 +++++++++++++++++- x/evm/querier.go | 105 +++++++++++++++++++++++++++++++ x/evm/types/key.go | 2 + x/evm/types/querier.go | 46 ++++++++++++++ 18 files changed, 545 insertions(+), 250 deletions(-) delete mode 100644 rpc/apis_test.go delete mode 100644 rpc/rpc.go delete mode 100644 rpc/rpc_test.go create mode 100644 rpc/tester/tester_test.go create mode 100644 x/evm/client/cli/query.go create mode 100644 x/evm/querier.go create mode 100644 x/evm/types/querier.go diff --git a/Makefile b/Makefile index ffd0a37b..1694a40c 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -PACKAGES=$(shell go list ./... | grep -Ev 'vendor|importer') +PACKAGES=$(shell go list ./... | grep -Ev 'vendor|importer|rpc/tester') COMMIT_HASH := $(shell git rev-parse --short HEAD) BUILD_FLAGS = -tags netgo -ldflags "-X github.com/cosmos/ethermint/version.GitCommit=${COMMIT_HASH}" DOCKER_TAG = unstable @@ -146,6 +146,9 @@ test-import: --blockchain blockchain --timeout=5m # TODO: remove tmp directory after test run to avoid subsequent errors +test-rpc: + @${GO_MOD} go test -v --vet=off ./rpc/tester + godocs: @echo "--> Wait a few seconds and visit http://localhost:6060/pkg/github.com/cosmos/ethermint" godoc -http=:6060 diff --git a/app/ethermint.go b/app/ethermint.go index 564bc000..717f70a1 100644 --- a/app/ethermint.go +++ b/app/ethermint.go @@ -59,8 +59,7 @@ var ( crisis.AppModuleBasic{}, slashing.AppModuleBasic{}, supply.AppModuleBasic{}, - // TODO: Enable EVM AppModuleBasic - //evm.AppModuleBasic{}, + evm.AppModuleBasic{}, ) ) @@ -185,7 +184,7 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, loadLatest bool, app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, &stakingKeeper, slashingSubspace, slashing.DefaultCodespace) app.crisisKeeper = crisis.NewKeeper(crisisSubspace, invCheckPeriod, app.supplyKeeper, auth.FeeCollectorName) - app.evmKeeper = evm.NewKeeper(app.accountKeeper, app.evmStoreKey, app.evmCodeKey) + app.evmKeeper = evm.NewKeeper(app.accountKeeper, app.evmStoreKey, app.evmCodeKey, cdc) // register the proposal types govRouter := gov.NewRouter() @@ -212,6 +211,7 @@ func NewEthermintApp(logger tmlog.Logger, db dbm.DB, loadLatest bool, mint.NewAppModule(app.mintKeeper), slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper), staking.NewAppModule(app.stakingKeeper, app.distrKeeper, app.accountKeeper, app.supplyKeeper), + evm.NewAppModule(app.evmKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that diff --git a/cmd/emintcli/main.go b/cmd/emintcli/main.go index b2963b1c..abac4fec 100644 --- a/cmd/emintcli/main.go +++ b/cmd/emintcli/main.go @@ -1,6 +1,8 @@ package main import ( + "github.com/cosmos/ethermint/rpc" + "github.com/tendermint/go-amino" "os" "path" @@ -10,7 +12,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" emintapp "github.com/cosmos/ethermint/app" - "github.com/cosmos/ethermint/rpc" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/tendermint/tendermint/libs/cli" @@ -43,7 +44,7 @@ func main() { rootCmd.AddCommand( sdkrpc.StatusCommand(), client.ConfigCmd(emintapp.DefaultCLIHome), - // TODO: Set up query command + queryCmd(cdc), // TODO: Set up tx command // TODO: Set up rest routes (if included, different from web3 api) rpc.Web3RpcCmd(cdc), @@ -59,6 +60,24 @@ func main() { } } +func queryCmd(cdc *amino.Codec) *cobra.Command { + queryCmd := &cobra.Command{ + Use: "query", + Aliases: []string{"q"}, + Short: "Querying subcommands", + } + + // TODO: Possibly add these query commands from other modules + //queryCmd.AddCommand( + // ... + //) + + // add modules' query commands + emintapp.ModuleBasics.AddQueryCommands(queryCmd, cdc) + + return queryCmd +} + func initConfig(cmd *cobra.Command) error { home, err := cmd.PersistentFlags().GetString(cli.HomeFlag) if err != nil { diff --git a/go.mod b/go.mod index 554a1f52..83b84c11 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 // indirect github.com/stretchr/testify v1.3.0 github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/tendermint/go-amino v0.15.0 github.com/tendermint/tendermint v0.32.0 github.com/tyler-smith/go-bip39 v1.0.0 // indirect github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 // indirect diff --git a/go.sum b/go.sum index 3171f7f7..6bee951b 100644 --- a/go.sum +++ b/go.sum @@ -39,15 +39,11 @@ github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cosmos/cosmos-sdk v0.28.2-0.20190709220430-3f519832a7a5 h1:gakqjbZrqlUB1/rx8r/s86SVcRatOafVDfJF99yBcng= -github.com/cosmos/cosmos-sdk v0.28.2-0.20190709220430-3f519832a7a5/go.mod h1:qzvnGkt2+ynMpjmf9/dws/94/qM87awRbuyvF7r2R8Q= github.com/cosmos/cosmos-sdk v0.28.2-0.20190711105643-280734d0e37f h1:jmVM19bsHZRVVe8rugzfILuL3VPgCj5b6941I20Naw0= github.com/cosmos/cosmos-sdk v0.28.2-0.20190711105643-280734d0e37f/go.mod h1:qzvnGkt2+ynMpjmf9/dws/94/qM87awRbuyvF7r2R8Q= -github.com/cosmos/cosmos-sdk v0.35.0 h1:EPeie1aKHwnXtTzKggvabG7aAPN+DDmju2xquvjFwao= github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI= github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= @@ -124,8 +120,6 @@ github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= @@ -146,7 +140,6 @@ github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -212,7 +205,6 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -301,7 +293,6 @@ github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+m github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8= github.com/zondax/hid v0.9.0/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -340,7 +331,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7 h1:bit1t3mgdR35yN0cX0G8orgLtOuyL9Wqxa1mccLB0ig= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/rpc/apis_test.go b/rpc/apis_test.go deleted file mode 100644 index 87822fe8..00000000 --- a/rpc/apis_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package rpc - -import ( - "context" - sdkcontext"github.com/cosmos/cosmos-sdk/client/context" - "testing" - "time" - - "github.com/cosmos/ethermint/version" - "github.com/ethereum/go-ethereum/rpc" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type apisTestSuite struct { - suite.Suite - Stop context.CancelFunc - Port int -} - -func (s *apisTestSuite) SetupSuite() { - stop, port, err := startAPIServer() - require.Nil(s.T(), err, "unexpected error") - s.Stop = stop - s.Port = port -} - -func (s *apisTestSuite) TearDownSuite() { - s.Stop() -} - -func (s *apisTestSuite) TestPublicWeb3APIClientVersion() { - res, err := rpcCall(s.Port, "web3_clientVersion", []string{}) - require.Nil(s.T(), err, "unexpected error") - require.Equal(s.T(), version.ClientVersion(), res) -} - -func (s *apisTestSuite) TestPublicWeb3APISha3() { - res, err := rpcCall(s.Port, "web3_sha3", []string{"0x67656c6c6f20776f726c64"}) - require.Nil(s.T(), err, "unexpected error") - require.Equal(s.T(), "0x1b84adea42d5b7d192fd8a61a85b25abe0757e9a65cab1da470258914053823f", res) -} - -func (s *apisTestSuite) TestMiningAPIs() { - res, err := rpcCall(s.Port, "eth_mining", nil) - require.Nil(s.T(), err, "unexpected error") - require.Equal(s.T(), false, res) - - res, err = rpcCall(s.Port, "eth_hashrate", nil) - require.Nil(s.T(), err, "unexpected error") - require.Equal(s.T(), "0x0", res) -} - -func TestAPIsTestSuite(t *testing.T) { - suite.Run(t, new(apisTestSuite)) -} - -func startAPIServer() (context.CancelFunc, int, error) { - config := &Config{ - RPCAddr: "127.0.0.1", - RPCPort: randomPort(), - } - timeouts := rpc.HTTPTimeouts{ - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - IdleTimeout: 5 * time.Second, - } - - ctx, cancel := context.WithCancel(context.Background()) - - _, err := StartHTTPEndpoint(ctx, config, GetRPCAPIs(sdkcontext.NewCLIContext()), timeouts) - if err != nil { - return cancel, 0, err - } - - return cancel, config.RPCPort, nil -} diff --git a/rpc/eth_api.go b/rpc/eth_api.go index 15ab5880..78178a94 100644 --- a/rpc/eth_api.go +++ b/rpc/eth_api.go @@ -1,8 +1,10 @@ package rpc import ( + "fmt" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/ethermint/version" + "github.com/cosmos/ethermint/x/evm/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" @@ -11,7 +13,7 @@ import ( ) // PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicEthAPI struct{ +type PublicEthAPI struct { cliCtx context.CLIContext } @@ -61,18 +63,41 @@ func (e *PublicEthAPI) Accounts() []common.Address { // BlockNumber returns the current block number. func (e *PublicEthAPI) BlockNumber() *big.Int { - return big.NewInt(0) + res, _, err := e.cliCtx.QueryWithData(fmt.Sprintf("custom/%s/blockNumber", types.ModuleName), nil) + if err != nil { + fmt.Printf("could not resolve: %s\n", err) + return nil + } + + var out types.QueryResBlockNumber + e.cliCtx.Codec.MustUnmarshalJSON(res, &out) + return out.Number } // GetBalance returns the provided account's balance up to the provided block number. func (e *PublicEthAPI) GetBalance(address common.Address, blockNum rpc.BlockNumber) *hexutil.Big { - out := big.NewInt(0) - return (*hexutil.Big)(out) + res, _, err := e.cliCtx.QueryWithData(fmt.Sprintf("custom/%s/balance/%s", types.ModuleName, address), nil) + if err != nil { + fmt.Printf("could not resolve: %s\n", err) + return nil + } + + var out types.QueryResBalance + e.cliCtx.Codec.MustUnmarshalJSON(res, &out) + return (*hexutil.Big)(out.Balance) } // GetStorageAt returns the contract storage at the given address, block number, and key. func (e *PublicEthAPI) GetStorageAt(address common.Address, key string, blockNum rpc.BlockNumber) hexutil.Bytes { - return nil + res, _, err := e.cliCtx.QueryWithData(fmt.Sprintf("custom/%s/storage/%s/%s", types.ModuleName, address, key), nil) + if err != nil { + fmt.Printf("could not resolve: %s\n", err) + return nil + } + + var out types.QueryResStorage + e.cliCtx.Codec.MustUnmarshalJSON(res, &out) + return out.Value[:] } // GetTransactionCount returns the number of transactions at the given address up to the given block number. @@ -102,7 +127,15 @@ func (e *PublicEthAPI) GetUncleCountByBlockNumber(blockNum rpc.BlockNumber) hexu // GetCode returns the contract code at the given address and block number. func (e *PublicEthAPI) GetCode(address common.Address, blockNumber rpc.BlockNumber) hexutil.Bytes { - return nil + res, _, err := e.cliCtx.QueryWithData(fmt.Sprintf("custom/%s/code/%s", types.ModuleName, address), nil) + if err != nil { + fmt.Printf("could not resolve: %s\n", err) + return nil + } + + var out types.QueryResCode + e.cliCtx.Codec.MustUnmarshalJSON(res, &out) + return out.Code } // Sign signs the provided data using the private key of address via Geth's signature standard. diff --git a/rpc/rpc.go b/rpc/rpc.go deleted file mode 100644 index 1b56f63a..00000000 --- a/rpc/rpc.go +++ /dev/null @@ -1,38 +0,0 @@ -package rpc - -import ( - "context" - "fmt" - - "github.com/ethereum/go-ethereum/rpc" -) - -// StartHTTPEndpoint starts the Tendermint Web3-compatible RPC layer. Consumes -// a Context for cancellation, a config struct, and a list of rpc.API interfaces -// that will be automatically wired into a JSON-RPC webserver. -func StartHTTPEndpoint(ctx context.Context, config *Config, apis []rpc.API, timeouts rpc.HTTPTimeouts) (*rpc.Server, error) { - uniqModules := make(map[string]string) - for _, api := range apis { - uniqModules[api.Namespace] = api.Namespace - } - - modules := make([]string, len(uniqModules)) - i := 0 - for k := range uniqModules { - modules[i] = k - i++ - } - - endpoint := fmt.Sprintf("%s:%d", config.RPCAddr, config.RPCPort) - _, server, err := rpc.StartHTTPEndpoint( - endpoint, apis, modules, config.RPCCORSDomains, config.RPCVHosts, timeouts, - ) - - go func() { - <-ctx.Done() - fmt.Println("Shutting down server.") - server.Stop() - }() - - return server, err -} diff --git a/rpc/rpc_test.go b/rpc/rpc_test.go deleted file mode 100644 index 19f47e33..00000000 --- a/rpc/rpc_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package rpc - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "math/rand" - "net/http" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/rpc" - "github.com/stretchr/testify/require" -) - -type TestService struct{} - -func (s *TestService) Foo(arg string) string { - return arg -} - -func TestStartHTTPEndpointStartStop(t *testing.T) { - config := &Config{ - RPCAddr: "127.0.0.1", - RPCPort: randomPort(), - } - - ctx, cancel := context.WithCancel(context.Background()) - - _, err := StartHTTPEndpoint( - ctx, config, []rpc.API{ - { - Namespace: "test", - Version: "1.0", - Service: &TestService{}, - Public: true, - }, - }, - rpc.HTTPTimeouts{ - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - IdleTimeout: 5 * time.Second, - }, - ) - require.Nil(t, err, "unexpected error") - - res, err := rpcCall(config.RPCPort, "test_foo", []string{"baz"}) - require.Nil(t, err, "unexpected error") - - resStr := res.(string) - require.Equal(t, "baz", resStr) - - cancel() - - _, err = rpcCall(config.RPCPort, "test_foo", []string{"baz"}) - require.NotNil(t, err) -} - -func rpcCall(port int, method string, params []string) (interface{}, error) { - parsedParams, err := json.Marshal(params) - if err != nil { - return nil, err - } - - fullBody := fmt.Sprintf( - `{ "id": 1, "jsonrpc": "2.0", "method": "%s", "params": %s }`, - method, string(parsedParams), - ) - - res, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d", port), "application/json", strings.NewReader(fullBody)) - if err != nil { - return nil, err - } - - data, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, err - } - - var out map[string]interface{} - err = json.Unmarshal(data, &out) - if err != nil { - return nil, err - } - - result := out["result"].(interface{}) - return result, nil -} - -func randomPort() int { - return rand.Intn(65535-1025) + 1025 -} diff --git a/rpc/tester/tester_test.go b/rpc/tester/tester_test.go new file mode 100644 index 00000000..f9bdfbef --- /dev/null +++ b/rpc/tester/tester_test.go @@ -0,0 +1,129 @@ +// This is a test utility for Ethermint's Web3 JSON-RPC services. +// +// To run these tests please first ensure you have the emintd running +// and have started the RPC service with `emintcl rest-server`. +// +// You can configure the desired port (or host) below. + +package tester + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/cosmos/ethermint/version" + "github.com/cosmos/ethermint/x/evm/types" + "io/ioutil" + "math/big" + "net/http" + "testing" +) + +const ( + host = "127.0.0.1" + port = 1317 + addrA = "0xc94770007dda54cF92009BFF0dE90c06F603a09f" + addrAStoreKey = 0 +) + +var addr = fmt.Sprintf("http://%s:%d/rpc", host, port) + +type Request struct { + Version string `json:"jsonrpc"` + Method string `json:"method"` + Params []string `json:"params"` + Id int `json:"id"` +} + +func createRequest(method string, params []string) Request { + return Request{ + Version: "2.0", + Method: method, + Params: params, + Id: 1, + } +} + +func call(t *testing.T, method string, params []string, resp interface{}) { + req, err := json.Marshal(createRequest(method, params)) + if err != nil { + t.Error(err) + } + + res, err := http.Post(addr, "application/json", bytes.NewBuffer(req)) + if err != nil { + t.Error(err) + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Error(err) + } + + err = json.Unmarshal(body, resp) + if err != nil { + t.Error(err) + } +} + +func TestEth_protocolVersion(t *testing.T) { + expectedRes := version.ProtocolVersion + + res := &types.QueryResProtocolVersion{} + call(t, "eth_protocolVersion", []string{}, res) + + t.Logf("Got protocol version: %s\n", res.Version) + + if res.Version != expectedRes { + t.Errorf("expected: %s got: %s\n", expectedRes, res) + } +} + +func TestEth_blockNumber(t *testing.T) { + res := &types.QueryResBlockNumber{} + call(t, "eth_blockNumber", []string{}, res) + + t.Logf("Got block number: %s\n", res.Number.String()) + + // -1 if x < y, 0 if x == y; where x is res, y is 0 + if res.Number.Cmp(big.NewInt(0)) < 1 { + t.Errorf("Invalid block number got: %v", res) + } +} + +func TestEth_GetBalance(t *testing.T) { + //expectedRes := types.QueryResBalance{Balance:} + res := &types.QueryResBalance{} + call(t, "eth_getBalance", []string{addrA, "latest"}, res) + + t.Logf("Got balance %s for %s\n", res.Balance.String(), addrA) + + // 0 if x == y; where x is res, y is 0 + if res.Balance.ToInt().Cmp(big.NewInt(0)) != 0 { + t.Errorf("expected balance: %d, got: %s", 0, res.Balance.String()) + } +} + +func TestEth_GetStorageAt(t *testing.T) { + expectedRes := types.QueryResStorage{Value: []byte{}} + res := &types.QueryResStorage{} + call(t, "eth_getStorageAt", []string{addrA, string(addrAStoreKey), "latest"}, res) + + t.Logf("Got value [%X] for %s with key %X\n", res.Value, addrA, addrAStoreKey) + + if !bytes.Equal(res.Value, expectedRes.Value) { + t.Errorf("expected: %X got: %X", expectedRes.Value, res.Value) + } +} + +func TestEth_GetCode(t *testing.T) { + expectedRes := types.QueryResCode{Code: []byte{}} + res := &types.QueryResCode{} + call(t, "eth_getCode", []string{addrA, "latest"}, res) + + t.Logf("Got code [%X] for %s\n", res.Code, addrA) + if !bytes.Equal(expectedRes.Code, res.Code) { + t.Errorf("expected: %X got: %X", expectedRes.Code, res.Code) + } +} diff --git a/rpc/web3_api.go b/rpc/web3_api.go index ec7c33d0..fd8dfde5 100644 --- a/rpc/web3_api.go +++ b/rpc/web3_api.go @@ -7,7 +7,7 @@ import ( ) // PublicWeb3API is the web3_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicWeb3API struct {} +type PublicWeb3API struct{} // NewPublicWeb3API creates an instance of the Web3 API. func NewPublicWeb3API() *PublicWeb3API { diff --git a/x/evm/client/cli/query.go b/x/evm/client/cli/query.go new file mode 100644 index 00000000..8f96788e --- /dev/null +++ b/x/evm/client/cli/query.go @@ -0,0 +1,96 @@ +package cli + +import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/ethermint/x/evm/types" + "github.com/spf13/cobra" +) + +func GetQueryCmd(moduleName string, cdc *codec.Codec) *cobra.Command { + evmQueryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the evm module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + evmQueryCmd.AddCommand(client.GetCommands( + GetCmdGetBlockNumber(moduleName, cdc), + GetCmdGetStorageAt(moduleName, cdc), + GetCmdGetCode(moduleName, cdc), + )...) + return evmQueryCmd +} + +// GetCmdGetBlockNumber queries information about the current block number +func GetCmdGetBlockNumber(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "block-number", + Short: "Gets block number (block height)", + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/blockNumber", queryRoute), nil) + if err != nil { + fmt.Printf("could not resolve: %s\n", err) + return nil + } + + var out types.QueryResBlockNumber + cdc.MustUnmarshalJSON(res, &out) + return cliCtx.PrintOutput(out) + }, + } +} + +// GetCmdGetStorageAt queries a key in an accounts storage +func GetCmdGetStorageAt(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "storage [account] [key]", + Short: "Gets storage for an account at a given key", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // TODO: Validate args + account := args[0] + key := args[1] + + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/storage/%s/%s", queryRoute, account, key), nil) + if err != nil { + fmt.Printf("could not resolve: %s\n", err) + return nil + } + var out types.QueryResStorage + cdc.MustUnmarshalJSON(res, &out) + return cliCtx.PrintOutput(out) + }, + } +} + +// GetCmdGetCode queries the code field of a given address +func GetCmdGetCode(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "code [account]", + Short: "Gets code from an account", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // TODO: Validate args + account := args[0] + + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/code/%s", queryRoute, account), nil) + if err != nil { + fmt.Printf("could not resolve: %s\n", err) + return nil + } + var out types.QueryResCode + cdc.MustUnmarshalJSON(res, &out) + return cliCtx.PrintOutput(out) + }, + } +} diff --git a/x/evm/genesis.go b/x/evm/genesis.go index 6b0cee02..eaa7e062 100644 --- a/x/evm/genesis.go +++ b/x/evm/genesis.go @@ -4,6 +4,9 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/ethermint/types" + ethcmn "github.com/ethereum/go-ethereum/common" + abci "github.com/tendermint/tendermint/abci/types" + "math/big" ) type ( @@ -15,8 +18,8 @@ type ( // GenesisAccount defines an account to be initialized in the genesis state. GenesisAccount struct { - Address sdk.AccAddress `json:"address"` - Coins sdk.Coins `json:"coins"` + Address ethcmn.Address `json:"address"` + Balance *big.Int `json:"balance"` Code []byte `json:"code,omitempty"` Storage types.Storage `json:"storage,omitempty"` } @@ -24,11 +27,11 @@ type ( func ValidateGenesis(data GenesisState) error { for _, acct := range data.Accounts { - if acct.Address == nil { + if len(acct.Address.Bytes()) == 0 { return fmt.Errorf("Invalid GenesisAccount Error: Missing Address") } - if acct.Coins == nil { - return fmt.Errorf("Invalid GenesisAccount Error: Missing Coins") + if acct.Balance == nil { + return fmt.Errorf("Invalid GenesisAccount Error: Missing Balance") } } return nil @@ -40,14 +43,15 @@ func DefaultGenesisState() GenesisState { } } -// TODO: Implement these once keeper is established -//func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) []abci.ValidatorUpdate { -// for _, record := range data.Accounts { -// // TODO: Add to keeper -// } -// return []abci.ValidatorUpdate{} -//} -// -//func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { -// return GenesisState{Accounts: nil} -//} +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) []abci.ValidatorUpdate { + for _, record := range data.Accounts { + keeper.SetCode(ctx, record.Address, record.Code) + keeper.CreateGenesisAccount(ctx, record) + } + return []abci.ValidatorUpdate{} +} + +// TODO: Implement +func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { + return GenesisState{Accounts: nil} +} diff --git a/x/evm/keeper.go b/x/evm/keeper.go index f1f8ee87..f52f20ca 100644 --- a/x/evm/keeper.go +++ b/x/evm/keeper.go @@ -1,6 +1,7 @@ package evm import ( + "github.com/cosmos/cosmos-sdk/codec" ethcmn "github.com/ethereum/go-ethereum/common" ethvm "github.com/ethereum/go-ethereum/core/vm" @@ -17,14 +18,31 @@ import ( // to the StateDB interface type Keeper struct { csdb *types.CommitStateDB + cdc *codec.Codec } -func NewKeeper(ak auth.AccountKeeper, storageKey, codeKey sdk.StoreKey) Keeper { +func NewKeeper(ak auth.AccountKeeper, storageKey, codeKey sdk.StoreKey, cdc *codec.Codec) Keeper { return Keeper{ csdb: types.NewCommitStateDB(sdk.Context{}, ak, storageKey, codeKey), + cdc: cdc, } } +// ---------------------------------------------------------------------------- +// Genesis +// ---------------------------------------------------------------------------- + +// CreateGenesisAccount initializes an account and its balance, code, and storage +func (k *Keeper) CreateGenesisAccount(ctx sdk.Context, account GenesisAccount) { + csdb := k.csdb.WithContext(ctx) + csdb.SetBalance(account.Address, account.Balance) + csdb.SetCode(account.Address, account.Code) + for _, key := range account.Storage { + csdb.SetState(account.Address, key, account.Storage[key]) + } + +} + // ---------------------------------------------------------------------------- // Setters // ---------------------------------------------------------------------------- diff --git a/x/evm/module.go b/x/evm/module.go index c2c0141f..c6a1e425 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -4,11 +4,18 @@ import ( "encoding/json" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/ethermint/x/evm/client/cli" "github.com/cosmos/ethermint/x/evm/types" "github.com/gorilla/mux" "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" ) +var _ module.AppModuleBasic = AppModuleBasic{} +var _ module.AppModule = AppModule{} + // app module Basics object type AppModuleBasic struct{} @@ -42,10 +49,61 @@ func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router // Get the root query command of this module func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { - return nil // cli.GetQueryCmd(StoreKey, cdc) + return cli.GetQueryCmd(types.ModuleName, cdc) } // Get the root tx command of this module func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { return nil // cli.GetTxCmd(StoreKey, cdc) } + +type AppModule struct { + AppModuleBasic + keeper Keeper +} + +// NewAppModule creates a new AppModule Object +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +func (AppModule) Name() string { + return types.ModuleName +} + +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} + +func (am AppModule) Route() string { + return types.RouterKey +} + +func (am AppModule) NewHandler() sdk.Handler { + return nil // NewHandler(am.keeper) +} +func (am AppModule) QuerierRoute() string { + return types.ModuleName +} + +func (am AppModule) NewQuerierHandler() sdk.Querier { + return NewQuerier(am.keeper) +} + +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +func (am AppModule) EndBlock(sdk.Context, abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + types.ModuleCdc.MustUnmarshalJSON(data, &genesisState) + return InitGenesis(ctx, am.keeper, genesisState) +} + +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return types.ModuleCdc.MustMarshalJSON(gs) +} diff --git a/x/evm/querier.go b/x/evm/querier.go new file mode 100644 index 00000000..13205133 --- /dev/null +++ b/x/evm/querier.go @@ -0,0 +1,105 @@ +package evm + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ethermint/version" + "github.com/cosmos/ethermint/x/evm/types" + ethcmn "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + abci "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +// Supported endpoints +const ( + QueryProtocolVersion = "protocolVersion" + QueryBalance = "balance" + QueryBlockNumber = "blockNumber" + QueryStorage = "storage" + QueryCode = "code" +) + +// NewQuerier is the module level router for state queries +func NewQuerier(keeper Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { + switch path[0] { + case QueryProtocolVersion: + return queryProtocolVersion(keeper) + case QueryBalance: + return queryBalance(ctx, path, keeper) + case QueryBlockNumber: + return queryBlockNumber(ctx, keeper) + case QueryStorage: + return queryStorage(ctx, path, keeper) + case QueryCode: + return queryCode(ctx, path, keeper) + default: + return nil, sdk.ErrUnknownRequest("unknown query endpoint") + } + } +} + +func queryProtocolVersion(keeper Keeper) ([]byte, sdk.Error) { + vers := version.ProtocolVersion + + res, err := codec.MarshalJSONIndent(keeper.cdc, vers) + if err != nil { + panic("could not marshal result to JSON") + } + + return res, nil +} + +func queryBalance(ctx sdk.Context, path []string, keeper Keeper) ([]byte, sdk.Error) { + addr := ethcmn.BytesToAddress([]byte(path[1])) + balance := keeper.GetBalance(ctx, addr) + hBalance := &hexutil.Big{} + err := hBalance.UnmarshalText(balance.Bytes()) + if err != nil { + panic("could not marshal big.Int to hexutil.Big") + } + + bRes := types.QueryResBalance{Balance: hBalance} + res, err := codec.MarshalJSONIndent(keeper.cdc, bRes) + if err != nil { + panic("could not marshal result to JSON") + } + + return res, nil +} + +func queryBlockNumber(ctx sdk.Context, keeper Keeper) ([]byte, sdk.Error) { + num := ctx.BlockHeight() + bnRes := types.QueryResBlockNumber{Number: big.NewInt(num)} + res, err := codec.MarshalJSONIndent(keeper.cdc, bnRes) + if err != nil { + panic("could not marshal result to JSON") + } + + return res, nil +} + +func queryStorage(ctx sdk.Context, path []string, keeper Keeper) ([]byte, sdk.Error) { + addr := ethcmn.BytesToAddress([]byte(path[1])) + key := ethcmn.BytesToHash([]byte(path[2])) + val := keeper.GetState(ctx, addr, key) + bRes := types.QueryResStorage{Value: val.Bytes()} + res, err := codec.MarshalJSONIndent(keeper.cdc, bRes) + if err != nil { + panic("could not marshal result to JSON") + } + return res, nil +} + +func queryCode(ctx sdk.Context, path []string, keeper Keeper) ([]byte, sdk.Error) { + addr := ethcmn.BytesToAddress([]byte(path[1])) + code := keeper.GetCode(ctx, addr) + cRes := types.QueryResCode{Code: code} + res, err := codec.MarshalJSONIndent(keeper.cdc, cRes) + if err != nil { + panic("could not marshal result to JSON") + } + + return res, nil +} diff --git a/x/evm/types/key.go b/x/evm/types/key.go index 632b0de7..b22b328d 100644 --- a/x/evm/types/key.go +++ b/x/evm/types/key.go @@ -6,4 +6,6 @@ const ( EvmStoreKey = "evmstore" EvmCodeKey = "evmcode" + + RouterKey = ModuleName ) diff --git a/x/evm/types/querier.go b/x/evm/types/querier.go new file mode 100644 index 00000000..b6e7cd57 --- /dev/null +++ b/x/evm/types/querier.go @@ -0,0 +1,46 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + "math/big" +) + +type QueryResProtocolVersion struct { + Version string `json:"result"` +} + +func (q QueryResProtocolVersion) String() string { + return q.Version +} + +type QueryResBalance struct { + Balance *hexutil.Big `json:"result"` +} + +func (q QueryResBalance) String() string { + return q.Balance.String() +} + +type QueryResBlockNumber struct { + Number *big.Int `json:"result"` +} + +func (q QueryResBlockNumber) String() string { + return q.Number.String() +} + +type QueryResStorage struct { + Value []byte `json:"value"` +} + +func (q QueryResStorage) String() string { + return string(q.Value) +} + +type QueryResCode struct { + Code []byte +} + +func (q QueryResCode) String() string { + return string(q.Code) +}