diff --git a/README.md b/README.md index 5402822..2fb9fdb 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,12 @@ Running the following commands will start a Docker container in a data directory at `/ethereum-data` and the Rosetta API accessible at port `8080`. -The `NETWORK` environment variable can be set to `MAINNET`, `ROPSTEN`, `RINKEBY`, `GOERLI` or `TESTNET` (which defaults to `ROPSTEN`). - -_It is possible to run `rosetta-ethereum` using a remote node by adding -`-e "GETH="` to any online command._ +#### Configuration Environment Variables +* `MODE` (required) - Determines if Rosetta can make outbound connections. Options: `ONLINE` or `OFFLINE`. +* `NETWORK` (required) - Ethereum network to launch and/or communicate with. Options: `MAINNET`, `ROPSTEN`, `RINKEBY`, `GOERLI` or `TESTNET` (which defaults to `ROPSTEN` for backwards compatibility). +* `PORT`(required) - Which port to use for Rosetta. +* `GETH` (optional) - Point to a remote `geth` node instead of initializing one +* `SKIP_GETH_ADMIN` (optional, default: `FALSE`) - Instruct Rosetta to not use the `geth` `admin` RPC calls. This is typically disabled by hosted blockchain node services. #### Mainnet:Online ```text diff --git a/cmd/run.go b/cmd/run.go index 726e94a..1c69439 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -91,7 +91,7 @@ func runRunCmd(cmd *cobra.Command, args []string) error { } var err error - client, err = ethereum.NewClient(cfg.GethURL, cfg.Params) + client, err = ethereum.NewClient(cfg.GethURL, cfg.Params, cfg.SkipGethAdmin) if err != nil { return fmt.Errorf("%w: cannot initialize ethereum client", err) } diff --git a/configuration/configuration.go b/configuration/configuration.go index 0b419db..a00078f 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -81,6 +81,11 @@ const ( // when GethEnv is not populated. DefaultGethURL = "http://localhost:8545" + // SkipGethAdminEnv is an optional environment variable + // to skip geth `admin` calls which are typically not supported + // by hosted node services. When not set, defaults to false. + SkipGethAdminEnv = "SKIP_GETH_ADMIN" + // MiddlewareVersion is the version of rosetta-ethereum. MiddlewareVersion = "0.0.4" ) @@ -94,6 +99,7 @@ type Configuration struct { RemoteGeth bool Port int GethArguments string + SkipGethAdmin bool // Block Reward Data Params *params.ChainConfig @@ -163,6 +169,16 @@ func LoadConfiguration() (*Configuration, error) { config.GethURL = envGethURL } + config.SkipGethAdmin = false + envSkipGethAdmin := os.Getenv(SkipGethAdminEnv) + if len(envSkipGethAdmin) > 0 { + val, err := strconv.ParseBool(envSkipGethAdmin) + if err != nil { + return nil, fmt.Errorf("%w: unable to parse SKIP_GETH_ADMIN %s", err, envSkipGethAdmin) + } + config.SkipGethAdmin = val + } + portValue := os.Getenv(PortEnv) if len(portValue) == 0 { return nil, errors.New("PORT must be populated") diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index bcb642a..a894c57 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -28,10 +28,11 @@ import ( func TestLoadConfiguration(t *testing.T) { tests := map[string]struct { - Mode string - Network string - Port string - Geth string + Mode string + Network string + Port string + Geth string + SkipGethAdmin string cfg *Configuration err error @@ -49,9 +50,10 @@ func TestLoadConfiguration(t *testing.T) { err: errors.New("PORT must be populated"), }, "all set (mainnet)": { - Mode: string(Online), - Network: Mainnet, - Port: "1000", + Mode: string(Online), + Network: Mainnet, + Port: "1000", + SkipGethAdmin: "FALSE", cfg: &Configuration{ Mode: Online, Network: &types.NetworkIdentifier{ @@ -63,13 +65,15 @@ func TestLoadConfiguration(t *testing.T) { Port: 1000, GethURL: DefaultGethURL, GethArguments: ethereum.MainnetGethArguments, + SkipGethAdmin: false, }, }, "all set (mainnet) + geth": { - Mode: string(Online), - Network: Mainnet, - Port: "1000", - Geth: "http://blah", + Mode: string(Online), + Network: Mainnet, + Port: "1000", + Geth: "http://blah", + SkipGethAdmin: "TRUE", cfg: &Configuration{ Mode: Online, Network: &types.NetworkIdentifier{ @@ -82,6 +86,7 @@ func TestLoadConfiguration(t *testing.T) { GethURL: "http://blah", RemoteGeth: true, GethArguments: ethereum.MainnetGethArguments, + SkipGethAdmin: true, }, }, "all set (ropsten)": { @@ -136,9 +141,10 @@ func TestLoadConfiguration(t *testing.T) { }, }, "all set (testnet)": { - Mode: string(Online), - Network: Testnet, - Port: "1000", + Mode: string(Online), + Network: Testnet, + Port: "1000", + SkipGethAdmin: "TRUE", cfg: &Configuration{ Mode: Online, Network: &types.NetworkIdentifier{ @@ -150,6 +156,7 @@ func TestLoadConfiguration(t *testing.T) { Port: 1000, GethURL: DefaultGethURL, GethArguments: ethereum.RopstenGethArguments, + SkipGethAdmin: true, }, }, "invalid mode": { @@ -178,6 +185,7 @@ func TestLoadConfiguration(t *testing.T) { os.Setenv(NetworkEnv, test.Network) os.Setenv(PortEnv, test.Port) os.Setenv(GethEnv, test.Geth) + os.Setenv(SkipGethAdminEnv, test.SkipGethAdmin) cfg, err := LoadConfiguration() if test.err != nil { diff --git a/ethereum/client.go b/ethereum/client.go index 52d6adc..bef9551 100644 --- a/ethereum/client.go +++ b/ethereum/client.go @@ -60,10 +60,12 @@ type Client struct { g GraphQL traceSemaphore *semaphore.Weighted + + skipAdminCalls bool } // NewClient creates a Client that from the provided url and params. -func NewClient(url string, params *params.ChainConfig) (*Client, error) { +func NewClient(url string, params *params.ChainConfig, skipAdminCalls bool) (*Client, error) { c, err := rpc.DialHTTPWithClient(url, &http.Client{ Timeout: gethHTTPTimeout, }) @@ -81,7 +83,7 @@ func NewClient(url string, params *params.ChainConfig) (*Client, error) { return nil, fmt.Errorf("%w: unable to create GraphQL client", err) } - return &Client{params, tc, c, g, semaphore.NewWeighted(maxTraceConcurrency)}, nil + return &Client{params, tc, c, g, semaphore.NewWeighted(maxTraceConcurrency), skipAdminCalls}, nil } // Close shuts down the RPC client connection. @@ -155,6 +157,11 @@ func (ec *Client) SuggestGasPrice(ctx context.Context) (*big.Int, error) { // Peers retrieves all peers of the node. func (ec *Client) peers(ctx context.Context) ([]*RosettaTypes.Peer, error) { var info []*p2p.PeerInfo + + if ec.skipAdminCalls { + return []*RosettaTypes.Peer{}, nil + } + if err := ec.c.CallContext(ctx, &info, "admin_peers"); err != nil { return nil, err } diff --git a/ethereum/client_test.go b/ethereum/client_test.go index b7e642d..85a1ea6 100644 --- a/ethereum/client_test.go +++ b/ethereum/client_test.go @@ -192,6 +192,83 @@ func TestStatus_NotSyncing(t *testing.T) { mockGraphQL.AssertExpectations(t) } +func TestStatus_NotSyncing_SkipAdminCalls(t *testing.T) { + mockJSONRPC := &mocks.JSONRPC{} + mockGraphQL := &mocks.GraphQL{} + + c := &Client{ + c: mockJSONRPC, + g: mockGraphQL, + traceSemaphore: semaphore.NewWeighted(100), + skipAdminCalls: true, + } + + ctx := context.Background() + mockJSONRPC.On( + "CallContext", + ctx, + mock.Anything, + "eth_getBlockByNumber", + "latest", + false, + ).Return( + nil, + ).Run( + func(args mock.Arguments) { + header := args.Get(1).(**types.Header) + file, err := ioutil.ReadFile("testdata/basic_header.json") + assert.NoError(t, err) + + *header = new(types.Header) + + assert.NoError(t, (*header).UnmarshalJSON(file)) + }, + ).Once() + + mockJSONRPC.On( + "CallContext", + ctx, + mock.Anything, + "eth_syncing", + ).Return( + nil, + ).Run( + func(args mock.Arguments) { + status := args.Get(1).(*json.RawMessage) + + *status = json.RawMessage("false") + }, + ).Once() + + adminPeersSkipped := true + mockJSONRPC.On( + "CallContext", + ctx, + mock.Anything, + "admin_peers", + ).Return( + nil, + ).Run( + func(args mock.Arguments) { + adminPeersSkipped = false + }, + ).Maybe() + + block, timestamp, syncStatus, peers, err := c.Status(ctx) + assert.True(t, adminPeersSkipped) + assert.Equal(t, &RosettaTypes.BlockIdentifier{ + Hash: "0x48269a339ce1489cff6bab70eff432289c4f490b81dbd00ff1f81c68de06b842", + Index: 8916656, + }, block) + assert.Equal(t, int64(1603225195000), timestamp) + assert.Nil(t, syncStatus) + assert.Equal(t, []*RosettaTypes.Peer{}, peers) + assert.NoError(t, err) + + mockJSONRPC.AssertExpectations(t) + mockGraphQL.AssertExpectations(t) +} + func TestStatus_Syncing(t *testing.T) { mockJSONRPC := &mocks.JSONRPC{} mockGraphQL := &mocks.GraphQL{} @@ -317,6 +394,88 @@ func TestStatus_Syncing(t *testing.T) { mockGraphQL.AssertExpectations(t) } +func TestStatus_Syncing_SkipAdminCalls(t *testing.T) { + mockJSONRPC := &mocks.JSONRPC{} + mockGraphQL := &mocks.GraphQL{} + + c := &Client{ + c: mockJSONRPC, + g: mockGraphQL, + traceSemaphore: semaphore.NewWeighted(100), + skipAdminCalls: true, + } + + ctx := context.Background() + mockJSONRPC.On( + "CallContext", + ctx, + mock.Anything, + "eth_getBlockByNumber", + "latest", + false, + ).Return( + nil, + ).Run( + func(args mock.Arguments) { + header := args.Get(1).(**types.Header) + file, err := ioutil.ReadFile("testdata/basic_header.json") + assert.NoError(t, err) + + *header = new(types.Header) + + assert.NoError(t, (*header).UnmarshalJSON(file)) + }, + ).Once() + + mockJSONRPC.On( + "CallContext", + ctx, + mock.Anything, + "eth_syncing", + ).Return( + nil, + ).Run( + func(args mock.Arguments) { + progress := args.Get(1).(*json.RawMessage) + file, err := ioutil.ReadFile("testdata/syncing_info.json") + assert.NoError(t, err) + + *progress = json.RawMessage(file) + }, + ).Once() + + adminPeersSkipped := true + mockJSONRPC.On( + "CallContext", + ctx, + mock.Anything, + "admin_peers", + ).Return( + nil, + ).Run( + func(args mock.Arguments) { + adminPeersSkipped = false + }, + ).Maybe() + + block, timestamp, syncStatus, peers, err := c.Status(ctx) + assert.True(t, adminPeersSkipped) + assert.Equal(t, &RosettaTypes.BlockIdentifier{ + Hash: "0x48269a339ce1489cff6bab70eff432289c4f490b81dbd00ff1f81c68de06b842", + Index: 8916656, + }, block) + assert.Equal(t, int64(1603225195000), timestamp) + assert.Equal(t, &RosettaTypes.SyncStatus{ + CurrentIndex: RosettaTypes.Int64(25), + TargetIndex: RosettaTypes.Int64(8916760), + }, syncStatus) + assert.Equal(t, []*RosettaTypes.Peer{}, peers) + assert.NoError(t, err) + + mockJSONRPC.AssertExpectations(t) + mockGraphQL.AssertExpectations(t) +} + func TestBalance(t *testing.T) { mockJSONRPC := &mocks.JSONRPC{} mockGraphQL := &mocks.GraphQL{}