Browse Source

Merge branch 'release-0.8.6'

master 0.8.6
Neil Booth 8 years ago
parent
commit
b6e7eb3c04
  1. 6
      RELEASE-NOTES
  2. 3
      electrumx_rpc.py
  3. 130
      lib/jsonrpc.py
  4. 8
      server/protocol.py
  5. 2
      server/version.py

6
RELEASE-NOTES

@ -1,3 +1,9 @@
version 0.8.6
-------------
- fix JSON bugs from 0.8.5
- fix issue #61 (valesi)
version 0.8.5 version 0.8.5
------------- -------------

3
electrumx_rpc.py

@ -25,7 +25,8 @@ class RPCClient(JSONRPC):
async def send_and_wait(self, method, params, timeout=None): async def send_and_wait(self, method, params, timeout=None):
# Raise incoming buffer size - presumably connection is trusted # Raise incoming buffer size - presumably connection is trusted
self.max_buffer_size = 5000000 self.max_buffer_size = 5000000
self.send_json_request(method, id_=method, params=params) payload = self.request_payload(method, id_=method, params=params)
self.encode_and_send_payload(payload)
future = asyncio.ensure_future(self.messages.get()) future = asyncio.ensure_future(self.messages.get())
for f in asyncio.as_completed([future], timeout=timeout): for f in asyncio.as_completed([future], timeout=timeout):

130
lib/jsonrpc.py

@ -15,28 +15,6 @@ import time
from lib.util import LoggedClass from lib.util import LoggedClass
def json_response_payload(result, id_):
# We should not respond to notifications
assert id_ is not None
return {'jsonrpc': '2.0', 'result': result, 'id': id_}
def json_error_payload(message, code, id_=None):
error = {'message': message, 'code': code}
return {'jsonrpc': '2.0', 'error': error, 'id': id_}
def json_request_payload(method, id_, params=None):
payload = {'jsonrpc': '2.0', 'id': id_, 'method': method}
if params:
payload['params'] = params
return payload
def json_notification_payload(method, params=None):
return json_request_payload(method, None, params)
def json_payload_id(payload):
return payload.get('id') if isinstance(payload, dict) else None
class JSONRPC(asyncio.Protocol, LoggedClass): class JSONRPC(asyncio.Protocol, LoggedClass):
'''Manages a JSONRPC connection. '''Manages a JSONRPC connection.
@ -56,7 +34,6 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
handle_notification() and handle_response() should not return handle_notification() and handle_response() should not return
anything or raise any exceptions. All three functions have anything or raise any exceptions. All three functions have
default "ignore" implementations supplied by this class. default "ignore" implementations supplied by this class.
''' '''
# See http://www.jsonrpc.org/specification # See http://www.jsonrpc.org/specification
@ -79,6 +56,31 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
class LargeRequestError(Exception): class LargeRequestError(Exception):
'''Raised if a large request was prevented from being sent.''' '''Raised if a large request was prevented from being sent.'''
@classmethod
def request_payload(cls, method, id_, params=None):
payload = {'jsonrpc': '2.0', 'id': id_, 'method': method}
if params:
payload['params'] = params
return payload
@classmethod
def response_payload(cls, result, id_):
# We should not respond to notifications
assert id_ is not None
return {'jsonrpc': '2.0', 'result': result, 'id': id_}
@classmethod
def notification_payload(cls, method, params=None):
return cls.request_payload(method, None, params)
@classmethod
def error_payload(cls, message, code, id_=None):
error = {'message': message, 'code': code}
return {'jsonrpc': '2.0', 'error': error, 'id': id_}
@classmethod
def payload_id(cls, payload):
return payload.get('id') if isinstance(payload, dict) else None
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -182,14 +184,14 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
message = message.decode() message = message.decode()
except UnicodeDecodeError as e: except UnicodeDecodeError as e:
msg = 'cannot decode binary bytes: {}'.format(e) msg = 'cannot decode binary bytes: {}'.format(e)
self.send_json_error(msg, self.INVALID_REQUEST) self.send_json_error(msg, self.PARSE_ERROR)
return return
try: try:
message = json.loads(message) message = json.loads(message)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
msg = 'cannot decode JSON: {}'.format(e) msg = 'cannot decode JSON: {}'.format(e)
self.send_json_error(msg, self.INVALID_REQUEST) self.send_json_error(msg, self.PARSE_ERROR)
return return
'''Queue the request for asynchronous handling.''' '''Queue the request for asynchronous handling.'''
@ -199,53 +201,53 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
def encode_payload(self, payload): def encode_payload(self, payload):
try: try:
text = (json.dumps(payload) + '\n').encode() binary = json.dumps(payload).encode()
except TypeError: except TypeError:
msg = 'JSON encoding failure: {}'.format(payload) msg = 'JSON encoding failure: {}'.format(payload)
self.log_error(msg) self.log_error(msg)
return self.json_error(msg, self.INTERNAL_ERROR, return self.json_error(msg, self.INTERNAL_ERROR,
json_payload_id(payload)) self.payload_id(payload))
self.check_oversized_request(len(text)) self.check_oversized_request(len(binary))
if 'error' in payload:
self.error_count += 1
self.send_count += 1 self.send_count += 1
self.send_size += len(text) self.send_size += len(binary)
self.using_bandwidth(len(text)) self.using_bandwidth(len(binary))
return text return binary
def send_text(self, text, close): def _send_bytes(self, text, close):
'''Send JSON text over the transport. Close it if close is True.''' '''Send JSON text over the transport. Close it if close is True.'''
# Confirmed this happens, sometimes a lot # Confirmed this happens, sometimes a lot
if self.transport.is_closing(): if self.transport.is_closing():
return return
self.transport.write(text) self.transport.write(text)
self.transport.write(b'\n')
if close: if close:
self.transport.close() self.transport.close()
def send_json_error(self, message, code, id_=None, close=True): def send_json_error(self, message, code, id_=None, close=True):
'''Send a JSON error and close the connection by default.''' '''Send a JSON error and close the connection by default.'''
self.send_text(self.json_error_text(message, code, id_), close) self._send_bytes(self.json_error_bytes(message, code, id_), close)
def encode_and_send_payload(self, payload): def encode_and_send_payload(self, payload):
'''Encode the payload and send it.''' '''Encode the payload and send it.'''
self.send_text(self.encode_payload(payload), False) self._send_bytes(self.encode_payload(payload), False)
def json_notification_text(self, method, params): def json_notification_bytes(self, method, params):
'''Return the text of a json notification.''' '''Return the bytes of a json notification.'''
return self.encode_payload(json_notification_payload(method, params)) return self.encode_payload(self.notification_payload(method, params))
def json_request_text(self, method, id_, params=None): def json_request_bytes(self, method, id_, params=None):
'''Return the text of a JSON request.''' '''Return the bytes of a JSON request.'''
return self.encode_payload(json_request_payload(method, params)) return self.encode_payload(self.request_payload(method, id_, params))
def json_response_text(self, result, id_): def json_response_bytes(self, result, id_):
'''Return the text of a JSON response.''' '''Return the bytes of a JSON response.'''
return self.encode_payload(json_response_payload(result, id_)) return self.encode_payload(self.response_payload(result, id_))
def json_error_text(self, message, code, id_=None): def json_error_bytes(self, message, code, id_=None):
'''Return the text of a JSON error.''' '''Return the bytes of a JSON error.'''
return self.encode_payload(json_error_payload(message, code, id_)) self.error_count += 1
return self.encode_payload(self.error_payload(message, code, id_))
async def handle_message(self, payload): async def handle_message(self, payload):
'''Asynchronously handle a JSON request or response. '''Asynchronously handle a JSON request or response.
@ -254,21 +256,21 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
''' '''
try: try:
if isinstance(payload, list): if isinstance(payload, list):
text = await self.process_json_batch(payload) binary = await self.process_json_batch(payload)
else: else:
text = await self.process_single_json(payload) binary = await self.process_single_json(payload)
except self.RPCError as e: except self.RPCError as e:
text = self.json_error_text(e.msg, e.code, binary = self.json_error_bytes(e.msg, e.code,
json_payload_id(payload)) self.payload_id(payload))
if text: if binary:
self.send_text(text, self.error_count > 10) self._send_bytes(binary, self.error_count > 10)
async def process_json_batch(self, batch): async def process_json_batch(self, batch):
'''Return the text response to a JSON batch request.''' '''Return the text response to a JSON batch request.'''
# Batches must have at least one request. # Batches must have at least one request.
if not batch: if not batch:
return self.json_error_text('empty batch', self.INVALID_REQUEST) return self.json_error_bytes('empty batch', self.INVALID_REQUEST)
# PYTHON 3.6: use asynchronous comprehensions when supported # PYTHON 3.6: use asynchronous comprehensions when supported
parts = [] parts = []
@ -280,8 +282,8 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
total_len += len(part) + 2 total_len += len(part) + 2
self.check_oversized_request(total_len) self.check_oversized_request(total_len)
if parts: if parts:
return '{' + ', '.join(parts) + '}' return b'[' + b', '.join(parts) + b']'
return '' return b''
async def process_single_json(self, payload): async def process_single_json(self, payload):
'''Return the JSON result of a single JSON request, response or '''Return the JSON result of a single JSON request, response or
@ -300,16 +302,16 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
await asyncio.sleep(secs) await asyncio.sleep(secs)
if not isinstance(payload, dict): if not isinstance(payload, dict):
return self.json_error_text('request must be a dict', return self.json_error_bytes('request must be a dict',
self.INVALID_REQUEST) self.INVALID_REQUEST)
if not 'id' in payload: if not 'id' in payload:
return await self.process_json_notification(payload) return await self.process_json_notification(payload)
id_ = payload['id'] id_ = payload['id']
if not isinstance(id_, self.ID_TYPES): if not isinstance(id_, self.ID_TYPES):
return self.json_error_text('invalid id: {}'.format(id_), return self.json_error_bytes('invalid id: {}'.format(id_),
self.INVALID_REQUEST) self.INVALID_REQUEST)
if 'method' in payload: if 'method' in payload:
return await self.process_json_request(payload) return await self.process_json_request(payload)
@ -338,12 +340,12 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
pass pass
else: else:
await self.handle_notification(method, params) await self.handle_notification(method, params)
return '' return b''
async def process_json_request(self, payload): async def process_json_request(self, payload):
method, params = self.method_and_params(payload) method, params = self.method_and_params(payload)
result = await self.handle_request(method, params) result = await self.handle_request(method, params)
return self.json_response_text(result, payload['id']) return self.json_response_bytes(result, payload['id'])
async def process_json_response(self, payload): async def process_json_response(self, payload):
# Only one of result and error should exist; we go with 'error' # Only one of result and error should exist; we go with 'error'
@ -352,7 +354,7 @@ class JSONRPC(asyncio.Protocol, LoggedClass):
await self.handle_response(None, payload['error'], payload['id']) await self.handle_response(None, payload['error'], payload['id'])
elif 'result' in payload: elif 'result' in payload:
await self.handle_response(payload['result'], None, payload['id']) await self.handle_response(payload['result'], None, payload['id'])
return '' return b''
def check_oversized_request(self, total_len): def check_oversized_request(self, total_len):
if total_len > max(1000, self.max_send): if total_len > max(1000, self.max_send):

8
server/protocol.py

@ -20,7 +20,7 @@ from functools import partial
import pylru import pylru
from lib.hash import sha256, double_sha256, hash_to_str, hex_str_to_hash from lib.hash import sha256, double_sha256, hash_to_str, hex_str_to_hash
from lib.jsonrpc import JSONRPC, json_notification_payload from lib.jsonrpc import JSONRPC
from lib.tx import Deserializer from lib.tx import Deserializer
import lib.util as util import lib.util as util
from server.block_processor import BlockProcessor from server.block_processor import BlockProcessor
@ -687,14 +687,14 @@ class ElectrumX(Session):
if self.subscribe_headers: if self.subscribe_headers:
key = 'headers_payload' key = 'headers_payload'
if key not in cache: if key not in cache:
cache[key] = json_notification_payload( cache[key] = self.notification_payload(
'blockchain.headers.subscribe', 'blockchain.headers.subscribe',
(self.electrum_header(height), ), (self.electrum_header(height), ),
) )
self.encode_and_send_payload(cache[key]) self.encode_and_send_payload(cache[key])
if self.subscribe_height: if self.subscribe_height:
payload = json_notification_payload( payload = self.notification_payload(
'blockchain.numblocks.subscribe', 'blockchain.numblocks.subscribe',
(height, ), (height, ),
) )
@ -705,7 +705,7 @@ class ElectrumX(Session):
for hash168 in matches: for hash168 in matches:
address = hash168_to_address(hash168) address = hash168_to_address(hash168)
status = await self.address_status(hash168) status = await self.address_status(hash168)
payload = json_notification_payload( payload = self.notification_payload(
'blockchain.address.subscribe', (address, status)) 'blockchain.address.subscribe', (address, status))
self.encode_and_send_payload(payload) self.encode_and_send_payload(payload)

2
server/version.py

@ -1 +1 @@
VERSION = "ElectrumX 0.8.5" VERSION = "ElectrumX 0.8.6"

Loading…
Cancel
Save