Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Configuration parameter 'SkipGethAdmin' to support skipping 'admin_peers' checks #38

Merged
merged 10 commits into from
Jun 18, 2021
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ Running the following commands will start a Docker container in
a data directory at `<working directory>/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=<node url>"` 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
Expand Down
2 changes: 1 addition & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -94,6 +99,7 @@ type Configuration struct {
RemoteGeth bool
Port int
GethArguments string
SkipGethAdmin bool

// Block Reward Data
Params *params.ChainConfig
Expand Down Expand Up @@ -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")
Expand Down
36 changes: 22 additions & 14 deletions configuration/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -82,6 +86,7 @@ func TestLoadConfiguration(t *testing.T) {
GethURL: "http://blah",
RemoteGeth: true,
GethArguments: ethereum.MainnetGethArguments,
SkipGethAdmin: true,
},
},
"all set (ropsten)": {
Expand Down Expand Up @@ -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{
Expand All @@ -150,6 +156,7 @@ func TestLoadConfiguration(t *testing.T) {
Port: 1000,
GethURL: DefaultGethURL,
GethArguments: ethereum.RopstenGethArguments,
SkipGethAdmin: true,
},
},
"invalid mode": {
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 9 additions & 2 deletions ethereum/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
159 changes: 159 additions & 0 deletions ethereum/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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{}
Expand Down