From 68bde74d16d43848ea50e54ec62735bfe4079d2d Mon Sep 17 00:00:00 2001 From: RomanZacharia Date: Thu, 23 Jun 2016 13:59:10 +0300 Subject: [PATCH 1/3] Added error handling for json-rpc --- pyethapp/jsonrpc.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pyethapp/jsonrpc.py b/pyethapp/jsonrpc.py index e21770cd..f50312de 100644 --- a/pyethapp/jsonrpc.py +++ b/pyethapp/jsonrpc.py @@ -53,6 +53,42 @@ def _fail_on_error_dispatch(self, request): RPCDispatcher._dispatch = _fail_on_error_dispatch +class EthRPCErrorResponse(JSONRPCErrorResponse): + edata = [] + + def _to_dict(self): + return { + 'jsonrpc': JSONRPCProtocol.JSON_RPC_VERSION, + 'id': self.unique_id, + 'error': { + 'message': str(self.error), + 'code': self._jsonrpc_error_code, + 'data': self.edata + } + } + + def add_extended_error(self, code, message, reason): + self.edata.append({'code': str(code), 'message': message, 'reason': reason}) + + +def _error_respond(ctx, error, data = []): + if not ctx.unique_id: + return None + + response = EthRPCErrorResponse() + + code, msg = _get_code_and_message(error) + + response.error = msg + response.unique_id = ctx.unique_id + response._jsonrpc_error_code = code + response.data = data + return response + + +JSONRPCRequest.error_respond = _error_respond + + # route logging messages From 3fc0af6fccbb334ace028fd641e9110336ae5eb0 Mon Sep 17 00:00:00 2001 From: RomanZacharia Date: Fri, 1 Jul 2016 15:25:57 +0300 Subject: [PATCH 2/3] Updated the error handling mechanism for the json-rpc --- pyethapp/jsonrpc.py | 94 ++++++++++++++++++++++++++++++---- pyethapp/tests/test_jsonrpc.py | 80 ++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 25 deletions(-) diff --git a/pyethapp/jsonrpc.py b/pyethapp/jsonrpc.py index f50312de..3b695f84 100644 --- a/pyethapp/jsonrpc.py +++ b/pyethapp/jsonrpc.py @@ -21,8 +21,9 @@ import rlp from tinyrpc.dispatch import RPCDispatcher from tinyrpc.dispatch import public as public_ -from tinyrpc.exc import BadRequestError, MethodNotFoundError -from tinyrpc.protocols.jsonrpc import JSONRPCProtocol, JSONRPCInvalidParamsError +from tinyrpc.exc import BadRequestError, MethodNotFoundError, RPCError +from tinyrpc.protocols.jsonrpc import JSONRPCProtocol, JSONRPCInvalidParamsError, JSONRPCRequest,\ + JSONRPCErrorResponse, _get_code_and_message, FixedErrorMessageMixin from tinyrpc.server.gevent import RPCServerGreenlets from tinyrpc.transports.wsgi import WsgiServerTransport from tinyrpc.transports import ServerTransport @@ -35,6 +36,7 @@ from ipc_rpc import bind_unix_listener, serve from ethereum.utils import int32 +from ethereum.exceptions import InvalidTransaction logger = log = slogging.get_logger('jsonrpc') @@ -53,6 +55,60 @@ def _fail_on_error_dispatch(self, request): RPCDispatcher._dispatch = _fail_on_error_dispatch +class ExecutionError(RPCError): + """An execution error in the RPC system occured.""" + + +class JSONRPCExecutionError(FixedErrorMessageMixin, ExecutionError): + jsonrpc_error_code = 3 + message = 'Execution error' + + + +class InsufficientGasError(JSONRPCExecutionError): + edata = [ + { + 'code': 102, + 'message': 'Insufficient gas' + } + ] + + +class GasLimitExceededError(JSONRPCExecutionError): + edata = [ + { + 'code': 103, + 'message': 'Gas limit exceeded' + } + ] + + +class RejectedError(JSONRPCExecutionError): + edata = [ + { + 'code': 104, + 'message': 'Rejected: Inappropriate value' + # The 'reason' data field might be excessive + # 'reason': 'Inappropriate value' + } + ] + + def __init__(self, message): + self.edata[0]['message'] = 'Rejected:' + message + + +def get_code_and_message(error): + if isinstance(error, ExecutionError): + code = error.jsonrpc_error_code + msg = error.message + + else: + return _get_code_and_message(error) + + return code, msg + + + class EthRPCErrorResponse(JSONRPCErrorResponse): edata = [] @@ -71,18 +127,19 @@ def add_extended_error(self, code, message, reason): self.edata.append({'code': str(code), 'message': message, 'reason': reason}) -def _error_respond(ctx, error, data = []): +def _error_respond(ctx, error): if not ctx.unique_id: return None response = EthRPCErrorResponse() - code, msg = _get_code_and_message(error) + code, msg = get_code_and_message(error) response.error = msg response.unique_id = ctx.unique_id response._jsonrpc_error_code = code - response.data = data + if hasattr(error, 'edata'): + response.edata = error.edata return response @@ -375,13 +432,21 @@ def register(cls, json_rpc_service): json_rpc_service.dispatcher.register_instance(dispatcher, cls.prefix) -def quantity_decoder(data): +def quantity_decoder(data, name=None): """Decode `data` representing a quantity.""" + if type(data) is dict and name: + data = data[name] if not is_string(data): + if name: + raise RejectedError(name + ' value should be a string representing its quantity') success = False elif not data.startswith('0x'): + if name: + raise RejectedError(name + ' value must start with 0x prefix') success = False # must start with 0x prefix elif len(data) > 3 and data[2] == '0': + if name: + raise RejectedError(name + ' value must not have leading zeros (except `0x0`)') success = False # must not have leading zeros (except `0x0`) else: data = data[2:] @@ -393,6 +458,8 @@ def quantity_decoder(data): except ValueError: success = False assert not success + if name: + raise RejectedError('Invalid ' + name + ' value') raise BadRequestError('Invalid quantity encoding') @@ -1199,15 +1266,15 @@ def call(self, data, block_id='pending'): raise BadRequestError('Transaction must be an object') to = address_decoder(data['to']) try: - startgas = quantity_decoder(data['gas']) + startgas = quantity_decoder(data, 'gas') except KeyError: startgas = test_block.gas_limit - test_block.gas_used try: - gasprice = quantity_decoder(data['gasPrice']) + gasprice = quantity_decoder(data, 'gasPrice') except KeyError: gasprice = 0 try: - value = quantity_decoder(data['value']) + value = quantity_decoder(data, 'value') except KeyError: value = 0 try: @@ -1225,9 +1292,13 @@ def call(self, data, block_id='pending'): tx.sender = sender try: + errmsg = 'Invalid transaction' success, output = processblock.apply_transaction(test_block, tx) - except processblock.InvalidTransaction: + except processblock.InsufficientBalance as e: + raise InsufficientGasError() + except InvalidTransaction as e: success = False + errmsg = e.__class__.__name__ + e.message # make sure we didn't change the real state snapshot_after = block.snapshot() assert snapshot_after == snapshot_before @@ -1236,7 +1307,8 @@ def call(self, data, block_id='pending'): if success: return output else: - return False + raise RejectedError(errmsg) + @public @decode_arg('block_id', block_id_decoder) diff --git a/pyethapp/tests/test_jsonrpc.py b/pyethapp/tests/test_jsonrpc.py index 87f307e5..dafa2179 100644 --- a/pyethapp/tests/test_jsonrpc.py +++ b/pyethapp/tests/test_jsonrpc.py @@ -27,10 +27,17 @@ from pyethapp.profiles import PROFILES from pyethapp.pow_service import PoWService +from tinyrpc.exc import RPCError +import json + ethereum.keys.PBKDF2_CONSTANTS['c'] = 100 # faster key derivation log = get_logger('test.jsonrpc') # pylint: disable=invalid-name -SOLIDITY_AVAILABLE = 'solidity' in Compilers().compilers +SOLIDITY_AVAILABLE = False +try: + SOLIDITY_AVAILABLE = 'solidity' in Compilers().compilers +except: + pass # EVM code corresponding to the following solidity code: # @@ -51,19 +58,6 @@ ).decode('hex') -def test_externally(): - # The results of the external rpc-tests are not evaluated as: - # 1) the Whisper protocol is not implemented and its tests fail; - # 2) the eth_accounts method should be skipped; - # 3) the eth_getFilterLogs fails due to the invalid test data; - os.system(''' - git clone https://github.com/ethereum/rpc-tests; - cd rpc-tests; - git submodule update --init --recursive; - npm install; - rm -rf /tmp/rpctests; - pyethapp -d /tmp/rpctests -l :info,eth.chainservice:debug,jsonrpc:debug -c jsonrpc.listen_port=8081 -c p2p.max_peers=0 -c p2p.min_peers=0 blocktest lib/tests/BlockchainTests/bcRPC_API_Test.json RPC_API_Test & sleep 60 && make test; - ''') @pytest.mark.skipif(not SOLIDITY_AVAILABLE, reason='solidity compiler not available') @@ -170,6 +164,20 @@ def rpc_request(self, method, *args): log.debug('got response', response=res) return res + def dispatch(self, message): + try: + server = JSONRPCServer(self) + rpcrequest = server.rpc_server.protocol.parse_request(message) + try: + response = server.dispatcher.dispatch(rpcrequest) + except Exception as e: + # an error occured within the method, return it + response = rpcrequest.error_respond(e) + except RPCError as e: + response = e.error_respond() + # respond with result` + return response._to_dict() + config = { 'data_dir': str(tmpdir), 'db': {'implementation': 'EphemDB'}, @@ -225,6 +233,50 @@ def fin(): return app +def test_rpc_errors(test_app): + + reqid = 0 + chain = test_app.services.chain.chain + assert chain.head_candidate.get_balance('\xff' * 20) == 0 + sender = test_app.services.accounts.unlocked_accounts[0].address + assert chain.head_candidate.get_balance(sender) > 0 + tx = { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'value': quantity_encoder(1) + } + # res = data_decoder(test_app.rpc_request('eth_call', tx, 0x1)) + + # tr = type(res) + + import pdb; pdb.set_trace() + + # json01 = '{"id":4,"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f","data":"0x12a7b914"},"0x8"]}' + # json01 = '{"id":4,"jsonrpc":"2.0","method":"eth_sendTransaction","params":['+json.dumps(tx)+']}' + reqid += 1 + json02 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_call", + "params": + [ + { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'gasPrice': '0x999999999999999999999999999999999999999999', + 'value': quantity_encoder(100), + 'data': '12345' + } + ] + } + response = test_app.dispatch(json.dumps(json02)) + + assert response['error']['code'] == 3 + assert response['error']['data'][0]['code'] == 104 + # res = data_decoder(test_app.rpc_request('eth_getTransactionByHash', 0x1)) + + + def test_send_transaction(test_app): chain = test_app.services.chain.chain assert chain.head_candidate.get_balance('\xff' * 20) == 0 From 609d08e988bf8c5ade303b86efcac786a5d9e95e Mon Sep 17 00:00:00 2001 From: RomanZacharia Date: Fri, 12 Aug 2016 14:52:14 +0300 Subject: [PATCH 3/3] Fixed the Filter not found error code --- pyethapp/jsonrpc.py | 75 +++++++++++------- pyethapp/tests/test_jsonrpc.py | 138 +++++++++++++++++++++++++++++---- 2 files changed, 172 insertions(+), 41 deletions(-) diff --git a/pyethapp/jsonrpc.py b/pyethapp/jsonrpc.py index 3b695f84..0abb218a 100644 --- a/pyethapp/jsonrpc.py +++ b/pyethapp/jsonrpc.py @@ -64,6 +64,16 @@ class JSONRPCExecutionError(FixedErrorMessageMixin, ExecutionError): message = 'Execution error' +class NotExistError(JSONRPCExecutionError): + edata = [ + { + 'code': 100, + 'message': 'The item does not exist' + } + ] + + def __init__(self, message): + self.edata[0]['message'] = message class InsufficientGasError(JSONRPCExecutionError): edata = [ @@ -88,8 +98,6 @@ class RejectedError(JSONRPCExecutionError): { 'code': 104, 'message': 'Rejected: Inappropriate value' - # The 'reason' data field might be excessive - # 'reason': 'Inappropriate value' } ] @@ -470,8 +478,10 @@ def quantity_encoder(i): return '0x' + (encode_hex(data).lstrip('0') or '0') -def data_decoder(data): +def data_decoder(data, name=None): """Decode `data` representing unformatted data.""" + if type(data) is dict and name: + data = data[name] if not data.startswith('0x'): data = '0x' + data if len(data) % 2 != 0: @@ -482,6 +492,8 @@ def data_decoder(data): try: return decode_hex(data[2:]) except TypeError: + if name: + raise RejectedError('Invalid ' + name + ' data hex encoding') raise BadRequestError('Invalid data hex encoding', data[2:]) @@ -498,10 +510,12 @@ def data_encoder(data, length=None): return '0x' + s.rjust(length * 2, '0') -def address_decoder(data): +def address_decoder(data, name=None): """Decode an address from hex with 0x prefix to 20 bytes.""" - addr = data_decoder(data) + addr = data_decoder(data, name) if len(addr) not in (20, 0): + if name: + raise RejectedError(name + ' address must be 20 or 0 bytes long') raise BadRequestError('Addresses must be 20 or 0 bytes long') return addr @@ -1173,7 +1187,7 @@ def sendTransaction(self, data): def get_data_default(key, decoder, default=None): if key in data: - return decoder(data[key]) + return decoder(data, key) return default to = get_data_default('to', address_decoder, b'') @@ -1198,13 +1212,19 @@ def get_data_default(key, decoder, default=None): if nonce is None or nonce == 0: nonce = self.app.services.chain.chain.head_candidate.get_nonce(sender) - tx = Transaction(nonce, gasprice, startgas, to, value, data_, v, r, s) - tx._sender = None - if not signed: - assert sender in self.app.services.accounts, 'can not sign: no account for sender' - self.app.services.accounts.sign_tx(sender, tx) - self.app.services.chain.add_transaction(tx, origin=None, force_broadcast=True) - log.debug('decoded tx', tx=tx.log_dict()) + try: + tx = Transaction(nonce, gasprice, startgas, to, value, data_, v, r, s) + tx._sender = None + if not signed: + assert sender in self.app.services.accounts, 'can not sign: no account for sender' + self.app.services.accounts.sign_tx(sender, tx) + self.app.services.chain.add_transaction(tx, origin=None, force_broadcast=True) + log.debug('decoded tx', tx=tx.log_dict()) + except InvalidTransaction as e: + if 'Startgas too low' in e.message: + raise InsufficientGasError() + raise + return data_encoder(tx.hash) @public @@ -1278,27 +1298,29 @@ def call(self, data, block_id='pending'): except KeyError: value = 0 try: - data_ = data_decoder(data['data']) + data_ = data_decoder(data, 'data') except KeyError: data_ = b'' try: - sender = address_decoder(data['from']) + sender = address_decoder(data, 'from') except KeyError: sender = '\x00' * 20 - # apply transaction - nonce = test_block.get_nonce(sender) - tx = Transaction(nonce, gasprice, startgas, to, value, data_) - tx.sender = sender - try: + # apply transaction + nonce = test_block.get_nonce(sender) + tx = Transaction(nonce, gasprice, startgas, to, value, data_) + tx.sender = sender errmsg = 'Invalid transaction' success, output = processblock.apply_transaction(test_block, tx) - except processblock.InsufficientBalance as e: + except processblock.InsufficientBalance: raise InsufficientGasError() except InvalidTransaction as e: + if 'Startgas too low' in e.message: + raise InsufficientGasError() success = False errmsg = e.__class__.__name__ + e.message + # make sure we didn't change the real state snapshot_after = block.snapshot() assert snapshot_after == snapshot_before @@ -1361,12 +1383,13 @@ def estimateGas(self, data, block_id='pending'): # apply transaction nonce = test_block.get_nonce(sender) - tx = Transaction(nonce, gasprice, startgas, to, value, data_) - tx.sender = sender - try: + tx = Transaction(nonce, gasprice, startgas, to, value, data_) + tx.sender = sender success, output = processblock.apply_transaction(test_block, tx) - except processblock.InvalidTransaction: + except processblock.InvalidTransaction as e: + if 'Startgas too low' in e.message: + raise InsufficientGasError() success = False # make sure we didn't change the real state snapshot_after = block.snapshot() @@ -1644,7 +1667,7 @@ def uninstallFilter(self, id_): @decode_arg('id_', quantity_decoder) def getFilterChanges(self, id_): if id_ not in self.filters: - raise BadRequestError('Unknown filter') + raise NotExistError('Unknown filter') filter_ = self.filters[id_] logger.debug('filter found', filter=filter_) if isinstance(filter_, (BlockFilter, PendingTransactionFilter)): diff --git a/pyethapp/tests/test_jsonrpc.py b/pyethapp/tests/test_jsonrpc.py index dafa2179..98b21f44 100644 --- a/pyethapp/tests/test_jsonrpc.py +++ b/pyethapp/tests/test_jsonrpc.py @@ -240,19 +240,7 @@ def test_rpc_errors(test_app): assert chain.head_candidate.get_balance('\xff' * 20) == 0 sender = test_app.services.accounts.unlocked_accounts[0].address assert chain.head_candidate.get_balance(sender) > 0 - tx = { - 'from': address_encoder(sender), - 'to': address_encoder('\xff' * 20), - 'value': quantity_encoder(1) - } - # res = data_decoder(test_app.rpc_request('eth_call', tx, 0x1)) - - # tr = type(res) - import pdb; pdb.set_trace() - - # json01 = '{"id":4,"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f","data":"0x12a7b914"},"0x8"]}' - # json01 = '{"id":4,"jsonrpc":"2.0","method":"eth_sendTransaction","params":['+json.dumps(tx)+']}' reqid += 1 json02 = { "id": reqid, @@ -263,18 +251,138 @@ def test_rpc_errors(test_app): { 'from': address_encoder(sender), 'to': address_encoder('\xff' * 20), - 'gasPrice': '0x999999999999999999999999999999999999999999', + 'gas': '0x1', + 'gasPrice': '0x7777', 'value': quantity_encoder(100), - 'data': '12345' } ] } response = test_app.dispatch(json.dumps(json02)) + assert response['error']['code'] == 3 + assert response['error']['data'][0]['code'] == 102 + + reqid += 1 + json03 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_call", + "params": + [ + { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'gasPrice': '0x01', + 'value': quantity_encoder(100), + } + ] + } + response = test_app.dispatch(json.dumps(json03)) assert response['error']['code'] == 3 assert response['error']['data'][0]['code'] == 104 - # res = data_decoder(test_app.rpc_request('eth_getTransactionByHash', 0x1)) + reqid += 1 + json04 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_call", + "params": + [ + { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'gas': '0x1', + 'gasPrice': '0x0', + 'value': quantity_encoder(10000000), + } + ] + } + response = test_app.dispatch(json.dumps(json04)) + assert response['error']['code'] == 3 + assert response['error']['data'][0]['code'] == 102 + + reqid += 1 + json05 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_call", + "params": + [ + { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'gasPrice': '0x0', + 'value': quantity_encoder(10000000), + } + ] + } + response = test_app.dispatch(json.dumps(json05)) + assert 'result' in response.keys() + + reqid += 1 + json06 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_sendTransaction", + "params": + [ + { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'value': quantity_encoder(1), + } + ] + } + response = test_app.dispatch(json.dumps(json06)) + assert 'result' in response.keys() + + reqid += 1 + json07 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_sendTransaction", + "params": + [ + { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'gas': '0x300000', + 'gasPrice': '0x0', + 'value': quantity_encoder(1), + } + ] + } + response = test_app.dispatch(json.dumps(json07)) + assert 'result' in response.keys() + + reqid += 1 + json08 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_getBlockByNumber", + "params": + [ + "earliest", + False + ] + } + response = test_app.dispatch(json.dumps(json08)) + assert 'result' in response.keys() + + reqid += 1 + json09 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_getFilterChanges", + "params": + [ + "0x12345" # invald filter id + ] + } + + response = test_app.dispatch(json.dumps(json09)) + assert response['error']['code'] == 3 + assert response['error']['data'][0]['code'] == 100 def test_send_transaction(test_app):