Skip to content

Commit 135ab68

Browse files
author
Neil Booth
committed
Simple protocol negotiation and setting of handlers
It turns out clients pass 0.10 instead of 1.0 as the protocol version. Distinguish some handlers for 1.0 and 1.1 protocols. Log protocol version request Add tests of new library function
1 parent eb91522 commit 135ab68

File tree

6 files changed

+112
-46
lines changed

6 files changed

+112
-46
lines changed

lib/jsonrpc.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class JSONRPC(object):
6363
INVALID_RESPONSE = -100
6464
ERROR_CODE_UNAVAILABLE = -101
6565
REQUEST_TIMEOUT = -102
66+
FATAL_ERROR = -103
6667

6768
ID_TYPES = (type(None), str, numbers.Number)
6869
HAS_BATCHES = False
@@ -405,7 +406,8 @@ def error_bytes(self, message, code, id_=None):
405406
self.error_count += 1
406407
if not self.close_after_send:
407408
fatal_log = None
408-
if code in (version.PARSE_ERROR, version.INVALID_REQUEST):
409+
if code in (version.PARSE_ERROR, version.INVALID_REQUEST,
410+
version.FATAL_ERROR):
409411
fatal_log = message
410412
elif self.error_count >= 10:
411413
fatal_log = 'too many errors, last: {}'.format(message)

lib/util.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,12 @@ def is_valid_hostname(hostname):
269269
if hostname and hostname[-1] == ".":
270270
hostname = hostname[:-1]
271271
return all(SEGMENT_REGEX.match(x) for x in hostname.split("."))
272+
273+
def protocol_tuple(s):
274+
'''Converts a protocol version number, such as "1.0" to a tuple (1, 0).
275+
276+
If the version number is bad, (0, ) indicating version 0 is returned.'''
277+
try:
278+
return tuple(int(part) for part in s.split('.'))
279+
except Exception:
280+
return (0, )

server/controller.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def __init__(self, env):
100100
'address.get_balance address.get_history address.get_mempool '
101101
'address.get_proof address.listunspent '
102102
'block.get_header estimatefee relayfee '
103-
'transaction.get transaction.get_merkle utxo.get_address'),
103+
'transaction.get_merkle utxo.get_address'),
104104
('server', 'donation_address'),
105105
]
106106
self.electrumx_handlers = {'.'.join([prefix, suffix]):
@@ -672,14 +672,14 @@ def address_to_hashX(self, address):
672672
pass
673673
raise RPCError('{} is not a valid address'.format(address))
674674

675-
def script_hash_to_hashX(self, script_hash):
675+
def scripthash_to_hashX(self, scripthash):
676676
try:
677-
bin_hash = hex_str_to_hash(script_hash)
677+
bin_hash = hex_str_to_hash(scripthash)
678678
if len(bin_hash) == 32:
679679
return bin_hash[:self.coin.HASHX_LEN]
680680
except Exception:
681681
pass
682-
raise RPCError('{} is not a valid script hash'.format(script_hash))
682+
raise RPCError('{} is not a valid script hash'.format(scripthash))
683683

684684
def assert_tx_hash(self, value):
685685
'''Raise an RPCError if the value is not a valid transaction
@@ -844,17 +844,23 @@ async def relayfee(self):
844844
to the daemon's memory pool.'''
845845
return await self.daemon_request('relayfee')
846846

847-
async def transaction_get(self, tx_hash, height=None):
847+
async def transaction_get(self, tx_hash):
848848
'''Return the serialized raw transaction given its hash
849849
850850
tx_hash: the transaction hash as a hexadecimal string
851-
height: ignored, do not use
852851
'''
853-
# For some reason Electrum passes a height. We don't require
854-
# it in anticipation it might be dropped in the future.
855852
self.assert_tx_hash(tx_hash)
856853
return await self.daemon_request('getrawtransaction', tx_hash)
857854

855+
async def transaction_get_1_0(self, tx_hash, height=None):
856+
'''Return the serialized raw transaction given its hash
857+
858+
tx_hash: the transaction hash as a hexadecimal string
859+
height: ignored, do not use
860+
'''
861+
# For some reason Electrum protocol 1.0 passes a height.
862+
return await self.transaction_get(tx_hash)
863+
858864
async def transaction_get_merkle(self, tx_hash, height):
859865
'''Return the markle tree to a confirmed transaction given its hash
860866
and height.

server/session.py

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from lib.hash import sha256, hash_to_str
1515
from lib.jsonrpc import JSONSession, RPCError, JSONRPCv2, JSONRPC
16+
import lib.util as util
1617
from server.daemon import DaemonError
1718
import server.version as version
1819

@@ -35,7 +36,6 @@ def __init__(self, controller, kind):
3536
self.daemon = self.bp.daemon
3637
self.client = 'unknown'
3738
self.client_version = (1, )
38-
self.protocol_version = '1.0'
3939
self.anon_logs = self.env.anon_logs
4040
self.last_delay = 0
4141
self.txs_sent = 0
@@ -113,19 +113,7 @@ def __init__(self, *args, **kwargs):
113113
self.hashX_subs = {}
114114
self.mempool_statuses = {}
115115
self.chunk_indices = []
116-
self.electrumx_handlers = {
117-
'blockchain.address.subscribe': self.address_subscribe,
118-
'blockchain.block.get_chunk': self.block_get_chunk,
119-
'blockchain.headers.subscribe': self.headers_subscribe,
120-
'blockchain.numblocks.subscribe': self.numblocks_subscribe,
121-
'blockchain.script_hash.subscribe': self.script_hash_subscribe,
122-
'blockchain.transaction.broadcast': self.transaction_broadcast,
123-
'server.add_peer': self.add_peer,
124-
'server.banner': self.banner,
125-
'server.features': self.server_features,
126-
'server.peers.subscribe': self.peers_subscribe,
127-
'server.version': self.server_version,
128-
}
116+
self.set_protocol_handlers(None)
129117

130118
def sub_count(self):
131119
return len(self.hashX_subs)
@@ -164,7 +152,7 @@ async def notify(self, height, touched):
164152

165153
for alias_status in changed:
166154
if len(alias_status[0]) == 64:
167-
method = 'blockchain.script_hash.subscribe'
155+
method = 'blockchain.scripthash.subscribe'
168156
else:
169157
method = 'blockchain.address.subscribe'
170158
pairs.append((method, alias_status))
@@ -247,12 +235,12 @@ async def address_subscribe(self, address):
247235
hashX = self.controller.address_to_hashX(address)
248236
return await self.hashX_subscribe(hashX, address)
249237

250-
async def script_hash_subscribe(self, script_hash):
238+
async def scripthash_subscribe(self, scripthash):
251239
'''Subscribe to a script hash.
252240
253-
script_hash: the SHA256 hash of the script to subscribe to'''
254-
hashX = self.controller.script_hash_to_hashX(script_hash)
255-
return await self.hashX_subscribe(hashX, script_hash)
241+
scripthash: the SHA256 hash of the script to subscribe to'''
242+
hashX = self.controller.scripthash_to_hashX(scripthash)
243+
return await self.hashX_subscribe(hashX, scripthash)
256244

257245
def server_features(self):
258246
'''Returns a dictionary of server features.'''
@@ -333,47 +321,98 @@ def server_version(self, client_name=None, protocol_version=None):
333321
in self.client.split('.'))
334322
except Exception:
335323
pass
336-
if protocol_version is not None:
337-
self.protocol_version = protocol_version
324+
325+
self.log_info('protocol version {} requested'.format(protocol_version))
326+
self.set_protocol_handlers(protocol_version)
327+
338328
return version.VERSION
339329

340330
async def transaction_broadcast(self, raw_tx):
341331
'''Broadcast a raw transaction to the network.
342332
343333
raw_tx: the raw transaction as a hexadecimal string'''
344-
# An ugly API: current Electrum clients only pass the raw
345-
# transaction in hex and expect error messages to be returned in
346-
# the result field. And the server shouldn't be doing the client's
347-
# user interface job here.
334+
# This returns errors as JSON RPC errors, as is natural
348335
try:
349336
tx_hash = await self.daemon.sendrawtransaction([raw_tx])
350337
self.txs_sent += 1
351338
self.log_info('sent tx: {}'.format(tx_hash))
352339
self.controller.sent_tx(tx_hash)
353340
return tx_hash
354341
except DaemonError as e:
355-
error = e.args[0]
342+
error, = e.args
356343
message = error['message']
357344
self.log_info('sendrawtransaction: {}'.format(message),
358345
throttle=True)
346+
raise RPCError('the transaction was rejected by network rules.'
347+
'\n\n{}\n[{}]'.format(message, raw_tx))
348+
349+
async def transaction_broadcast_1_0(self, raw_tx):
350+
'''Broadcast a raw transaction to the network.
351+
352+
raw_tx: the raw transaction as a hexadecimal string'''
353+
# An ugly API: current Electrum clients only pass the raw
354+
# transaction in hex and expect error messages to be returned in
355+
# the result field. And the server shouldn't be doing the client's
356+
# user interface job here.
357+
try:
358+
return await self.transaction_broadcast(raw_tx)
359+
except RPCError as e:
360+
message, = e.args
359361
if 'non-mandatory-script-verify-flag' in message:
360-
return (
362+
message = (
361363
'Your client produced a transaction that is not accepted '
362364
'by the network any more. Please upgrade to Electrum '
363365
'2.5.1 or newer.'
364366
)
365367

366-
return (
367-
'The transaction was rejected by network rules. ({})\n[{}]'
368-
.format(message, raw_tx)
369-
)
368+
return message
369+
370+
def set_protocol_handlers(self, version_str):
371+
controller = self.controller
372+
if version_str is None:
373+
version_str = version.PROTOCOL_MIN
374+
ptuple = util.protocol_tuple(version_str)
375+
# Disconnect if requested protocol version in unsupported
376+
if (ptuple < util.protocol_tuple(version.PROTOCOL_MIN)
377+
or ptuple > util.protocol_tuple(version.PROTOCOL_MAX)):
378+
self.log_info('unsupported protocol version {}'
379+
.format(version_str))
380+
raise RPCError('unsupported protocol version: {}'
381+
.format(version_str), JSONRPC.FATAL_ERROR)
382+
383+
handlers = {
384+
'blockchain.address.subscribe': self.address_subscribe,
385+
'blockchain.block.get_chunk': self.block_get_chunk,
386+
'blockchain.headers.subscribe': self.headers_subscribe,
387+
'blockchain.numblocks.subscribe': self.numblocks_subscribe,
388+
'blockchain.transaction.broadcast': self.transaction_broadcast_1_0,
389+
'blockchain.transaction.get': controller.transaction_get_1_0,
390+
'server.add_peer': self.add_peer,
391+
'server.banner': self.banner,
392+
'server.features': self.server_features,
393+
'server.peers.subscribe': self.peers_subscribe,
394+
'server.version': self.server_version,
395+
}
396+
397+
handlers.update(controller.electrumx_handlers)
398+
399+
if ptuple >= (1, 1):
400+
# Remove deprecated methods
401+
del handlers['blockchain.address.get_proof']
402+
del handlers['blockchain.numblocks.subscribe']
403+
del handlers['blockchain.utxo.get_address']
404+
# Add new handlers
405+
handlers.update({
406+
'blockchain.scripthash.subscribe': self.scripthash_subscribe,
407+
'blockchain.transaction.broadcast': self.transaction_broadcast,
408+
'blockchain.transaction.get': controller.transaction_get,
409+
})
410+
411+
self.electrumx_handlers = handlers
370412

371413
def request_handler(self, method):
372414
'''Return the async handler for the given request method.'''
373-
handler = self.electrumx_handlers.get(method)
374-
if not handler:
375-
handler = self.controller.electrumx_handlers.get(method)
376-
return handler
415+
return self.electrumx_handlers.get(method)
377416

378417

379418
class LocalRPC(SessionBase):

server/version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Server name and protocol versions
22

33
VERSION = 'ElectrumX 1.0.17'
4-
PROTOCOL_MIN = '1.0'
5-
PROTOCOL_MAX = '1.0'
4+
PROTOCOL_MIN = '0.10'
5+
PROTOCOL_MAX = '1.1'

tests/lib/test_util.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,13 @@ def test_is_valid_hostname():
8383
len255 = ('a' * 62 + '.') * 4 + 'abc'
8484
assert is_valid_hostname(len255)
8585
assert not is_valid_hostname(len255 + 'd')
86+
87+
88+
def test_protocol_tuple():
89+
assert util.protocol_tuple(None) == (0, )
90+
assert util.protocol_tuple("foo") == (0, )
91+
assert util.protocol_tuple(1) == (0, )
92+
assert util.protocol_tuple("1") == (1, )
93+
assert util.protocol_tuple("0.1") == (0, 1)
94+
assert util.protocol_tuple("0.10") == (0, 10)
95+
assert util.protocol_tuple("2.5.3") == (2, 5, 3)

0 commit comments

Comments
 (0)