diff --git a/pyethapp/jsonrpc.py b/pyethapp/jsonrpc.py index e21770cd..0abb218a 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,105 @@ 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 NotExistError(JSONRPCExecutionError): + edata = [ + { + 'code': 100, + 'message': 'The item does not exist' + } + ] + + def __init__(self, message): + self.edata[0]['message'] = message + +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' + } + ] + + 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 = [] + + 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): + 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 + if hasattr(error, 'edata'): + response.edata = error.edata + return response + + +JSONRPCRequest.error_respond = _error_respond + + # route logging messages @@ -339,13 +440,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:] @@ -357,6 +466,8 @@ def quantity_decoder(data): except ValueError: success = False assert not success + if name: + raise RejectedError('Invalid ' + name + ' value') raise BadRequestError('Invalid quantity encoding') @@ -367,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: @@ -379,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:]) @@ -395,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 @@ -1070,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'') @@ -1095,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 @@ -1163,35 +1286,41 @@ 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: - 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.InvalidTransaction: + 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 @@ -1200,7 +1329,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) @@ -1253,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() @@ -1536,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 87f307e5..98b21f44 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,158 @@ 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 + + reqid += 1 + json02 = { + "id": reqid, + "jsonrpc": "2.0", + "method": "eth_call", + "params": + [ + { + 'from': address_encoder(sender), + 'to': address_encoder('\xff' * 20), + 'gas': '0x1', + 'gasPrice': '0x7777', + 'value': quantity_encoder(100), + } + ] + } + 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 + + 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): chain = test_app.services.chain.chain assert chain.head_candidate.get_balance('\xff' * 20) == 0