diff --git a/lib/library.go b/lib/library.go index 728b3d617..91d153686 100644 --- a/lib/library.go +++ b/lib/library.go @@ -641,3 +641,15 @@ func ExportNodeLogs() *C.char { } return C.CString(string(data)) } + +// ChaosModeUpdate changes the URL of the upstream RPC client. +//export ChaosModeUpdate +func ChaosModeUpdate(on C.int) *C.char { + node := statusBackend.StatusNode() + if node == nil { + return makeJSONResponse(errors.New("node is not running")) + } + + err := node.ChaosModeCheckRPCClientsUpstreamURL(on == 1) + return makeJSONResponse(err) +} diff --git a/mobile/status.go b/mobile/status.go index d5d65ab06..d88e6a7d9 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -632,3 +632,14 @@ func ExportNodeLogs() string { } return string(data) } + +// ChaosModeUpdate sets the Chaos Mode on or off. +func ChaosModeUpdate(on bool) string { + node := statusBackend.StatusNode() + if node == nil { + return makeJSONResponse(errors.New("node is not running")) + } + + err := node.ChaosModeCheckRPCClientsUpstreamURL(on) + return makeJSONResponse(err) +} diff --git a/node/status_node.go b/node/status_node.go index 37f9857a0..0f1bb9962 100644 --- a/node/status_node.go +++ b/node/status_node.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "sync" "time" @@ -639,6 +640,35 @@ func (n *StatusNode) RPCPrivateClient() *rpc.Client { return n.rpcPrivateClient } +// ChaosModeCheckRPCClientsUpstreamURL updates RPCClient and RPCPrivateClient upstream URLs, +// if defined, without restarting the node. This is required for the Chaos Unicorn Day. +// Additionally, if the passed URL is Infura, it changes it to httpstat.us/500. +func (n *StatusNode) ChaosModeCheckRPCClientsUpstreamURL(on bool) error { + url := n.config.UpstreamConfig.URL + + if on { + if strings.Contains(url, "infura.io") { + url = "https://httpstat.us/500" + } + } + + publicClient := n.RPCClient() + if publicClient != nil { + if err := publicClient.UpdateUpstreamURL(url); err != nil { + return err + } + } + + privateClient := n.RPCPrivateClient() + if privateClient != nil { + if err := privateClient.UpdateUpstreamURL(url); err != nil { + return err + } + } + + return nil +} + // EnsureSync waits until blockchain synchronization // is complete and returns. func (n *StatusNode) EnsureSync(ctx context.Context) error { diff --git a/node/status_node_test.go b/node/status_node_test.go index 49ddb0b3c..394f646f9 100644 --- a/node/status_node_test.go +++ b/node/status_node_test.go @@ -1,9 +1,12 @@ package node import ( + "fmt" "io/ioutil" "math" "net" + "net/http" + "net/http/httptest" "os" "path" "reflect" @@ -333,3 +336,50 @@ func TestStatusNodeDiscoverNode(t *testing.T) { require.NoError(t, err) require.Equal(t, net.ParseIP("127.0.0.2").To4(), node.IP()) } + +func TestChaosModeCheckRPCClientsUpstreamURL(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{ + "id": 1, + "jsonrpc": "2.0", + "result": 1 + }`) + })) + defer ts.Close() + + config := params.NodeConfig{ + NoDiscovery: true, + ListenAddr: "127.0.0.1:0", + UpstreamConfig: params.UpstreamRPCConfig{ + Enabled: true, + // put "infura.io" substring to simulate blocking an actual infura.io URLs + URL: ts.URL + "?actualURL=infura.io", + }, + } + n := New() + require.NoError(t, n.Start(&config)) + defer func() { require.NoError(t, n.Stop()) }() + require.NotNil(t, n.RPCClient()) + + client := n.RPCClient() + require.NotNil(t, client) + + err := client.Call(nil, "net_version") + require.NoError(t, err) + + // act + err = n.ChaosModeCheckRPCClientsUpstreamURL(true) + require.NoError(t, err) + + // assert + err = client.Call(nil, "net_version") + require.EqualError(t, err, `500 Internal Server Error "500 Internal Server Error"`) + + // act + err = n.ChaosModeCheckRPCClientsUpstreamURL(false) + require.NoError(t, err) + + // assert + err = client.Call(nil, "net_version") + require.NoError(t, err) +} diff --git a/rpc/client.go b/rpc/client.go index 92fa5fad8..4173017d2 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -31,6 +31,8 @@ type Handler func(context.Context, ...interface{}) (interface{}, error) // scheme. It automatically decides where RPC call // goes - Upstream or Local node. type Client struct { + sync.RWMutex + upstreamEnabled bool upstreamURL string @@ -72,6 +74,25 @@ func NewClient(client *gethrpc.Client, upstream params.UpstreamRPCConfig) (*Clie return &c, nil } +// UpdateUpstreamURL changes the upstream RPC client URL, if the upstream is enabled. +func (c *Client) UpdateUpstreamURL(url string) error { + if c.upstream == nil { + return nil + } + + rpcClient, err := gethrpc.Dial(url) + if err != nil { + return err + } + + c.Lock() + c.upstream = rpcClient + c.upstreamURL = url + c.Unlock() + + return nil +} + // Call performs a JSON-RPC call with the given arguments and unmarshals into // result if no error occurred. // @@ -118,7 +139,10 @@ func (c *Client) CallContextIgnoringLocalHandlers(ctx context.Context, result in } if c.router.routeRemote(method) { - return c.upstream.CallContext(ctx, result, method, args...) + c.RLock() + client := c.upstream + c.RUnlock() + return client.CallContext(ctx, result, method, args...) } return c.local.CallContext(ctx, result, method, args...) diff --git a/rpc/client_test.go b/rpc/client_test.go index 06758eab9..c503ea84e 100644 --- a/rpc/client_test.go +++ b/rpc/client_test.go @@ -76,3 +76,41 @@ func TestBlockedRoutesRawCall(t *testing.T) { require.Contains(t, rawResult, fmt.Sprintf(`{"code":-32700,"message":"%s"}`, ErrMethodNotFound)) } } + +func TestUpdateUpstreamURL(t *testing.T) { + ts := createTestServer("") + defer ts.Close() + + updatedUpstreamTs := createTestServer("") + defer updatedUpstreamTs.Close() + + gethRPCClient, err := gethrpc.Dial(ts.URL) + require.NoError(t, err) + + c, err := NewClient(gethRPCClient, params.UpstreamRPCConfig{Enabled: true, URL: ts.URL}) + require.NoError(t, err) + require.Equal(t, ts.URL, c.upstreamURL) + + // cache the original upstream client + originalUpstreamClient := c.upstream + + err = c.UpdateUpstreamURL(updatedUpstreamTs.URL) + require.NoError(t, err) + // the upstream cleint instance should change + require.NotEqual(t, originalUpstreamClient, c.upstream) + require.Equal(t, updatedUpstreamTs.URL, c.upstreamURL) +} + +func createTestServer(resp string) *httptest.Server { + if resp == "" { + resp = `{ + "id": 1, + "jsonrpc": "2.0", + "result": "0x234234e22b9ffc2387e18636e0534534a3d0c56b0243567432453264c16e78a2adc" + }` + } + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, resp) + })) +}