From 32504a866f641a5a2dc762028dd9ee2d4662f5f5 Mon Sep 17 00:00:00 2001 From: Matthew Slipper Date: Thu, 9 Aug 2018 03:36:47 -0700 Subject: [PATCH] Start RPC API implementation --- Gopkg.lock | 12 +++++ Makefile | 12 ++--- cmd/{ethermintcli => emintcli}/main.go | 0 cmd/{ethermintd => emintd}/main.go | 0 server/rpc/apis.go | 39 ++++++++++++++ server/rpc/apis_test.go | 55 +++++++++++++++++++ server/rpc/config.go | 16 ++++++ server/rpc/rpc.go | 33 ++++++++++++ server/rpc/rpc_test.go | 74 ++++++++++++++++++++++++++ server/start.go | 1 - version/version.go | 13 +++++ 11 files changed, 248 insertions(+), 7 deletions(-) rename cmd/{ethermintcli => emintcli}/main.go (100%) rename cmd/{ethermintd => emintd}/main.go (100%) create mode 100644 server/rpc/apis.go create mode 100644 server/rpc/apis_test.go create mode 100644 server/rpc/config.go create mode 100644 server/rpc/rpc.go create mode 100644 server/rpc/rpc_test.go delete mode 100644 server/start.go diff --git a/Gopkg.lock b/Gopkg.lock index 4b8fd799..c40e628a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -237,6 +237,7 @@ packages = [ "assert", "require", + "suite", ] pruneopts = "T" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" @@ -275,6 +276,14 @@ pruneopts = "T" revision = "d8387025d2b9d158cf4efb07e7ebf814bcce2057" +[[projects]] + branch = "master" + digest = "1:3a6bdd02e7f2585c860e368467c5989310740af6206a1ada85cfa19c712e5afd" + name = "github.com/tendermint/ethermint" + packages = ["version"] + pruneopts = "T" + revision = "c1e6ebf80a6cc9119bc178faee18ef13490d707a" + [[projects]] digest = "1:0e2addab3f64ece97ca434b2bf2d4e8cb54a4509904a03be8c81da3fc2ddb245" name = "github.com/tendermint/go-amino" @@ -437,6 +446,7 @@ "github.com/cosmos/cosmos-sdk/wire", "github.com/cosmos/cosmos-sdk/x/auth", "github.com/ethereum/go-ethereum/common", + "github.com/ethereum/go-ethereum/common/hexutil", "github.com/ethereum/go-ethereum/common/math", "github.com/ethereum/go-ethereum/consensus", "github.com/ethereum/go-ethereum/consensus/ethash", @@ -455,6 +465,8 @@ "github.com/hashicorp/golang-lru", "github.com/pkg/errors", "github.com/stretchr/testify/require", + "github.com/stretchr/testify/suite", + "github.com/tendermint/ethermint/version", "github.com/tendermint/tendermint/libs/common", "github.com/tendermint/tendermint/libs/db", "github.com/tendermint/tendermint/libs/log", diff --git a/Makefile b/Makefile index d68cdbee..239556c4 100644 --- a/Makefile +++ b/Makefile @@ -28,16 +28,16 @@ all: tools deps install build: ifeq ($(OS),Windows_NT) - go build $(BUILD_FLAGS) -o build/$(ETHERMINT_DAEMON_BINARY).exe ./cmd/ethermintd - go build $(BUILD_FLAGS) -o build/$(ETHERMINT_CLI_BINARY).exe ./cmd/ethermintcli + go build $(BUILD_FLAGS) -o build/$(ETHERMINT_DAEMON_BINARY).exe ./cmd/emintd + go build $(BUILD_FLAGS) -o build/$(ETHERMINT_CLI_BINARY).exe ./cmd/emintcli else - go build $(BUILD_FLAGS) -o build/$(ETHERMINT_DAEMON_BINARY) ./cmd/ethermintd/ - go build $(BUILD_FLAGS) -o build/$(ETHERMINT_CLI_BINARY) ./cmd/ethermintcli/ + go build $(BUILD_FLAGS) -o build/$(ETHERMINT_DAEMON_BINARY) ./cmd/emintd/ + go build $(BUILD_FLAGS) -o build/$(ETHERMINT_CLI_BINARY) ./cmd/emintcli/ endif install: - go install $(BUILD_FLAGS) ./cmd/ethermintd - go install $(BUILD_FLAGS) ./cmd/ethermintcli + go install $(BUILD_FLAGS) ./cmd/emintd + go install $(BUILD_FLAGS) ./cmd/emintcli clean: @rm -rf ./build ./vendor diff --git a/cmd/ethermintcli/main.go b/cmd/emintcli/main.go similarity index 100% rename from cmd/ethermintcli/main.go rename to cmd/emintcli/main.go diff --git a/cmd/ethermintd/main.go b/cmd/emintd/main.go similarity index 100% rename from cmd/ethermintd/main.go rename to cmd/emintd/main.go diff --git a/server/rpc/apis.go b/server/rpc/apis.go new file mode 100644 index 00000000..ae30f0f9 --- /dev/null +++ b/server/rpc/apis.go @@ -0,0 +1,39 @@ +// Package rpc contains RPC handler methods and utilities to start +// Ethermint's Web3-compatibly JSON-RPC server. +package rpc + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rpc" + "github.com/cosmos/ethermint/version" +) + +// returns the master list of public APIs for use with StartHTTPEndpoint +func GetRPCAPIs() []rpc.API { + return []rpc.API{ + { + Namespace: "web3", + Version: "1.0", + Service: NewPublicWeb3API(), + }, + } +} + +// PublicWeb3API is the web3_ prefixed set of APIs in the WEB3 JSON-RPC spec. +type PublicWeb3API struct { +} + +func NewPublicWeb3API() *PublicWeb3API { + return &PublicWeb3API{} +} + +// ClientVersion returns the client version in the Web3 user agent format. +func (a *PublicWeb3API) ClientVersion() string { + return version.ClientVersion() +} + +// Sha3 returns the keccak-256 hash of the passed-in input. +func (a *PublicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes { + return crypto.Keccak256(input) +} diff --git a/server/rpc/apis_test.go b/server/rpc/apis_test.go new file mode 100644 index 00000000..f58c2a38 --- /dev/null +++ b/server/rpc/apis_test.go @@ -0,0 +1,55 @@ +package rpc + +import ( + "context" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/cosmos/ethermint/version" + "testing" +) + +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 TestAPIsTestSuite(t *testing.T) { + suite.Run(t, new(apisTestSuite)) +} + +func startAPIServer() (context.CancelFunc, int, error) { + config := &Config{ + RPCAddr: "127.0.0.1", + RPCPort: randomPort(), + } + ctx, cancel := context.WithCancel(context.Background()) + _, err := StartHTTPEndpoint(ctx, config, GetRPCAPIs()) + if err != nil { + return cancel, 0, err + } + return cancel, config.RPCPort, nil +} diff --git a/server/rpc/config.go b/server/rpc/config.go new file mode 100644 index 00000000..c79f58df --- /dev/null +++ b/server/rpc/config.go @@ -0,0 +1,16 @@ +package rpc + +// Config contains configuration fields that determine the +// behavior of the RPC HTTP server. +type Config struct { + // EnableRPC defines whether or not to enable the RPC server + EnableRPC bool + // RPCAddr defines the IP address to listen on + RPCAddr string + // RPCPort defines the port to listen on + RPCPort int + // RPCCORSDomains defines list of domains to enable CORS headers for (used by browsers) + RPCCORSDomains []string + // RPCVhosts defines list of domains to listen on (useful if Tendermint is addressable via DNS) + RPCVHosts []string +} diff --git a/server/rpc/rpc.go b/server/rpc/rpc.go new file mode 100644 index 00000000..641f19f2 --- /dev/null +++ b/server/rpc/rpc.go @@ -0,0 +1,33 @@ +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) (*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) + + go func() { + <-ctx.Done() + fmt.Println("Shutting down server.") + server.Stop() + }() + + return server, err +} diff --git a/server/rpc/rpc_test.go b/server/rpc/rpc_test.go new file mode 100644 index 00000000..fcf50192 --- /dev/null +++ b/server/rpc/rpc_test.go @@ -0,0 +1,74 @@ +package rpc + +import ( + "context" + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" + "io/ioutil" + "math/rand" + "net/http" + "strings" + "testing" +) + +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, + }, + }) + 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/server/start.go b/server/start.go deleted file mode 100644 index abb4e431..00000000 --- a/server/start.go +++ /dev/null @@ -1 +0,0 @@ -package server diff --git a/version/version.go b/version/version.go index 44d4e60f..ae4e0650 100644 --- a/version/version.go +++ b/version/version.go @@ -1,5 +1,13 @@ package version +import ( + "fmt" + "runtime" +) + +// AppName represents the application name as the 'user agent' on the larger Ethereum network. +const AppName = "Ethermint" + // Version contains the application semantic version. // // TODO: How do we want to version this being that an initial Ethermint has @@ -8,3 +16,8 @@ const Version = "0.0.0" // GitCommit contains the git SHA1 short hash set by build flags. var GitCommit = "" + +// ClientVersion returns the full version string for identification on the larger Ethereum network. +func ClientVersion() string { + return fmt.Sprintf("%s/%s+%s/%s/%s", AppName, Version, GitCommit, runtime.GOOS, runtime.Version()) +}