Fixed eth_sendTransaction routing to the local node (#351)

* Fixed eth_sendTransaction routing to the local node
* Add local RPC handlers for eth_accounts and eth_sendTransaction
This commit is contained in:
Ivan Tomilov 2017-09-25 19:04:40 +03:00 committed by Ivan Daniluk
parent 750612f2bc
commit fc8f59e121
16 changed files with 501 additions and 163 deletions

View File

@ -5,7 +5,6 @@ import (
"sync"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/jail"
"github.com/status-im/status-go/geth/log"
@ -224,19 +223,9 @@ func (m *StatusBackend) DiscardTransactions(ids []common.QueuedTxID) map[common.
// registerHandlers attaches Status callback handlers to running node
func (m *StatusBackend) registerHandlers() error {
runningNode, err := m.nodeManager.Node()
if err != nil {
return err
}
var lightEthereum *les.LightEthereum
if err := runningNode.Service(&lightEthereum); err != nil {
log.Error("Cannot get light ethereum service", "error", err)
return err
}
lightEthereum.StatusBackend.SetAccountsFilterHandler(m.accountManager.AccountsListRequestHandler())
log.Info("Registered handler", "fn", "AccountsFilterHandler")
rpcClient := m.NodeManager().RPCClient()
rpcClient.RegisterHandler("eth_accounts", m.accountManager.AccountsRPCHandler())
rpcClient.RegisterHandler("eth_sendTransaction", m.txQueueManager.SendTransactionRPCHandler)
m.txQueueManager.SetTransactionQueueHandler(m.txQueueManager.TransactionQueueHandler())
log.Info("Registered handler", "fn", "TransactionQueueHandler")

View File

@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/les"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
@ -22,14 +21,8 @@ func (s *BackendTestSuite) TestAccountsList() {
require.NoError(err)
require.NotNil(runningNode)
var lesService *les.LightEthereum
require.NoError(runningNode.Service(&lesService))
require.NotNil(lesService)
accounts := lesService.StatusBackend.AccountManager().Accounts()
for _, acc := range accounts {
fmt.Println(acc.Hex())
}
accounts, err := s.backend.AccountManager().Accounts()
require.NoError(err)
// make sure that we start with empty accounts list (nobody has logged in yet)
require.Zero(len(accounts), "accounts returned, while there should be none (we haven't logged in yet)")
@ -39,7 +32,8 @@ func (s *BackendTestSuite) TestAccountsList() {
require.NoError(err)
// ensure that there is still no accounts returned
accounts = lesService.StatusBackend.AccountManager().Accounts()
accounts, err = s.backend.AccountManager().Accounts()
require.NoError(err)
require.Zero(len(accounts), "accounts returned, while there should be none (we haven't logged in yet)")
// select account (sub-accounts will be created for this key)
@ -47,7 +41,8 @@ func (s *BackendTestSuite) TestAccountsList() {
require.NoError(err, "account selection failed")
// at this point main account should show up
accounts = lesService.StatusBackend.AccountManager().Accounts()
accounts, err = s.backend.AccountManager().Accounts()
require.NoError(err)
require.Equal(1, len(accounts), "exactly single account is expected (main account)")
require.Equal(string(accounts[0].Hex()), "0x"+address,
fmt.Sprintf("main account is not retured as the first key: got %s, expected %s", accounts[0].Hex(), "0x"+address))
@ -57,7 +52,8 @@ func (s *BackendTestSuite) TestAccountsList() {
require.NoError(err, "cannot create sub-account")
// now we expect to see both main account and sub-account 1
accounts = lesService.StatusBackend.AccountManager().Accounts()
accounts, err = s.backend.AccountManager().Accounts()
require.NoError(err)
require.Equal(2, len(accounts), "exactly 2 accounts are expected (main + sub-account 1)")
require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key")
require.Equal(string(accounts[1].Hex()), "0x"+subAccount1, "subAcount1 not returned")
@ -68,7 +64,8 @@ func (s *BackendTestSuite) TestAccountsList() {
require.False(subAccount1 == subAccount2 || subPubKey1 == subPubKey2, "sub-account index auto-increament failed")
// finally, all 3 accounts should show up (main account, sub-accounts 1 and 2)
accounts = lesService.StatusBackend.AccountManager().Accounts()
accounts, err = s.backend.AccountManager().Accounts()
require.NoError(err)
require.Equal(3, len(accounts), "unexpected number of accounts")
require.Equal(string(accounts[0].Hex()), "0x"+address, "main account is not retured as the first key")

View File

@ -388,7 +388,6 @@ func (s *BackendTestSuite) TestJailWhisper() {
throw 'message not sent: ' + JSON.stringify(message);
}
var filterName = '` + whisperMessage1 + `';
var filterId = filter.filterId;
if (!filterId) {
@ -576,11 +575,13 @@ func (s *BackendTestSuite) TestJailWhisper() {
jailInstance.Parse(testCaseKey, `
var shh = web3.shh;
// topic must be 4-byte long
var makeTopic = function () {
var min = 1;
var max = Math.pow(16, 8);
var randInt = Math.floor(Math.random() * (max - min + 1)) + min;
return web3.toHex(randInt);
var topic = '0x';
for (var i = 0; i < 8; i++) {
topic += Math.floor(Math.random() * 16).toString(16);
}
return topic;
};
`)

View File

@ -1,10 +1,12 @@
package api_test
import (
"encoding/json"
"math/rand"
"testing"
"time"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les"
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
"github.com/status-im/status-go/geth/api"
@ -256,6 +258,119 @@ func (s *BackendTestSuite) TestCallRPC() {
}
}
func (s *BackendTestSuite) TestCallRPCSendTransaction() {
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
s.NoError(err)
nodeStarted, err := s.backend.StartNode(nodeConfig)
s.NoError(err)
defer s.backend.StopNode()
<-nodeStarted
// Allow to sync the blockchain.
time.Sleep(TestConfig.Node.SyncSeconds * time.Second)
err = s.backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)
s.NoError(err)
transactionCompleted := make(chan struct{})
var txHash gethcommon.Hash
node.SetDefaultNodeNotificationHandler(func(rawSignal string) {
var signal node.SignalEnvelope
err := json.Unmarshal([]byte(rawSignal), &signal)
s.NoError(err)
if signal.Type == node.EventTransactionQueued {
event := signal.Event.(map[string]interface{})
txID := event["id"].(string)
txHash, err = s.backend.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account1.Password)
s.NoError(err, "cannot complete queued transaction %s", txID)
close(transactionCompleted)
}
})
result := s.backend.CallRPC(`{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_sendTransaction",
"params": [{
"from": "` + TestConfig.Account1.Address + `",
"to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
"value": "0x9184e72a"
}]
}`)
s.NotContains(result, "error")
select {
case <-transactionCompleted:
case <-time.After(time.Minute):
s.FailNow("sending transaction timed out")
}
s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+txHash.String()+`"}`, result)
}
func (s *BackendTestSuite) TestCallRPCSendTransactionUpstream() {
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
s.NoError(err)
nodeConfig.UpstreamConfig.Enabled = true
nodeConfig.UpstreamConfig.URL = "https://ropsten.infura.io/nKmXgiFgc2KqtoQ8BCGJ"
nodeStarted, err := s.backend.StartNode(nodeConfig)
s.NoError(err)
defer s.backend.StopNode()
<-nodeStarted
// Allow to sync the blockchain.
time.Sleep(TestConfig.Node.SyncSeconds * time.Second)
err = s.backend.AccountManager().SelectAccount(TestConfig.Account2.Address, TestConfig.Account2.Password)
s.NoError(err)
transactionCompleted := make(chan struct{})
var txHash gethcommon.Hash
node.SetDefaultNodeNotificationHandler(func(rawSignal string) {
var signal node.SignalEnvelope
err := json.Unmarshal([]byte(rawSignal), &signal)
s.NoError(err)
if signal.Type == node.EventTransactionQueued {
event := signal.Event.(map[string]interface{})
txID := event["id"].(string)
txHash, err = s.backend.CompleteTransaction(common.QueuedTxID(txID), TestConfig.Account2.Password)
s.NoError(err, "cannot complete queued transaction %s", txID)
close(transactionCompleted)
}
})
result := s.backend.CallRPC(`{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_sendTransaction",
"params": [{
"from": "` + TestConfig.Account2.Address + `",
"to": "` + TestConfig.Account1.Address + `",
"value": "0x9184e72a"
}]
}`)
s.NotContains(result, "error")
select {
case <-transactionCompleted:
case <-time.After(time.Minute):
s.FailNow("sending transaction timed out")
}
s.Equal(`{"jsonrpc":"2.0","id":1,"result":"`+txHash.String()+`"}`, result)
}
// FIXME(tiabc): There's also a test with the same name in geth/node/manager_test.go
// so this test should only check StatusBackend logic with a mocked version of the underlying NodeManager.
func (s *BackendTestSuite) TestRaceConditions() {

View File

@ -133,3 +133,28 @@ func (r RPCCall) ParseGasPrice() *hexutil.Big {
return (*hexutil.Big)(parsedValue)
}
// ToSendTxArgs converts RPCCall to SendTxArgs.
func (r RPCCall) ToSendTxArgs() SendTxArgs {
var err error
var fromAddr, toAddr gethcommon.Address
fromAddr, err = r.ParseFromAddress()
if err != nil {
fromAddr = gethcommon.HexToAddress("0x0")
}
toAddr, err = r.ParseToAddress()
if err != nil {
toAddr = gethcommon.HexToAddress("0x0")
}
return SendTxArgs{
To: &toAddr,
From: fromAddr,
Value: r.ParseValue(),
Data: r.ParseData(),
Gas: r.ParseGas(),
GasPrice: r.ParseGasPrice(),
}
}

View File

@ -125,8 +125,11 @@ type AccountManager interface {
// Logout clears whisper identities
Logout() error
// AccountsListRequestHandler returns handler to process account list request
AccountsListRequestHandler() func(entities []common.Address) []common.Address
// Accounts returns handler to process account list request
Accounts() ([]common.Address, error)
// AccountsRPCHandler returns RPC wrapper for Accounts()
AccountsRPCHandler() rpc.Handler
// AddressToDecryptedAccount tries to load decrypted key for a given account.
// The running node, has a keystore directory which is loaded on start. Key file
@ -224,6 +227,8 @@ type TxQueueManager interface {
// TODO(adam): might be not needed
SetTransactionReturnHandler(fn EnqueuedTxReturnHandler)
SendTransactionRPCHandler(ctx context.Context, args ...interface{}) (interface{}, error)
// TransactionReturnHandler returns handler that processes responses from internal tx manager
TransactionReturnHandler() func(queuedTx *QueuedTx, err error)

View File

@ -348,16 +348,29 @@ func (mr *MockAccountManagerMockRecorder) Logout() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockAccountManager)(nil).Logout))
}
// AccountsListRequestHandler mocks base method
func (m *MockAccountManager) AccountsListRequestHandler() func([]common.Address) []common.Address {
ret := m.ctrl.Call(m, "AccountsListRequestHandler")
ret0, _ := ret[0].(func([]common.Address) []common.Address)
// Accounts mocks base method
func (m *MockAccountManager) Accounts() ([]common.Address, error) {
ret := m.ctrl.Call(m, "Accounts")
ret0, _ := ret[0].([]common.Address)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Accounts indicates an expected call of Accounts
func (mr *MockAccountManagerMockRecorder) Accounts() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accounts", reflect.TypeOf((*MockAccountManager)(nil).Accounts))
}
// AccountsRPCHandler mocks base method
func (m *MockAccountManager) AccountsRPCHandler() rpc.Handler {
ret := m.ctrl.Call(m, "AccountsRPCHandler")
ret0, _ := ret[0].(rpc.Handler)
return ret0
}
// AccountsListRequestHandler indicates an expected call of AccountsListRequestHandler
func (mr *MockAccountManagerMockRecorder) AccountsListRequestHandler() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsListRequestHandler", reflect.TypeOf((*MockAccountManager)(nil).AccountsListRequestHandler))
// AccountsRPCHandler indicates an expected call of AccountsRPCHandler
func (mr *MockAccountManagerMockRecorder) AccountsRPCHandler() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsRPCHandler", reflect.TypeOf((*MockAccountManager)(nil).AccountsRPCHandler))
}
// AddressToDecryptedAccount mocks base method

View File

@ -3,7 +3,6 @@ package jail
import (
"context"
gethcommon "github.com/ethereum/go-ethereum/common"
gethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/jail/internal/vm"
@ -32,50 +31,10 @@ func NewExecutionPolicy(
// Execute handles the execution of a RPC request and routes appropriately to either a local or remote ethereum node.
func (ep *ExecutionPolicy) Execute(req common.RPCCall, vm *vm.VM) (map[string]interface{}, error) {
if params.SendTransactionMethodName == req.Method {
return ep.executeSendTransaction(vm, req)
}
client := ep.nodeManager.RPCClient()
return ep.executeWithClient(client, vm, req)
}
// executeRemoteSendTransaction defines a function to execute RPC method eth_sendTransaction over the upstream server.
func (ep *ExecutionPolicy) executeSendTransaction(vm *vm.VM, req common.RPCCall) (map[string]interface{}, error) {
messageID, err := preProcessRequest(vm)
if err != nil {
return nil, err
}
// TODO(adam): check if context is used
ctx := context.WithValue(context.Background(), common.MessageIDKey, messageID)
args := sendTxArgsFromRPCCall(req)
tx := ep.txQueueManager.CreateTransaction(ctx, args)
if err := ep.txQueueManager.QueueTransaction(tx); err != nil {
return nil, err
}
if err := ep.txQueueManager.WaitForTransaction(tx); err != nil {
return nil, err
}
// invoke post processing
postProcessRequest(vm, req, messageID)
res := map[string]interface{}{
"jsonrpc": "2.0",
"id": req.ID,
// @TODO(adam): which one is actually used?
"result": tx.Hash.Hex(),
"hash": tx.Hash.Hex(),
}
return res, nil
}
func (ep *ExecutionPolicy) executeWithClient(client *rpc.Client, vm *vm.VM, req common.RPCCall) (map[string]interface{}, error) {
// Arbitrary JSON-RPC response.
var result interface{}
@ -96,7 +55,9 @@ func (ep *ExecutionPolicy) executeWithClient(client *rpc.Client, vm *vm.VM, req
if client == nil {
resp = newErrorResponse("RPC client is not available. Node is stopped?", &req.ID)
} else {
err = client.Call(&result, req.Method, req.Params...)
// TODO(adam): check if context is used
ctx := context.WithValue(context.Background(), common.MessageIDKey, messageID)
err = client.CallContext(ctx, &result, req.Method, req.Params...)
if err != nil {
if err2, ok := err.(gethrpc.Error); ok {
resp["error"] = map[string]interface{}{
@ -150,32 +111,3 @@ func currentMessageID(vm *vm.VM) string {
}
return msgID.String()
}
func sendTxArgsFromRPCCall(req common.RPCCall) common.SendTxArgs {
// no need to persist extra state for other requests
if req.Method != params.SendTransactionMethodName {
return common.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 common.SendTxArgs{
To: &toAddr,
From: fromAddr,
Value: req.ParseValue(),
Data: req.ParseData(),
Gas: req.ParseGas(),
GasPrice: req.ParseGasPrice(),
}
}

View File

@ -2,6 +2,7 @@ package node
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
@ -14,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/extkeys"
"github.com/status-im/status-go/geth/common"
"github.com/status-im/status-go/geth/rpc"
)
// errors
@ -303,31 +305,49 @@ func (m *AccountManager) importExtendedKey(extKey *extkeys.ExtendedKey, password
return
}
// AccountsListRequestHandler returns handler to process account list request
func (m *AccountManager) AccountsListRequestHandler() func(entities []gethcommon.Address) []gethcommon.Address {
return func(entities []gethcommon.Address) []gethcommon.Address {
if m.selectedAccount == nil {
return []gethcommon.Address{}
// Accounts returns list of addresses for selected account, including
// subaccounts.
func (m *AccountManager) Accounts() ([]gethcommon.Address, error) {
am, err := m.nodeManager.AccountManager()
if err != nil {
return nil, err
}
var addresses []gethcommon.Address
for _, wallet := range am.Wallets() {
for _, account := range wallet.Accounts() {
addresses = append(addresses, account.Address)
}
}
m.refreshSelectedAccount()
if m.selectedAccount == nil {
return []gethcommon.Address{}, nil
}
filtered := make([]gethcommon.Address, 0)
for _, account := range entities {
// main account
if m.selectedAccount.Address.Hex() == account.Hex() {
filtered = append(filtered, account)
} else {
// sub accounts
for _, subAccount := range m.selectedAccount.SubAccounts {
if subAccount.Address.Hex() == account.Hex() {
filtered = append(filtered, account)
}
m.refreshSelectedAccount()
filtered := make([]gethcommon.Address, 0)
for _, account := range addresses {
// main account
if m.selectedAccount.Address.Hex() == account.Hex() {
filtered = append(filtered, account)
} else {
// sub accounts
for _, subAccount := range m.selectedAccount.SubAccounts {
if subAccount.Address.Hex() == account.Hex() {
filtered = append(filtered, account)
}
}
}
}
return filtered
return filtered, nil
}
// AccountsRPCHandler returns RPC Handler for the Accounts() method.
func (m *AccountManager) AccountsRPCHandler() rpc.Handler {
return func(context.Context, ...interface{}) (interface{}, error) {
return m.Accounts()
}
}

View File

@ -225,18 +225,42 @@ func (m *TxQueueManager) completeRemoteTransaction(queuedTx *common.QueuedTx, pa
return emptyHash, err
}
args := queuedTx.Args
if args.GasPrice == nil {
value, err := m.gasPrice()
if err != nil {
return emptyHash, err
}
args.GasPrice = value
}
chainID := big.NewInt(int64(config.NetworkID))
nonce := uint64(txCount)
gasPrice := (*big.Int)(queuedTx.Args.GasPrice)
dataVal := []byte(queuedTx.Args.Data)
priceVal := (*big.Int)(queuedTx.Args.Value)
gasPrice := (*big.Int)(args.GasPrice)
data := []byte(args.Data)
value := (*big.Int)(args.Value)
toAddr := gethcommon.Address{}
if args.To != nil {
toAddr = *args.To
}
gas, err := m.estimateGas(queuedTx.Args)
gas, err := m.estimateGas(args)
if err != nil {
return emptyHash, err
}
tx := types.NewTransaction(nonce, *queuedTx.Args.To, priceVal, (*big.Int)(gas), gasPrice, dataVal)
log.Info(
"preparing raw transaction",
"from", args.From.Hex(),
"to", toAddr.Hex(),
"gas", gas,
"gasPrice", gasPrice,
"value", value,
)
tx := types.NewTransaction(nonce, toAddr, value, (*big.Int)(gas), gasPrice, data)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), selectedAcct.AccountKey.PrivateKey)
if err != nil {
return emptyHash, err
@ -305,6 +329,20 @@ func (m *TxQueueManager) estimateGas(args common.SendTxArgs) (*hexutil.Big, erro
return &estimatedGas, nil
}
func (m *TxQueueManager) gasPrice() (*hexutil.Big, error) {
client := m.nodeManager.RPCClient()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var gasPrice hexutil.Big
if err := client.CallContext(ctx, &gasPrice, "eth_gasPrice"); err != nil {
log.Warn("failed to get gas price", "err", err)
return nil, err
}
return &gasPrice, nil
}
// CompleteTransactions instructs backend to complete sending of multiple transactions
func (m *TxQueueManager) CompleteTransactions(ids []common.QueuedTxID, password string) map[common.QueuedTxID]common.RawCompleteTransactionResult {
results := make(map[common.QueuedTxID]common.RawCompleteTransactionResult)
@ -430,3 +468,25 @@ func (m *TxQueueManager) sendTransactionErrorCode(err error) string {
func (m *TxQueueManager) SetTransactionReturnHandler(fn common.EnqueuedTxReturnHandler) {
m.txQueue.SetTxReturnHandler(fn)
}
// SendTransactionRPCHandler is a handler for eth_sendTransaction method.
// It accepts one param which is a slice with a map of transaction params.
func (m *TxQueueManager) SendTransactionRPCHandler(ctx context.Context, args ...interface{}) (interface{}, error) {
log.Info("SendTransactionRPCHandler called")
// TODO(adam): it's a hack to parse arguments as common.RPCCall can do that.
// We should refactor parsing these params to a separate struct.
rpcCall := common.RPCCall{Params: args}
tx := m.CreateTransaction(ctx, rpcCall.ToSendTxArgs())
if err := m.QueueTransaction(tx); err != nil {
return nil, err
}
if err := m.WaitForTransaction(tx); err != nil {
return nil, err
}
return tx.Hash.Hex(), nil
}

View File

@ -2,7 +2,11 @@ package rpc
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"sync"
"github.com/ethereum/go-ethereum/node"
"github.com/status-im/status-go/geth/params"
@ -10,6 +14,9 @@ import (
gethrpc "github.com/ethereum/go-ethereum/rpc"
)
// Handler defines handler for RPC methods.
type Handler func(context.Context, ...interface{}) (interface{}, error)
// Client represents RPC client with custom routing
// scheme. It automatically decides where RPC call
// goes - Upstream or Local node.
@ -21,6 +28,9 @@ type Client struct {
upstream *gethrpc.Client
router *router
handlersMx sync.RWMutex // mx guards handlers
handlers map[string]Handler // locally registered handlers
}
// NewClient initializes Client and tries to connect to both,
@ -29,7 +39,9 @@ type Client struct {
// Client is safe for concurrent use and will automatically
// reconnect to the server if connection is lost.
func NewClient(node *node.Node, upstream params.UpstreamRPCConfig) (*Client, error) {
c := &Client{}
c := &Client{
handlers: make(map[string]Handler),
}
var err error
c.local, err = node.Attach()
@ -72,8 +84,92 @@ func (c *Client) Call(result interface{}, method string, args ...interface{}) er
//
// It uses custom routing scheme for calls.
func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
// check locally registered handlers first
if handler, ok := c.handler(method); ok {
return c.callMethod(ctx, result, handler, args...)
}
if c.router.routeRemote(method) {
return c.upstream.CallContext(ctx, result, method, args...)
}
return c.local.CallContext(ctx, result, method, args...)
}
// RegisterHandler registers local handler for specific RPC method.
//
// If method is registered, it will be executed with given handler and
// never routed to the upstream or local servers.
func (c *Client) RegisterHandler(method string, handler Handler) {
c.handlersMx.Lock()
defer c.handlersMx.Unlock()
c.handlers[method] = handler
}
// callMethod calls registered RPC handler with given args and pointer to result.
// It handles proper params and result converting
//
// TODO(divan): use cancellation via context here?
func (c *Client) callMethod(ctx context.Context, result interface{}, handler Handler, args ...interface{}) error {
response, err := handler(ctx, args...)
if err != nil {
return err
}
// if result is nil, just ignore result -
// the same way as gethrpc.CallContext() caller would expect
if result == nil {
return nil
}
if err := setResultFromRPCResponse(result, response); err != nil {
return err
}
return nil
}
// handler is a concurrently safe method to get registered handler by name.
func (c *Client) handler(method string) (Handler, bool) {
c.handlersMx.RLock()
defer c.handlersMx.RUnlock()
handler, ok := c.handlers[method]
return handler, ok
}
// setResultFromRPCResponse tries to set result value from response using reflection
// as concrete types are unknown.
func setResultFromRPCResponse(result, response interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid result type: %s", r)
}
}()
responseValue := reflect.ValueOf(response)
// If it is called via CallRaw, result has type json.RawMessage and
// we should marshal the response before setting it.
// Otherwise, it is called with CallContext and result is of concrete type,
// thus we should try to set it as it is.
// If response type and result type are incorrect, an error should be returned.
// TODO(divan): add additional checks for result underlying value, if needed:
// some example: https://golang.org/src/encoding/json/decode.go#L596
switch reflect.ValueOf(result).Elem().Type() {
case reflect.TypeOf(json.RawMessage{}), reflect.TypeOf([]byte{}):
data, err := json.Marshal(response)
if err != nil {
return err
}
responseValue = reflect.ValueOf(data)
}
value := reflect.ValueOf(result).Elem()
if !value.CanSet() {
return errors.New("can't assign value to result")
}
value.Set(responseValue)
return nil
}

View File

@ -0,0 +1,45 @@
package rpc
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestSetResultFromRPCResponse(t *testing.T) {
var err error
var resultRawMessage json.RawMessage
err = setResultFromRPCResponse(&resultRawMessage, []string{"one", "two", "three"})
require.NoError(t, err)
require.Equal(t, json.RawMessage(`["one","two","three"]`), resultRawMessage)
var resultSlice []int
err = setResultFromRPCResponse(&resultSlice, []int{1, 2, 3})
require.NoError(t, err)
require.Equal(t, []int{1, 2, 3}, resultSlice)
var resultMap map[string]interface{}
err = setResultFromRPCResponse(&resultMap, map[string]interface{}{"test": true})
require.NoError(t, err)
require.Equal(t, map[string]interface{}{"test": true}, resultMap)
var resultStruct struct {
A int
B string
}
err = setResultFromRPCResponse(&resultStruct, struct {
A int
B string
}{5, "test"})
require.NoError(t, err)
require.Equal(t, struct {
A int
B string
}{5, "test"}, resultStruct)
var resultIncorrectType []int
err = setResultFromRPCResponse(&resultIncorrectType, []string{"a", "b"})
require.Error(t, err)
}

View File

@ -1,11 +1,13 @@
package rpc_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/geth/node"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/geth/rpc"
@ -242,3 +244,40 @@ func (s *RPCTestSuite) TestCallRPC() {
}
}
}
// TestCallRawResult checks if returned response is a valid JSON-RPC response.
func (s *RPCTestSuite) TestCallRawResult() {
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
s.NoError(err)
nodeStarted, err := s.NodeManager.StartNode(nodeConfig)
s.NoError(err)
defer s.NodeManager.StopNode()
<-nodeStarted
client := s.NodeManager.RPCClient()
jsonResult := client.CallRaw(`{"jsonrpc":"2.0","method":"shh_version","params":[],"id":67}`)
s.Equal(`{"jsonrpc":"2.0","id":67,"result":"5.0"}`, jsonResult)
}
// TestCallContextResult checks if result passed to CallContext
// is set accordingly to its underlying memory layout.
func (s *RPCTestSuite) TestCallContextResult() {
nodeConfig, err := MakeTestNodeConfig(params.RopstenNetworkID)
s.NoError(err)
nodeStarted, err := s.NodeManager.StartNode(nodeConfig)
s.NoError(err)
defer s.NodeManager.StopNode()
<-nodeStarted
client := s.NodeManager.RPCClient()
var blockNumber hexutil.Uint
err = client.CallContext(context.Background(), &blockNumber, "eth_blockNumber")
s.NoError(err)
s.True(blockNumber > 0, "blockNumber should be higher than 0")
}

View File

@ -35,7 +35,8 @@ func (r *router) routeRemote(method string) bool {
// remoteMethods contains methods that should be routed to
// the upstream node; the rest is considered to be routed to
// the local node.
// TODO(tiabc): Write a test on each of these methods to ensure they're all routed to the proper node and ensure they really work.
// TODO(tiabc): Write a test on each of these methods to ensure they're all routed to the proper node and ensure they really work
// TODO(tiabc: as we already caught https://github.com/status-im/status-go/issues/350 as the result of missing such test.
// Although it's tempting to only list methods coming to the local node as there're fewer of them
// but it's deceptive: we want to ensure that only known requests leave our zone of responsibility.
// Also, we want new requests in newer Geth versions not to be accidentally routed to the upstream.
@ -47,7 +48,7 @@ var remoteMethods = [...]string{
"eth_mining",
"eth_hashrate",
"eth_gasPrice",
//"eth_accounts", // goes to the local because we handle sub-accounts
//"eth_accounts", // due to sub-accounts handling
"eth_blockNumber",
"eth_getBalance",
"eth_getStorageAt",
@ -57,8 +58,8 @@ var remoteMethods = [...]string{
"eth_getUncleCountByBlockHash",
"eth_getUncleCountByBlockNumber",
"eth_getCode",
//"eth_sign", // goes to the local because only the local node has an injected account to sign the payload with
"eth_sendTransaction",
//"eth_sign", // only the local node has an injected account to sign the payload with
//"eth_sendTransaction", // we handle this specially calling eth_estimateGas, signing it locally and sending eth_sendRawTransaction afterwards
"eth_sendRawTransaction",
"eth_call",
"eth_estimateGas",

View File

@ -12,12 +12,12 @@ func TestRouteWithUpstream(t *testing.T) {
router := newRouter(true)
for _, method := range remoteMethods {
require.True(t, router.routeRemote(method))
require.True(t, router.routeRemote(method), "method "+method+" should routed to remote")
}
for _, method := range localMethods {
t.Run(method, func(t *testing.T) {
require.False(t, router.routeRemote(method))
require.False(t, router.routeRemote(method), "method "+method+" should routed to local")
})
}
}
@ -26,10 +26,10 @@ func TestRouteWithoutUpstream(t *testing.T) {
router := newRouter(false)
for _, method := range remoteMethods {
require.True(t, router.routeRemote(method))
require.False(t, router.routeRemote(method), "method "+method+" should routed to locally without UpstreamEnabled")
}
for _, method := range localMethods {
require.True(t, router.routeRemote(method))
require.False(t, router.routeRemote(method), "method "+method+" should routed to local")
}
}

File diff suppressed because one or more lines are too long