diff --git a/geth/api/backend.go b/geth/api/backend.go index c6d16f09d..6fb1eab32 100644 --- a/geth/api/backend.go +++ b/geth/api/backend.go @@ -29,12 +29,13 @@ func NewStatusBackend() *StatusBackend { nodeManager := node.NewNodeManager() accountManager := node.NewAccountManager(nodeManager) + return &StatusBackend{ nodeManager: nodeManager, accountManager: accountManager, - txQueueManager: node.NewTxQueueManager(nodeManager, accountManager), - jailManager: jail.New(nodeManager), + jailManager: jail.New(nodeManager, accountManager), rpcManager: node.NewRPCManager(nodeManager), + txQueueManager: node.NewTxQueueManager(nodeManager, accountManager), } } diff --git a/geth/api/backend_jail_test.go b/geth/api/backend_jail_test.go index cba3e8feb..fa7b718fb 100644 --- a/geth/api/backend_jail_test.go +++ b/geth/api/backend_jail_test.go @@ -128,7 +128,7 @@ func (s *BackendTestSuite) TestJailSendQueuedTransaction() { { `["commands", "send"]`, txParams, - `{"result": {"context":{"` + jail.SendTransactionRequest + `":true},"result":{"transaction-hash":"TX_HASH"}}}`, + `{"result": {"context":{"` + params.SendTransactionMethodName + `":true},"result":{"transaction-hash":"TX_HASH"}}}`, }, { `["commands", "getBalance"]`, diff --git a/geth/common/rpccall.go b/geth/common/rpccall.go new file mode 100644 index 000000000..cde105f4b --- /dev/null +++ b/geth/common/rpccall.go @@ -0,0 +1,135 @@ +package common + +import ( + "errors" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// RPCCall represents a unit of a rpc request which is to be executed. +type RPCCall struct { + ID int64 + Method string + Params []interface{} +} + +// contains series of errors for parsing operations. +var ( + ErrInvalidFromAddress = errors.New("Failed to parse From Address") + ErrInvalidToAddress = errors.New("Failed to parse To Address") +) + +// ParseFromAddress returns the address associated with the RPCCall. +func (r RPCCall) ParseFromAddress() (gethcommon.Address, error) { + params, ok := r.Params[0].(map[string]interface{}) + if !ok { + return gethcommon.HexToAddress("0x"), ErrInvalidFromAddress + } + + from, ok := params["from"].(string) + if !ok { + return gethcommon.HexToAddress("0x"), ErrInvalidFromAddress + } + + return gethcommon.HexToAddress(from), nil +} + +// ParseToAddress returns the gethcommon.Address associated with the call. +func (r RPCCall) ParseToAddress() (gethcommon.Address, error) { + params, ok := r.Params[0].(map[string]interface{}) + if !ok { + return gethcommon.HexToAddress("0x"), ErrInvalidToAddress + } + + to, ok := params["to"].(string) + if !ok { + return gethcommon.HexToAddress("0x"), ErrInvalidToAddress + } + + return gethcommon.HexToAddress(to), nil +} + +// ParseData returns the bytes associated with the call. +func (r RPCCall) ParseData() hexutil.Bytes { + params, ok := r.Params[0].(map[string]interface{}) + if !ok { + return hexutil.Bytes("0x") + } + + data, ok := params["data"].(string) + if !ok { + data = "0x" + } + + byteCode, err := hexutil.Decode(data) + if err != nil { + byteCode = hexutil.Bytes(data) + } + + return byteCode +} + +// ParseValue returns the hex big associated with the call. +// nolint: dupl +func (r RPCCall) ParseValue() *hexutil.Big { + params, ok := r.Params[0].(map[string]interface{}) + if !ok { + return nil + //return (*hexutil.Big)(big.NewInt("0x0")) + } + + inputValue, ok := params["value"].(string) + if !ok { + return nil + } + + parsedValue, err := hexutil.DecodeBig(inputValue) + if err != nil { + return nil + } + + return (*hexutil.Big)(parsedValue) +} + +// ParseGas returns the hex big associated with the call. +// nolint: dupl +func (r RPCCall) ParseGas() *hexutil.Big { + params, ok := r.Params[0].(map[string]interface{}) + if !ok { + return nil + } + + inputValue, ok := params["gas"].(string) + if !ok { + return nil + } + + parsedValue, err := hexutil.DecodeBig(inputValue) + if err != nil { + return nil + } + + return (*hexutil.Big)(parsedValue) +} + +// ParseGasPrice returns the hex big associated with the call. +// nolint: dupl +func (r RPCCall) ParseGasPrice() *hexutil.Big { + params, ok := r.Params[0].(map[string]interface{}) + if !ok { + return nil + } + + inputValue, ok := params["gasPrice"].(string) + if !ok { + return nil + } + + parsedValue, err := hexutil.DecodeBig(inputValue) + if err != nil { + return nil + } + + return (*hexutil.Big)(parsedValue) +} diff --git a/geth/common/types.go b/geth/common/types.go index 7a1862de9..b01571785 100644 --- a/geth/common/types.go +++ b/geth/common/types.go @@ -265,6 +265,16 @@ type AccountInfo struct { Error string `json:"error"` } +// StopRPCCallError defines a error type specific for killing a execution process. +type StopRPCCallError struct { + Err error +} + +// Error returns the internal error associated with the critical error. +func (c StopRPCCallError) Error() string { + return c.Err.Error() +} + // CompleteTransactionResult is a JSON returned from transaction complete function (used in exposed method) type CompleteTransactionResult struct { ID string `json:"id"` diff --git a/geth/jail/execution_policy.go b/geth/jail/execution_policy.go new file mode 100644 index 000000000..cc568f6b6 --- /dev/null +++ b/geth/jail/execution_policy.go @@ -0,0 +1,209 @@ +package jail + +import ( + "context" + "encoding/json" + "math/big" + "time" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/robertkrimen/otto" + "github.com/status-im/status-go/geth/common" +) + +// ExecutionPolicy provides a central container for the executions of RPCCall requests for both +// remote/upstream processing and internal node processing. +type ExecutionPolicy struct { + nodeManager common.NodeManager + accountManager common.AccountManager +} + +// NewExecutionPolicy returns a new instance of ExecutionPolicy. +func NewExecutionPolicy(nodeManager common.NodeManager, accountManager common.AccountManager) *ExecutionPolicy { + return &ExecutionPolicy{ + nodeManager: nodeManager, + accountManager: accountManager, + } +} + +// ExecuteSendTransaction defines a function to execute RPC requests for eth_sendTransaction method only. +func (ep *ExecutionPolicy) ExecuteSendTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { + config, err := ep.nodeManager.NodeConfig() + if err != nil { + return nil, err + } + + if config.UpstreamConfig.Enabled { + return ep.executeRemoteSendTransaction(req, call) + } + + return ep.executeLocalSendTransaction(req, call) +} + +// executeRemoteSendTransaction defines a function to execute RPC method eth_sendTransaction over the upstream server. +func (ep *ExecutionPolicy) executeRemoteSendTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { + config, err := ep.nodeManager.NodeConfig() + if err != nil { + return nil, err + } + + selectedAcct, err := ep.accountManager.SelectedAccount() + if err != nil { + return nil, err + } + + client, err := ep.nodeManager.RPCClient() + if err != nil { + return nil, err + } + + fromAddr, err := req.ParseFromAddress() + if err != nil { + return nil, err + } + + toAddr, err := req.ParseToAddress() + if err != nil { + return nil, err + } + + // We need to request a new transaction nounce from upstream node. + ctx, canceller := context.WithTimeout(context.Background(), time.Minute) + defer canceller() + + var num hexutil.Uint + if err := client.CallContext(ctx, &num, "eth_getTransactionCount", fromAddr, "pending"); err != nil { + return nil, err + } + + nonce := uint64(num) + gas := (*big.Int)(req.ParseGas()) + dataVal := []byte(req.ParseData()) + priceVal := (*big.Int)(req.ParseValue()) + gasPrice := (*big.Int)(req.ParseGasPrice()) + chainID := big.NewInt(int64(config.NetworkID)) + + tx := types.NewTransaction(nonce, toAddr, priceVal, gas, gasPrice, dataVal) + txs, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAcct.AccountKey.PrivateKey) + if err != nil { + return nil, err + } + + // Attempt to get the hex version of the transaction. + txBytes, err := rlp.EncodeToBytes(txs) + if err != nil { + return nil, err + } + + //TODO(influx6): Should we use a single context with a higher timeout, say 3-5 minutes + // for calls to rpcClient? + ctx2, canceler2 := context.WithTimeout(context.Background(), time.Minute) + defer canceler2() + + var result json.RawMessage + if err := client.CallContext(ctx2, &result, "eth_sendRawTransaction", gethcommon.ToHex(txBytes)); err != nil { + return nil, err + } + + resp, err := call.Otto.Object(`({"jsonrpc":"2.0"})`) + if err != nil { + return nil, err + } + + resp.Set("id", req.ID) + resp.Set("result", result) + resp.Set("hash", txs.Hash().String()) + + return resp, nil +} + +// executeLocalSendTransaction defines a function which handles execution of RPC method over the internal rpc server +// from the eth.LightClient. It specifically caters to process eth_sendTransaction. +func (ep *ExecutionPolicy) executeLocalSendTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { + resp, err := call.Otto.Object(`({"jsonrpc":"2.0"})`) + if err != nil { + return nil, err + } + + resp.Set("id", req.ID) + + txHash, err := processRPCCall(ep.nodeManager, req, call) + resp.Set("result", txHash.Hex()) + + if err != nil { + resp = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object() + return resp, nil + } + + return resp, nil +} + +// ExecuteOtherTransaction defines a function which handles the processing of non `eth_sendTransaction` +// rpc request to the internal node server. +func (ep *ExecutionPolicy) ExecuteOtherTransaction(req common.RPCCall, call otto.FunctionCall) (*otto.Object, error) { + client, err := ep.nodeManager.RPCClient() + if err != nil { + return nil, common.StopRPCCallError{Err: err} + } + + JSON, err := call.Otto.Object("JSON") + if err != nil { + return nil, err + } + + var result json.RawMessage + + resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) + resp.Set("id", req.ID) + + // do extra request pre processing (persist message id) + // within function semaphore will be acquired and released, + // so that no more than one client (per cell) can enter + messageID, err := preProcessRequest(call.Otto, req) + if err != nil { + return nil, common.StopRPCCallError{Err: err} + } + + err = client.Call(&result, req.Method, req.Params...) + + switch err := err.(type) { + case nil: + if result == nil { + + // Special case null because it is decoded as an empty + // raw message for some reason. + resp.Set("result", otto.NullValue()) + + } else { + + resultVal, callErr := JSON.Call("parse", string(result)) + + if callErr != nil { + resp = newErrorResponse(call.Otto, -32603, callErr.Error(), &req.ID).Object() + } else { + resp.Set("result", resultVal) + } + + } + + case rpc.Error: + + resp.Set("error", map[string]interface{}{ + "code": err.ErrorCode(), + "message": err.Error(), + }) + + default: + + resp = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object() + } + + // do extra request post processing (setting back tx context) + postProcessRequest(call.Otto, req, messageID) + + return resp, nil +} diff --git a/geth/jail/handlers.go b/geth/jail/handlers.go index 1b96641cd..0bb3d36da 100644 --- a/geth/jail/handlers.go +++ b/geth/jail/handlers.go @@ -97,7 +97,7 @@ func makeSendHandler(jail *Jail) func(call otto.FunctionCall) (response otto.Val // makeJethIsConnectedHandler returns jeth.isConnected() handler func makeJethIsConnectedHandler(jail *Jail) func(call otto.FunctionCall) (response otto.Value) { return func(call otto.FunctionCall) otto.Value { - client, err := jail.requestManager.RPCClient() + client, err := jail.nodeManager.RPCClient() if err != nil { return newErrorResponse(call.Otto, -32603, err.Error(), nil) } diff --git a/geth/jail/jail.go b/geth/jail/jail.go index fd3f6670f..cb69e0696 100644 --- a/geth/jail/jail.go +++ b/geth/jail/jail.go @@ -1,15 +1,19 @@ package jail import ( + "context" "encoding/json" "errors" "fmt" "sync" - "github.com/ethereum/go-ethereum/rpc" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/les/status" + "github.com/ethereum/go-ethereum/log" "github.com/robertkrimen/otto" "github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/log" + "github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/static" "fknsrs.biz/p/ottoext/loop" @@ -28,16 +32,21 @@ var ( type Jail struct { // FIXME(tiabc): This mutex handles cells field access and must be renamed appropriately: cellsMutex sync.RWMutex - requestManager *RequestManager + nodeManager common.NodeManager + accountManager common.AccountManager + policy *ExecutionPolicy cells map[string]*JailCell // jail supports running many isolated instances of jailed runtime baseJSCode string // JavaScript used to initialize all new cells with } -// New returns new Jail environment. -func New(nodeManager common.NodeManager) *Jail { +// New returns new Jail environment with the associated NodeManager and +// AccountManager. +func New(nodeManager common.NodeManager, accountManager common.AccountManager) *Jail { return &Jail{ + nodeManager: nodeManager, + accountManager: accountManager, cells: make(map[string]*JailCell), - requestManager: NewRequestManager(nodeManager), + policy: NewExecutionPolicy(nodeManager, accountManager), } } @@ -167,91 +176,52 @@ func (jail *Jail) Call(chatID string, path string, args string) string { // Send will serialize the first argument, send it to the node and returns the response. // nolint: errcheck, unparam func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { - client, err := jail.requestManager.RPCClient() - if err != nil { - return newErrorResponse(call.Otto, -32603, err.Error(), nil) - } - // Remarshal the request into a Go value. JSON, _ := call.Otto.Object("JSON") reqVal, err := JSON.Call("stringify", call.Argument(0)) if err != nil { throwJSException(err.Error()) } + var ( rawReq = []byte(reqVal.String()) - reqs []RPCCall + reqs []common.RPCCall batch bool ) + if rawReq[0] == '[' { batch = true json.Unmarshal(rawReq, &reqs) } else { batch = false - reqs = make([]RPCCall, 1) + reqs = make([]common.RPCCall, 1) json.Unmarshal(rawReq, &reqs[0]) } - // Execute the requests. resps, _ := call.Otto.Object("new Array()") + + // Execute the requests. for _, req := range reqs { - resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) - resp.Set("id", req.ID) - var result json.RawMessage + var resErr error + var res *otto.Object - // execute directly w/o RPC call to node - if req.Method == SendTransactionRequest { - txHash, err := jail.requestManager.ProcessSendTransactionRequest(call.Otto, req) - resp.Set("result", txHash.Hex()) - if err != nil { - resp = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object() - } - resps.Call("push", resp) - continue - } - - // do extra request pre processing (persist message id) - // within function semaphore will be acquired and released, - // so that no more than one client (per cell) can enter - messageID, err := jail.requestManager.PreProcessRequest(call.Otto, req) - if err != nil { - return newErrorResponse(call.Otto, -32603, err.Error(), nil) - } - - errc := make(chan error, 1) - errc2 := make(chan error) - go func() { - errc2 <- <-errc - }() - errc <- client.Call(&result, req.Method, req.Params...) - err = <-errc2 - - switch err := err.(type) { - case nil: - if result == nil { - // Special case null because it is decoded as an empty - // raw message for some reason. - resp.Set("result", otto.NullValue()) - } else { - resultVal, callErr := JSON.Call("parse", string(result)) - if callErr != nil { - resp = newErrorResponse(call.Otto, -32603, callErr.Error(), &req.ID).Object() - } else { - resp.Set("result", resultVal) - } - } - case rpc.Error: - resp.Set("error", map[string]interface{}{ - "code": err.ErrorCode(), - "message": err.Error(), - }) + switch req.Method { + case params.SendTransactionMethodName: + res, resErr = jail.policy.ExecuteSendTransaction(req, call) default: - resp = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object() + res, resErr = jail.policy.ExecuteOtherTransaction(req, call) } - resps.Call("push", resp) - // do extra request post processing (setting back tx context) - jail.requestManager.PostProcessRequest(call.Otto, req, messageID) + if resErr != nil { + switch resErr.(type) { + case common.StopRPCCallError: + return newErrorResponse(call.Otto, -32603, err.Error(), nil) + default: + res = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object() + } + } + + resps.Call("push", res) } // Return the responses either to the callback (if supplied) @@ -261,18 +231,115 @@ func (jail *Jail) Send(call otto.FunctionCall) (response otto.Value) { } else { response, _ = resps.Get("0") } + if fn := call.Argument(1); fn.Class() == "Function" { fn.Call(otto.NullValue(), otto.NullValue(), response) return otto.UndefinedValue() } + return response } -func newErrorResponse(otto *otto.Otto, code int, msg string, id interface{}) otto.Value { +//================================================================================================================================== + +func processRPCCall(manager common.NodeManager, req common.RPCCall, call otto.FunctionCall) (gethcommon.Hash, error) { + lightEthereum, err := manager.LightEthereumService() + if err != nil { + return gethcommon.Hash{}, err + } + + backend := lightEthereum.StatusBackend + + messageID, err := preProcessRequest(call.Otto, req) + if err != nil { + return gethcommon.Hash{}, err + } + + // onSendTransactionRequest() will use context to obtain and release ticket + ctx := context.Background() + ctx = context.WithValue(ctx, common.MessageIDKey, messageID) + + // this call blocks, up until Complete Transaction is called + txHash, err := backend.SendTransaction(ctx, sendTxArgsFromRPCCall(req)) + if err != nil { + return gethcommon.Hash{}, err + } + + // invoke post processing + postProcessRequest(call.Otto, req, messageID) + + return txHash, nil +} + +// preProcessRequest pre-processes a given RPC call to a given Otto VM +func preProcessRequest(vm *otto.Otto, req common.RPCCall) (string, error) { + messageID := currentMessageID(vm.Context()) + + return messageID, nil +} + +// postProcessRequest post-processes a given RPC call to a given Otto VM +func postProcessRequest(vm *otto.Otto, req common.RPCCall, messageID string) { + if len(messageID) > 0 { + vm.Call("addContext", nil, messageID, common.MessageIDKey, messageID) // nolint: errcheck + } + + // set extra markers for queued transaction requests + if req.Method == params.SendTransactionMethodName { + vm.Call("addContext", nil, messageID, params.SendTransactionMethodName, true) // nolint: errcheck + } +} + +func sendTxArgsFromRPCCall(req common.RPCCall) status.SendTxArgs { + if req.Method != params.SendTransactionMethodName { // no need to persist extra state for other requests + return status.SendTxArgs{} + } + + var err error + var fromAddr, toAddr gethcommon.Address + + fromAddr, err = req.ParseFromAddress() + if err != nil { + fromAddr = gethcommon.HexToAddress("0x0") + } + + toAddr, err = req.ParseToAddress() + if err != nil { + toAddr = gethcommon.HexToAddress("0x0") + } + + return status.SendTxArgs{ + To: &toAddr, + From: fromAddr, + Value: req.ParseValue(), + Data: req.ParseData(), + Gas: req.ParseGas(), + GasPrice: req.ParseGasPrice(), + } +} + +// currentMessageID looks for `status.message_id` variable in current JS context +func currentMessageID(ctx otto.Context) string { + if statusObj, ok := ctx.Symbols["status"]; ok { + messageID, err := statusObj.Object().Get("message_id") + if err != nil { + return "" + } + if messageID, err := messageID.ToString(); err == nil { + return messageID + } + } + + return "" +} + +//========================================================================================================== + +func newErrorResponse(vm *otto.Otto, code int, msg string, id interface{}) otto.Value { // Bundle the error into a JSON RPC call response m := map[string]interface{}{"jsonrpc": "2.0", "id": id, "error": map[string]interface{}{"code": code, msg: msg}} res, _ := json.Marshal(m) - val, _ := otto.Run("(" + string(res) + ")") + val, _ := vm.Run("(" + string(res) + ")") return val } diff --git a/geth/jail/jail_cell_test.go b/geth/jail/jail_cell_test.go index be169601d..a36efe115 100644 --- a/geth/jail/jail_cell_test.go +++ b/geth/jail/jail_cell_test.go @@ -92,7 +92,7 @@ func (s *JailTestSuite) TestJailLoopInCall() { require := s.Require() require.NotNil(s.jail) - s.StartTestNode(params.RopstenNetworkID) + s.StartTestNode(params.RopstenNetworkID, true) defer s.StopTestNode() // load Status JS and add test command to it diff --git a/geth/jail/jail_rpc_test.go b/geth/jail/jail_rpc_test.go new file mode 100644 index 000000000..0f968c86f --- /dev/null +++ b/geth/jail/jail_rpc_test.go @@ -0,0 +1,357 @@ +package jail_test + +import ( + "testing" + "time" + + "encoding/json" + "net/http" + "net/http/httptest" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/robertkrimen/otto" + "github.com/status-im/status-go/geth/common" + "github.com/status-im/status-go/geth/jail" + "github.com/status-im/status-go/geth/node" + "github.com/status-im/status-go/geth/params" + . "github.com/status-im/status-go/geth/testing" + "github.com/stretchr/testify/suite" +) + +type txRequest struct { + Method string `json:"method"` + Version string `json:"jsonrpc"` + ID int `json:"id,omitempty"` + Payload json.RawMessage `json:"params,omitempty"` +} + +type service struct { + Handler http.HandlerFunc +} + +func (s service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.Handler(w, r) +} + +//================================================================================================== + +func TestJailRPCTestSuite(t *testing.T) { + suite.Run(t, new(JailRPCTestSuite)) +} + +type JailRPCTestSuite struct { + BaseTestSuite + Account common.AccountManager + Policy *jail.ExecutionPolicy +} + +func (s *JailRPCTestSuite) SetupTest() { + require := s.Require() + + nodeman := node.NewNodeManager() + require.NotNil(nodeman) + + acctman := node.NewAccountManager(nodeman) + require.NotNil(acctman) + + policy := jail.NewExecutionPolicy(nodeman, acctman) + require.NotNil(policy) + + s.Policy = policy + s.Account = acctman + s.NodeManager = nodeman +} + +func (s *JailRPCTestSuite) TestSendTransaction() { + require := s.Require() + require.NotNil(s.NodeManager) + + odFunc := otto.FunctionCall{ + Otto: otto.New(), + This: otto.NullValue(), + } + + request := common.RPCCall{ + ID: 65454545334343, + Method: "eth_sendTransaction", + Params: []interface{}{ + map[string]interface{}{ + "from": TestConfig.Account1.Address, + "to": "0xe410006cad020e3690c8ba21ed8b0f065dde2453", + "value": "0x2", + "nonce": "0x1", + "data": "Will-power", + "gasPrice": "0x4a817c800", + "gasLimit": "0x5208", + "chainId": 3391, + }, + }, + } + + nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID) + require.NoError(err) + + nodeConfig.UpstreamConfig.Enabled = true + + rpcService := new(service) + rpcService.Handler = func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var txReq txRequest + + if err := json.NewDecoder(r.Body).Decode(&txReq); err != nil { + require.NoError(err) + return + } + + switch txReq.Method { + case "eth_getTransactionCount": + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"jsonrpc": "2.0", "status":200, "result": "0x434"}`)) + return + } + + payload := ([]byte)(txReq.Payload) + + var bu []interface{} + + jserr := json.Unmarshal(payload, &bu) + require.NoError(jserr) + require.NotNil(bu) + require.Len(bu, 1) + + buElem, ok := bu[0].(string) + require.Equal(ok, true) + + decoded, err := hexutil.Decode(buElem) + require.NoError(err) + require.NotNil(decoded) + + var tx types.Transaction + decodeErr := rlp.DecodeBytes(decoded, &tx) + require.NoError(decodeErr) + + // Validate we are receiving transaction from the proper network chain. + require.Equal(tx.ChainId().Int64(), int64(nodeConfig.NetworkID)) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"jsonrpc": "2.0", "status":200, "result": "3434=done"}`)) + } + + httpRPCServer := httptest.NewServer(rpcService) + nodeConfig.UpstreamConfig.URL = httpRPCServer.URL + + // Start NodeManagers Node + started, err := s.NodeManager.StartNode(nodeConfig) + require.NoError(err) + + select { + case <-started: + break + case <-time.After(1 * time.Second): + require.Fail("Failed to start NodeManager") + break + } + + defer s.NodeManager.StopNode() + + client, err := s.NodeManager.RPCClient() + require.NoError(err) + require.NotNil(client) + + selectErr := s.Account.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password) + require.NoError(selectErr) + + res, err := s.Policy.ExecuteSendTransaction(request, odFunc) + require.NoError(err) + + result, err := res.Get("result") + require.NoError(err) + require.NotNil(result) + + exported, err := result.Export() + require.NoError(err) + + rawJSON, ok := exported.(json.RawMessage) + require.True(ok, "Expected raw json payload") + require.Equal(string(rawJSON), "\"3434=done\"") +} + +// func (s *JailRPCTestSuite) TestMainnetAcceptance() { +// require := s.Require() +// require.NotNil(s.NodeManager) + +// odFunc := otto.FunctionCall{ +// Otto: otto.New(), +// This: otto.NullValue(), +// } + +// request := common.RPCCall{ +// ID: 65454545334343, +// Method: "eth_sendTransaction", +// Params: []interface{}{ +// map[string]interface{}{ +// "from": TestConfig.Account1.Address, +// "to": "0xe410006cad020e3690c8ba21ed8b0f065dde2453", +// "value": "0x2", +// "nonce": "0x1", +// "data": "Will-power", +// "gasPrice": "0x4a817c800", +// "gasLimit": "0x5208", +// "chainId": 3391, +// }, +// }, +// } + +// nodeConfig, err := MakeTestNodeConfig(params.MainNetworkID) +// require.NoError(err) + +// nodeConfig.UpstreamConfig.Enabled = true + +// // Start NodeManagers Node +// started, err := s.NodeManager.StartNode(nodeConfig) +// require.NoError(err) + +// select { +// case <-started: +// break +// case <-time.After(1 * time.Second): +// require.Fail("Failed to start NodeManager") +// break +// } + +// defer s.NodeManager.StopNode() + +// client, err := s.NodeManager.RPCClient() +// require.NoError(err) +// require.NotNil(client) + +// selectErr := s.Account.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password) +// require.NoError(selectErr) + +// res, err := s.Policy.ExecuteSendTransaction(request, odFunc) +// require.NoError(err) + +// _, err = res.Get("hash") +// require.NoError(err) +// } + +// func (s *JailRPCTestSuite) TestRobstenAcceptance() { +// require := s.Require() +// require.NotNil(s.NodeManager) + +// odFunc := otto.FunctionCall{ +// Otto: otto.New(), +// This: otto.NullValue(), +// } + +// request := common.RPCCall{ +// ID: 65454545334343, +// Method: "eth_sendTransaction", +// Params: []interface{}{ +// map[string]interface{}{ +// "from": TestConfig.Account1.Address, +// "to": "0xe410006cad020e3690c8ba21ed8b0f065dde2453", +// "value": "0x2", +// "nonce": "0x1", +// "data": "Will-power", +// "gasPrice": "0x4a817c800", +// "gasLimit": "0x5208", +// "chainId": 3391, +// }, +// }, +// } + +// nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID) +// require.NoError(err) + +// nodeConfig.UpstreamConfig.Enabled = true + +// // Start NodeManagers Node +// started, err := s.NodeManager.StartNode(nodeConfig) +// require.NoError(err) + +// select { +// case <-started: +// break +// case <-time.After(1 * time.Second): +// require.Fail("Failed to start NodeManager") +// break +// } + +// defer s.NodeManager.StopNode() + +// client, err := s.NodeManager.RPCClient() +// require.NoError(err) +// require.NotNil(client) + +// selectErr := s.Account.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password) +// require.NoError(selectErr) + +// res, err := s.Policy.ExecuteSendTransaction(request, odFunc) +// require.NoError(err) + +// _, err = res.Get("hash") +// require.NoError(err) +// } + +// func (s *JailRPCTestSuite) TestRinkebyAcceptance() { +// require := s.Require() +// require.NotNil(s.NodeManager) + +// odFunc := otto.FunctionCall{ +// Otto: otto.New(), +// This: otto.NullValue(), +// } + +// request := common.RPCCall{ +// ID: 65454545334343, +// Method: "eth_sendTransaction", +// Params: []interface{}{ +// map[string]interface{}{ +// "from": TestConfig.Account1.Address, +// "to": "0xe410006cad020e3690c8ba21ed8b0f065dde2453", +// "value": "0x2", +// "nonce": "0x1", +// "data": "Will-power", +// "gasPrice": "0x4a817c800", +// "gasLimit": "0x5208", +// "chainId": 3391, +// }, +// }, +// } + +// nodeConfig, err := MakeTestNodeConfig(params.RinkebyNetworkID) +// require.NoError(err) + +// nodeConfig.UpstreamConfig.Enabled = true + +// // Start NodeManagers Node +// started, err := s.NodeManager.StartNode(nodeConfig) +// require.NoError(err) + +// select { +// case <-started: +// break +// case <-time.After(1 * time.Second): +// require.Fail("Failed to start NodeManager") +// break +// } + +// defer s.NodeManager.StopNode() + +// client, err := s.NodeManager.RPCClient() +// require.NoError(err) +// require.NotNil(client) + +// selectErr := s.Account.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password) +// require.NoError(selectErr) + +// res, err := s.Policy.ExecuteSendTransaction(request, odFunc) +// require.NoError(err) + +// _, err = res.Get("hash") +// require.NoError(err) +// } diff --git a/geth/jail/jail_test.go b/geth/jail/jail_test.go index 64dc231ab..1b549f10f 100644 --- a/geth/jail/jail_test.go +++ b/geth/jail/jail_test.go @@ -30,12 +30,19 @@ type JailTestSuite struct { } func (s *JailTestSuite) SetupTest() { - s.NodeManager = node.NewNodeManager() - s.Require().NotNil(s.NodeManager) - s.Require().IsType(&node.NodeManager{}, s.NodeManager) - s.jail = jail.New(s.NodeManager) - s.Require().NotNil(s.jail) - s.Require().IsType(&jail.Jail{}, s.jail) + require := s.Require() + + nodeManager := node.NewNodeManager() + require.NotNil(nodeManager) + + accountManager := node.NewAccountManager(nodeManager) + require.NotNil(accountManager) + + jail := jail.New(nodeManager, accountManager) + require.NotNil(jail) + + s.jail = jail + s.NodeManager = nodeManager } func (s *JailTestSuite) TestInit() { @@ -119,7 +126,7 @@ func (s *JailTestSuite) TestJailRPCSend() { require := s.Require() require.NotNil(s.jail) - s.StartTestNode(params.RopstenNetworkID) + s.StartTestNode(params.RopstenNetworkID, false) defer s.StopTestNode() // load Status JS and add test command to it @@ -154,7 +161,8 @@ func (s *JailTestSuite) TestIsConnected() { require.NotNil(s.jail) // TODO(tiabc): Is this required? - s.StartTestNode(params.RopstenNetworkID) + s.StartTestNode(params.RopstenNetworkID, false) + defer s.StopTestNode() s.jail.Parse(testChatID, "") diff --git a/geth/jail/requests.go b/geth/jail/requests.go index 9beaf8e09..7ce92cf85 100644 --- a/geth/jail/requests.go +++ b/geth/jail/requests.go @@ -1,235 +1,234 @@ package jail -import ( - "context" +// import ( +// "context" - gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/les/status" - "github.com/ethereum/go-ethereum/rpc" - "github.com/robertkrimen/otto" - "github.com/status-im/status-go/geth/common" -) +// gethcommon "github.com/ethereum/go-ethereum/common" +// "github.com/ethereum/go-ethereum/common/hexutil" +// "github.com/ethereum/go-ethereum/les/status" +// "github.com/ethereum/go-ethereum/rpc" +// "github.com/robertkrimen/otto" +// "github.com/status-im/status-go/geth/common" +// ) -const ( - // SendTransactionRequest is triggered on send transaction request - SendTransactionRequest = "eth_sendTransaction" -) +// const ( +// // SendTransactionRequest is triggered on send transaction request +// SendTransactionRequest = "eth_sendTransaction" +// ) -// RequestManager represents interface to manage jailed requests. -// Whenever some request passed to a Jail, needs to be pre/post processed, -// request manager is the right place for that. -type RequestManager struct { - nodeManager common.NodeManager -} +// // RequestManager represents interface to manage jailed requests. +// // Whenever some request passed to a Jail, needs to be pre/post processed, +// // request manager is the right place for that. +// type RequestManager struct { +// nodeManager common.NodeManager +// } -// NewRequestManager returns a new instance of the RequestManager pointer. -func NewRequestManager(nodeManager common.NodeManager) *RequestManager { - return &RequestManager{ - nodeManager: nodeManager, - } -} +// func NewRequestManager(nodeManager common.NodeManager) *RequestManager { +// return &RequestManager{ +// nodeManager: nodeManager, +// } +// } -// PreProcessRequest pre-processes a given RPC call to a given Otto VM -func (m *RequestManager) PreProcessRequest(vm *otto.Otto, req RPCCall) (string, error) { - messageID := currentMessageID(vm.Context()) +// // PreProcessRequest pre-processes a given RPC call to a given Otto VM +// func (m *RequestManager) PreProcessRequest(vm *otto.Otto, req RPCCall) (string, error) { +// messageID := currentMessageID(vm.Context()) - return messageID, nil -} +// return messageID, nil +// } -// PostProcessRequest post-processes a given RPC call to a given Otto VM -func (m *RequestManager) PostProcessRequest(vm *otto.Otto, req RPCCall, messageID string) { - // Errors are ignored because addContext may not exist and it's alright. - if len(messageID) > 0 { - vm.Call("addContext", nil, messageID, common.MessageIDKey, messageID) // nolint: errcheck - } +// // PostProcessRequest post-processes a given RPC call to a given Otto VM +// func (m *RequestManager) PostProcessRequest(vm *otto.Otto, req RPCCall, messageID string) { +// if len(messageID) > 0 { +// vm.Call("addContext", nil, messageID, common.MessageIDKey, messageID) // nolint: errcheck +// } - // set extra markers for queued transaction requests - if req.Method == SendTransactionRequest { - vm.Call("addContext", nil, messageID, SendTransactionRequest, true) // nolint: errcheck - } -} +// // set extra markers for queued transaction requests +// if req.Method == SendTransactionRequest { +// vm.Call("addContext", nil, messageID, SendTransactionRequest, true) // nolint: errcheck +// } +// } -// ProcessSendTransactionRequest processes send transaction request. -// Both pre and post processing happens within this function. Pre-processing -// happens before transaction is send to backend, and post processing occurs -// when backend notifies that transaction sending is complete (either successfully -// or with error) -func (m *RequestManager) ProcessSendTransactionRequest(vm *otto.Otto, req RPCCall) (gethcommon.Hash, error) { - lightEthereum, err := m.nodeManager.LightEthereumService() - if err != nil { - return gethcommon.Hash{}, err - } +// // ProcessSendTransactionRequest processes send transaction request. +// // Both pre and post processing happens within this function. Pre-processing +// // happens before transaction is send to backend, and post processing occurs +// // when backend notifies that transaction sending is complete (either successfully +// // or with error) +// func (m *RequestManager) ProcessSendTransactionRequest(vm *otto.Otto, req RPCCall) (gethcommon.Hash, error) { +// lightEthereum, err := m.nodeManager.LightEthereumService() +// if err != nil { +// return gethcommon.Hash{}, err +// } - backend := lightEthereum.StatusBackend +// backend := lightEthereum.StatusBackend - messageID, err := m.PreProcessRequest(vm, req) - if err != nil { - return gethcommon.Hash{}, err - } - // onSendTransactionRequest() will use context to obtain and release ticket - ctx := context.Background() - ctx = context.WithValue(ctx, common.MessageIDKey, messageID) +// messageID, err := m.PreProcessRequest(vm, req) +// if err != nil { +// return gethcommon.Hash{}, err +// } - // this call blocks, up until Complete Transaction is called - txHash, err := backend.SendTransaction(ctx, sendTxArgsFromRPCCall(req)) - if err != nil { - return gethcommon.Hash{}, err - } +// // onSendTransactionRequest() will use context to obtain and release ticket +// ctx := context.Background() +// ctx = context.WithValue(ctx, common.MessageIDKey, messageID) - // invoke post processing - m.PostProcessRequest(vm, req, messageID) +// // this call blocks, up until Complete Transaction is called +// txHash, err := backend.SendTransaction(ctx, sendTxArgsFromRPCCall(req)) +// if err != nil { +// return gethcommon.Hash{}, err +// } - return txHash, nil -} +// // invoke post processing +// m.PostProcessRequest(vm, req, messageID) -// RPCClient returns RPC client instance, creating it if necessary. -func (m *RequestManager) RPCClient() (*rpc.Client, error) { - return m.nodeManager.RPCClient() -} +// return txHash, nil +// } -// RPCCall represents RPC call parameters -type RPCCall struct { - ID int64 - Method string - Params []interface{} -} +// // RPCClient returns RPC client instance, creating it if necessary. +// func (m *RequestManager) RPCClient() (*rpc.Client, error) { +// return m.nodeManager.RPCClient() +// } -func sendTxArgsFromRPCCall(req RPCCall) status.SendTxArgs { - if req.Method != SendTransactionRequest { // no need to persist extra state for other requests - return status.SendTxArgs{} - } +// // RPCCall represents RPC call parameters +// type RPCCall struct { +// ID int64 +// Method string +// Params []interface{} +// } - return status.SendTxArgs{ - From: req.parseFromAddress(), - To: req.parseToAddress(), - Value: req.parseValue(), - Data: req.parseData(), - Gas: req.parseGas(), - GasPrice: req.parseGasPrice(), - } -} +// func sendTxArgsFromRPCCall(req RPCCall) status.SendTxArgs { +// if req.Method != SendTransactionRequest { // no need to persist extra state for other requests +// return status.SendTxArgs{} +// } -func (r RPCCall) parseFromAddress() gethcommon.Address { - params, ok := r.Params[0].(map[string]interface{}) - if !ok { - return gethcommon.HexToAddress("0x") - } +// return status.SendTxArgs{ +// From: req.parseFromAddress(), +// To: req.parseToAddress(), +// Value: req.parseValue(), +// Data: req.parseData(), +// Gas: req.parseGas(), +// GasPrice: req.parseGasPrice(), +// } +// } - from, ok := params["from"].(string) - if !ok { - from = "0x" - } +// func (r RPCCall) parseFromAddress() gethcommon.Address { +// params, ok := r.Params[0].(map[string]interface{}) +// if !ok { +// return gethcommon.HexToAddress("0x") +// } - return gethcommon.HexToAddress(from) -} +// from, ok := params["from"].(string) +// if !ok { +// from = "0x" +// } -func (r RPCCall) parseToAddress() *gethcommon.Address { - params, ok := r.Params[0].(map[string]interface{}) - if !ok { - return nil - } +// return gethcommon.HexToAddress(from) +// } - to, ok := params["to"].(string) - if !ok { - return nil - } +// func (r RPCCall) parseToAddress() *gethcommon.Address { +// params, ok := r.Params[0].(map[string]interface{}) +// if !ok { +// return nil +// } - address := gethcommon.HexToAddress(to) - return &address -} +// to, ok := params["to"].(string) +// if !ok { +// return nil +// } -func (r RPCCall) parseData() hexutil.Bytes { - params, ok := r.Params[0].(map[string]interface{}) - if !ok { - return hexutil.Bytes("0x") - } +// address := gethcommon.HexToAddress(to) +// return &address +// } - data, ok := params["data"].(string) - if !ok { - data = "0x" - } +// func (r RPCCall) parseData() hexutil.Bytes { +// params, ok := r.Params[0].(map[string]interface{}) +// if !ok { +// return hexutil.Bytes("0x") +// } - byteCode, err := hexutil.Decode(data) - if err != nil { - byteCode = hexutil.Bytes(data) - } +// data, ok := params["data"].(string) +// if !ok { +// data = "0x" +// } - return byteCode -} +// byteCode, err := hexutil.Decode(data) +// if err != nil { +// byteCode = hexutil.Bytes(data) +// } -// nolint: dupl -func (r RPCCall) parseValue() *hexutil.Big { - params, ok := r.Params[0].(map[string]interface{}) - if !ok { - return nil - //return (*hexutil.Big)(big.NewInt("0x0")) - } +// return byteCode +// } - inputValue, ok := params["value"].(string) - if !ok { - return nil - } +// // nolint: dupl +// func (r RPCCall) parseValue() *hexutil.Big { +// params, ok := r.Params[0].(map[string]interface{}) +// if !ok { +// return nil +// //return (*hexutil.Big)(big.NewInt("0x0")) +// } - parsedValue, err := hexutil.DecodeBig(inputValue) - if err != nil { - return nil - } +// inputValue, ok := params["value"].(string) +// if !ok { +// return nil +// } - return (*hexutil.Big)(parsedValue) -} +// parsedValue, err := hexutil.DecodeBig(inputValue) +// if err != nil { +// return nil +// } -// nolint: dupl -func (r RPCCall) parseGas() *hexutil.Big { - params, ok := r.Params[0].(map[string]interface{}) - if !ok { - return nil - } +// return (*hexutil.Big)(parsedValue) +// } - inputValue, ok := params["gas"].(string) - if !ok { - return nil - } +// // nolint: dupl +// func (r RPCCall) parseGas() *hexutil.Big { +// params, ok := r.Params[0].(map[string]interface{}) +// if !ok { +// return nil +// } - parsedValue, err := hexutil.DecodeBig(inputValue) - if err != nil { - return nil - } +// inputValue, ok := params["gas"].(string) +// if !ok { +// return nil +// } - return (*hexutil.Big)(parsedValue) -} +// parsedValue, err := hexutil.DecodeBig(inputValue) +// if err != nil { +// return nil +// } -// nolint: dupl -func (r RPCCall) parseGasPrice() *hexutil.Big { - params, ok := r.Params[0].(map[string]interface{}) - if !ok { - return nil - } +// return (*hexutil.Big)(parsedValue) +// } - inputValue, ok := params["gasPrice"].(string) - if !ok { - return nil - } +// // nolint: dupl +// func (r RPCCall) parseGasPrice() *hexutil.Big { +// params, ok := r.Params[0].(map[string]interface{}) +// if !ok { +// return nil +// } - parsedValue, err := hexutil.DecodeBig(inputValue) - if err != nil { - return nil - } +// inputValue, ok := params["gasPrice"].(string) +// if !ok { +// return nil +// } - return (*hexutil.Big)(parsedValue) -} +// parsedValue, err := hexutil.DecodeBig(inputValue) +// if err != nil { +// return nil +// } -// currentMessageID looks for `status.message_id` variable in current JS context -func currentMessageID(ctx otto.Context) string { - if statusObj, ok := ctx.Symbols["status"]; ok { - messageID, err := statusObj.Object().Get("message_id") - if err != nil { - return "" - } - if messageID, err := messageID.ToString(); err == nil { - return messageID - } - } +// return (*hexutil.Big)(parsedValue) +// } - return "" -} +// // currentMessageID looks for `status.message_id` variable in current JS context +// func currentMessageID(ctx otto.Context) string { +// if statusObj, ok := ctx.Symbols["status"]; ok { +// messageID, err := statusObj.Object().Get("message_id") +// if err != nil { +// return "" +// } +// if messageID, err := messageID.ToString(); err == nil { +// return messageID +// } +// } + +// return "" +// } diff --git a/geth/node/manager.go b/geth/node/manager.go index eb97daa10..8a826b5b0 100644 --- a/geth/node/manager.go +++ b/geth/node/manager.go @@ -502,12 +502,17 @@ func (m *NodeManager) AccountKeyStore() (*keystore.KeyStore, error) { return keyStore, nil } -// RPCClient exposes reference to RPC client connected to the running node +// RPCClient exposes reference to RPC client connected to the running node. func (m *NodeManager) RPCClient() (*rpc.Client, error) { if m == nil { return nil, ErrInvalidNodeManager } + config, err := m.NodeConfig() + if err != nil { + return nil, err + } + m.RLock() defer m.RUnlock() @@ -515,8 +520,20 @@ func (m *NodeManager) RPCClient() (*rpc.Client, error) { if m.node == nil || m.nodeStarted == nil { return nil, ErrNoRunningNode } + <-m.nodeStarted + // Connect to upstream RPC server with new client and cache instance. + if config.UpstreamConfig.Enabled { + m.rpcClient, err = rpc.Dial(config.UpstreamConfig.URL) + if err != nil { + log.Error("Failed to conect to upstream RPC server", "error", err) + return nil, err + } + + return m.rpcClient, nil + } + if m.rpcClient == nil { var err error m.rpcClient, err = m.node.Attach() diff --git a/geth/node/manager_test.go b/geth/node/manager_test.go index e415ff35a..3d00fd12a 100644 --- a/geth/node/manager_test.go +++ b/geth/node/manager_test.go @@ -248,7 +248,7 @@ func (s *ManagerTestSuite) TestReferences() { } // test with node fully started - s.StartTestNode(params.RinkebyNetworkID) + s.StartTestNode(params.RinkebyNetworkID, false) defer s.StopTestNode() var nodeReadyTestCases = []struct { name string @@ -403,7 +403,7 @@ func (s *ManagerTestSuite) TestResetChainData() { require := s.Require() require.NotNil(s.NodeManager) - s.StartTestNode(params.RinkebyNetworkID) + s.StartTestNode(params.RinkebyNetworkID, false) defer s.StopTestNode() time.Sleep(2 * time.Second) // allow to sync for some time @@ -422,7 +422,7 @@ func (s *ManagerTestSuite) TestRestartNode() { require := s.Require() require.NotNil(s.NodeManager) - s.StartTestNode(params.RinkebyNetworkID) + s.StartTestNode(params.RinkebyNetworkID, false) defer s.StopTestNode() s.True(s.NodeManager.IsNodeRunning()) diff --git a/geth/node/node.go b/geth/node/node.go index c17b4c8d3..50383682e 100644 --- a/geth/node/node.go +++ b/geth/node/node.go @@ -70,9 +70,15 @@ func MakeNode(config *params.NodeConfig) (*node.Node, error) { return nil, ErrNodeMakeFailure } - // start Ethereum service - if err := activateEthService(stack, config); err != nil { - return nil, fmt.Errorf("%v: %v", ErrEthServiceRegistrationFailure, err) + // start Ethereum service if we are not expected to use an upstream server. + if !config.UpstreamConfig.Enabled { + + if err := activateEthService(stack, config); err != nil { + return nil, fmt.Errorf("%v: %v", ErrEthServiceRegistrationFailure, err) + } + + } else { + log.Info("Blockchain synchronization is switched off, RPC requests will be proxied to %s", config.UpstreamConfig.URL) } // start Whisper service @@ -163,6 +169,7 @@ func activateEthService(stack *node.Node, config *params.NodeConfig) error { ethConf.NetworkId = config.NetworkID ethConf.DatabaseCache = config.LightEthConfig.DatabaseCache ethConf.MaxPeers = config.MaxPeers + if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { lightEth, err := les.New(ctx, ðConf) if err == nil { diff --git a/geth/node/rpc.go b/geth/node/rpc.go index 308191397..7023c04f6 100644 --- a/geth/node/rpc.go +++ b/geth/node/rpc.go @@ -1,17 +1,22 @@ package node import ( + "bytes" "encoding/json" "errors" + "io" + "io/ioutil" "net/http" "net/http/httptest" - "strings" "sync" "time" + "net/url" + "github.com/ethereum/go-ethereum/les/status" "github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/log" + "github.com/status-im/status-go/geth/params" ) const ( @@ -60,27 +65,35 @@ func NewRPCManager(nodeManager common.NodeManager) *RPCManager { // Call executes RPC request on node's in-proc RPC server func (c *RPCManager) Call(inputJSON string) string { - server, err := c.nodeManager.RPCServer() + config, err := c.nodeManager.NodeConfig() if err != nil { return c.makeJSONErrorResponse(err) } // allow HTTP requests to block w/o outputJSON := make(chan string, 1) - go func() { - httpReq := httptest.NewRequest("POST", "/", strings.NewReader(inputJSON)) - rr := httptest.NewRecorder() - server.ServeHTTP(rr, httpReq) - // Check the status code is what we expect. - if respStatus := rr.Code; respStatus != http.StatusOK { - log.Error("handler returned wrong status code", "got", respStatus, "want", http.StatusOK) - outputJSON <- c.makeJSONErrorResponse(ErrRPCServerCallFailed) + go func() { + body := bytes.NewBufferString(inputJSON) + + var err error + var res []byte + + if config.UpstreamConfig.Enabled { + log.Info("Making RPC JSON Request to upstream RPCServer") + res, err = c.callUpstreamStream(config, body) + } else { + log.Info("Making RPC JSON Request to internal RPCServer") + res, err = c.callNodeStream(body) + } + + if err != nil { + outputJSON <- c.makeJSONErrorResponse(err) return } - // everything is ok, return - outputJSON <- rr.Body.String() + outputJSON <- string(res) + return }() // wait till call is complete @@ -94,6 +107,65 @@ func (c *RPCManager) Call(inputJSON string) string { return c.makeJSONErrorResponse(ErrRPCServerTimeout) } +// callNodeStream delivers giving request and body content to the external ethereum +// (infura) RPCServer to process the request and returns response. +func (c *RPCManager) callUpstreamStream(config *params.NodeConfig, body io.Reader) ([]byte, error) { + upstreamURL, err := url.Parse(config.UpstreamConfig.URL) + if err != nil { + return nil, err + } + + httpReq, err := http.NewRequest("POST", upstreamURL.String(), body) + if err != nil { + return nil, err + } + + httpClient := http.Client{ + Timeout: 20 * time.Second, + } + + res, err := httpClient.Do(httpReq) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if respStatusCode := res.StatusCode; respStatusCode != http.StatusOK { + log.Error("handler returned wrong status code", "got", respStatusCode, "want", http.StatusOK) + return nil, ErrRPCServerCallFailed + } + + return ioutil.ReadAll(res.Body) +} + +// callNodeStream delivers giving request and body content to the internal ethereum +// RPCServer to process the request. +func (c *RPCManager) callNodeStream(body io.Reader) ([]byte, error) { + server, err := c.nodeManager.RPCServer() + if err != nil { + return nil, err + } + + httpReq, err := http.NewRequest("POST", "/", body) + if err != nil { + return nil, err + } + + rr := httptest.NewRecorder() + + server.ServeHTTP(rr, httpReq) + + // Check the status code is what we expect. + if respStatus := rr.Code; respStatus != http.StatusOK { + log.Error("handler returned wrong status code", "got", respStatus, "want", http.StatusOK) + // outputJSON <- c.makeJSONErrorResponse(ErrRPCServerCallFailed) + return nil, ErrRPCServerCallFailed + } + + return rr.Body.Bytes(), nil +} + // makeJSONErrorResponse returns error as JSON response func (c *RPCManager) makeJSONErrorResponse(err error) string { response := jsonErrResponse{ diff --git a/geth/node/rpc_test.go b/geth/node/rpc_test.go index 33c2ec1c2..8c1aa66e3 100644 --- a/geth/node/rpc_test.go +++ b/geth/node/rpc_test.go @@ -1,6 +1,7 @@ package node_test import ( + "net/http" "testing" "github.com/status-im/status-go/geth/log" @@ -10,6 +11,14 @@ import ( "github.com/stretchr/testify/suite" ) +type service struct { + Handler http.HandlerFunc +} + +func (s service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.Handler(w, r) +} + func TestRPCTestSuite(t *testing.T) { suite.Run(t, new(RPCTestSuite)) } @@ -20,9 +29,11 @@ type RPCTestSuite struct { func (s *RPCTestSuite) SetupTest() { require := s.Require() - s.NodeManager = node.NewNodeManager() - require.NotNil(s.NodeManager) - require.IsType(&node.NodeManager{}, s.NodeManager) + + nodeManager := node.NewNodeManager() + require.NotNil(nodeManager) + + s.NodeManager = nodeManager } func (s *RPCTestSuite) TestCallRPC() { @@ -38,10 +49,13 @@ func (s *RPCTestSuite) TestCallRPC() { nodeConfig.IPCEnabled = false nodeConfig.WSEnabled = false nodeConfig.HTTPHost = "" // to make sure that no HTTP interface is started + nodeStarted, err := s.NodeManager.StartNode(nodeConfig) require.NoError(err) require.NotNil(nodeConfig) + defer s.NodeManager.StopNode() + <-nodeStarted progress := make(chan struct{}, 25) diff --git a/geth/node/whisper_test.go b/geth/node/whisper_test.go index d654531fe..2b2e435b4 100644 --- a/geth/node/whisper_test.go +++ b/geth/node/whisper_test.go @@ -30,7 +30,7 @@ func (s *WhisperTestSuite) TestWhisperFilterRace() { require := s.Require() require.NotNil(s.NodeManager) - s.StartTestNode(params.RinkebyNetworkID) + s.StartTestNode(params.RinkebyNetworkID, false) defer s.StopTestNode() whisperService, err := s.NodeManager.WhisperService() diff --git a/geth/params/config.go b/geth/params/config.go index 2f787301b..d414c1940 100644 --- a/geth/params/config.go +++ b/geth/params/config.go @@ -207,6 +207,20 @@ func (c *BootClusterConfig) String() string { return string(data) } +//===================================================================================== + +// UpstreamRPCConfig stores configuration for upstream rpc connection. +type UpstreamRPCConfig struct { + // Enabled flag specifies whether feature is enabled + Enabled bool + + // URL sets the rpc upstream host address for communication with + // a non-local infura endpoint. + URL string +} + +//===================================================================================== + // NodeConfig stores configuration options for a node type NodeConfig struct { // DevMode is true when given configuration is to be used during development. @@ -287,6 +301,9 @@ type NodeConfig struct { // LogToStderr defines whether logged info should also be output to os.Stderr LogToStderr bool + // UpstreamConfig extra config for providing upstream infura server. + UpstreamConfig UpstreamRPCConfig `json:"UpstreamConfig"` + // BootClusterConfig extra configuration for supporting cluster BootClusterConfig *BootClusterConfig `json:"BootClusterConfig," validate:"structonly"` @@ -458,9 +475,15 @@ func (c *NodeConfig) updateConfig() error { if err := c.updateGenesisConfig(); err != nil { return err } + + if err := c.updateUpstreamConfig(); err != nil { + return err + } + if err := c.updateBootClusterConfig(); err != nil { return err } + if err := c.updateRelativeDirsConfig(); err != nil { return err } @@ -493,6 +516,27 @@ func (c *NodeConfig) updateGenesisConfig() error { return nil } +// updateUpstreamConfig sets the proper UpstreamConfig.URL for the network id being used. +func (c *NodeConfig) updateUpstreamConfig() error { + + // If we have a URL already set then keep URL incase + // of custom server. + if c.UpstreamConfig.URL != "" { + return nil + } + + switch c.NetworkID { + case MainNetworkID: + c.UpstreamConfig.URL = UpstreamMainNetEthereumNetworkURL + case RopstenNetworkID: + c.UpstreamConfig.URL = UpstreamRopstenEthereumNetworkURL + case RinkebyNetworkID: + c.UpstreamConfig.URL = UpstreamRinkebyEthereumNetworkURL + } + + return nil +} + // updateBootClusterConfig loads boot nodes and CHT for a given network and mode. // This is necessary until we have LES protocol support CHT sync, and better node // discovery on mobile devices) diff --git a/geth/params/config_test.go b/geth/params/config_test.go index d2d896f6c..a06d6c5d9 100644 --- a/geth/params/config_test.go +++ b/geth/params/config_test.go @@ -72,6 +72,38 @@ var loadConfigTestCases = []struct { require.Equal(t, "/foo/bar", nodeConfig.KeyStoreDir) }, }, + { + `test Upstream config setting`, + `{ + "NetworkId": 3, + "DataDir": "$TMPDIR", + "Name": "TestStatusNode", + "WSPort": 4242, + "IPCEnabled": true, + "WSEnabled": false, + "UpstreamConfig": { + "Enabled": true, + "URL": "http://upstream.loco.net/nodes" + } + }`, + func(t *testing.T, dataDir string, nodeConfig *params.NodeConfig, err error) { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if nodeConfig.NetworkID != 3 { + t.Fatal("wrong NetworkId") + } + + if !nodeConfig.UpstreamConfig.Enabled { + t.Fatal("wrong UpstreamConfig.Enabled state") + } + + if nodeConfig.UpstreamConfig.URL != "http://upstream.loco.net/nodes" { + t.Fatal("wrong UpstreamConfig.URL value") + } + }, + }, { `test parameter overriding`, `{ diff --git a/geth/params/defaults.go b/geth/params/defaults.go index 803c21891..85b9e31da 100644 --- a/geth/params/defaults.go +++ b/geth/params/defaults.go @@ -29,6 +29,9 @@ const ( // WSHost is a host interface for the websocket RPC server WSHost = "localhost" + // SendTransactionMethodName defines the name for a giving transaction. + SendTransactionMethodName = "eth_sendTransaction" + // WSPort is a WS-RPC port (replaced in unit tests) WSPort = 8546 @@ -84,6 +87,18 @@ const ( // FirebaseNotificationTriggerURL is URL where FCM notification requests are sent to FirebaseNotificationTriggerURL = "https://fcm.googleapis.com/fcm/send" + // UpstreamMainNetEthereumNetworkURL is URL where the upstream ethereum network is loaded to + // allow us avoid syncing node. + UpstreamMainNetEthereumNetworkURL = "https://mainnet.infura.io/nKmXgiFgc2KqtoQ8BCGJ" + + // UpstreamRopstenEthereumNetworkURL is URL where the upstream ethereum network is loaded to + // allow us avoid syncing node. + UpstreamRopstenEthereumNetworkURL = "https://ropsten.infura.io/nKmXgiFgc2KqtoQ8BCGJ" + + // UpstreamRinkebyEthereumNetworkURL is URL where the upstream ethereum network is loaded to + // allow us avoid syncing node. + UpstreamRinkebyEthereumNetworkURL = "https://rinkeby.infura.io/nKmXgiFgc2KqtoQ8BCGJ" + // MainNetworkID is id of the main network MainNetworkID = 1 diff --git a/geth/params/testdata/config.mainnet.json b/geth/params/testdata/config.mainnet.json index 68c1bd3fb..44e81128f 100755 --- a/geth/params/testdata/config.mainnet.json +++ b/geth/params/testdata/config.mainnet.json @@ -22,6 +22,10 @@ "LogFile": "geth.log", "LogLevel": "INFO", "LogToStderr": true, + "UpstreamConfig": { + "Enabled": false, + "URL": "https://mainnet.infura.io/nKmXgiFgc2KqtoQ8BCGJ" + }, "BootClusterConfig": { "Enabled": true, "RootNumber": 805, diff --git a/geth/params/testdata/config.rinkeby.json b/geth/params/testdata/config.rinkeby.json index 834cb50fe..af52e59e6 100755 --- a/geth/params/testdata/config.rinkeby.json +++ b/geth/params/testdata/config.rinkeby.json @@ -22,6 +22,10 @@ "LogFile": "geth.log", "LogLevel": "INFO", "LogToStderr": true, + "UpstreamConfig": { + "Enabled": false, + "URL": "https://rinkeby.infura.io/nKmXgiFgc2KqtoQ8BCGJ" + }, "BootClusterConfig": { "Enabled": true, "RootNumber": 66, diff --git a/geth/params/testdata/config.ropsten.json b/geth/params/testdata/config.ropsten.json index 202c614f8..fbb743d24 100755 --- a/geth/params/testdata/config.ropsten.json +++ b/geth/params/testdata/config.ropsten.json @@ -22,6 +22,10 @@ "LogFile": "geth.log", "LogLevel": "INFO", "LogToStderr": true, + "UpstreamConfig": { + "Enabled": false, + "URL": "https://ropsten.infura.io/nKmXgiFgc2KqtoQ8BCGJ" + }, "BootClusterConfig": { "Enabled": true, "RootNumber": 259, diff --git a/geth/testing/testing.go b/geth/testing/testing.go index a6a4f047c..d5bd9a02f 100644 --- a/geth/testing/testing.go +++ b/geth/testing/testing.go @@ -17,6 +17,7 @@ import ( ) var ( + // TestConfig defines the default config usable at package-level. TestConfig *common.TestConfig // RootDir is the main application directory @@ -54,18 +55,25 @@ func init() { } } +// BaseTestSuite defines a base tests suit which others suites can embedded to +// access initialization methods useful for testing. type BaseTestSuite struct { suite.Suite NodeManager common.NodeManager } -func (s *BaseTestSuite) StartTestNode(networkID int) { +// StartTestNode initiazes a NodeManager instances with configuration retrieved +// from the test config. +func (s *BaseTestSuite) StartTestNode(networkID int, upstream bool) { require := s.Require() require.NotNil(s.NodeManager) nodeConfig, err := MakeTestNodeConfig(networkID) require.NoError(err) + nodeConfig.UpstreamConfig.Enabled = upstream + require.Equal(nodeConfig.UpstreamConfig.Enabled, upstream) + keyStoreDir := filepath.Join(TestDataDir, TestNetworkNames[networkID], "keystore") require.NoError(common.ImportTestAccount(keyStoreDir, "test-account1.pk")) require.NoError(common.ImportTestAccount(keyStoreDir, "test-account2.pk")) @@ -78,6 +86,7 @@ func (s *BaseTestSuite) StartTestNode(networkID int) { require.True(s.NodeManager.IsNodeRunning()) } +// StopTestNode attempts to stop initialized NodeManager. func (s *BaseTestSuite) StopTestNode() { require := s.Require() require.NotNil(s.NodeManager) @@ -88,6 +97,7 @@ func (s *BaseTestSuite) StopTestNode() { require.False(s.NodeManager.IsNodeRunning()) } +// FirstBlockHash validates Attach operation for the NodeManager. func FirstBlockHash(require *assertions.Assertions, nodeManager common.NodeManager, expectedHash string) { require.NotNil(nodeManager) @@ -110,6 +120,8 @@ func FirstBlockHash(require *assertions.Assertions, nodeManager common.NodeManag require.Equal(expectedHash, firstBlock.Hash.Hex()) } +// MakeTestNodeConfig defines a function to return a giving params.NodeConfig +// where specific network addresses are assigned based on provieded network id. func MakeTestNodeConfig(networkID int) (*params.NodeConfig, error) { configJSON := `{ "NetworkId": ` + strconv.Itoa(networkID) + `,