Skip to content
This repository was archived by the owner on Aug 8, 2018. It is now read-only.

Fixed jsonrpc Error handling #161

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 162 additions & 31 deletions pyethapp/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')

Expand All @@ -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


Expand Down Expand Up @@ -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:]
Expand All @@ -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')


Expand All @@ -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:
Expand All @@ -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:])


Expand All @@ -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

Expand Down Expand Up @@ -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'')
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)):
Expand Down
Loading