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
This commit is contained in:
parent
eb91522d20
commit
135ab68f74
@ -63,6 +63,7 @@ class JSONRPC(object):
|
|||||||
INVALID_RESPONSE = -100
|
INVALID_RESPONSE = -100
|
||||||
ERROR_CODE_UNAVAILABLE = -101
|
ERROR_CODE_UNAVAILABLE = -101
|
||||||
REQUEST_TIMEOUT = -102
|
REQUEST_TIMEOUT = -102
|
||||||
|
FATAL_ERROR = -103
|
||||||
|
|
||||||
ID_TYPES = (type(None), str, numbers.Number)
|
ID_TYPES = (type(None), str, numbers.Number)
|
||||||
HAS_BATCHES = False
|
HAS_BATCHES = False
|
||||||
@ -405,7 +406,8 @@ class JSONSessionBase(util.LoggedClass):
|
|||||||
self.error_count += 1
|
self.error_count += 1
|
||||||
if not self.close_after_send:
|
if not self.close_after_send:
|
||||||
fatal_log = None
|
fatal_log = None
|
||||||
if code in (version.PARSE_ERROR, version.INVALID_REQUEST):
|
if code in (version.PARSE_ERROR, version.INVALID_REQUEST,
|
||||||
|
version.FATAL_ERROR):
|
||||||
fatal_log = message
|
fatal_log = message
|
||||||
elif self.error_count >= 10:
|
elif self.error_count >= 10:
|
||||||
fatal_log = 'too many errors, last: {}'.format(message)
|
fatal_log = 'too many errors, last: {}'.format(message)
|
||||||
|
|||||||
@ -269,3 +269,12 @@ def is_valid_hostname(hostname):
|
|||||||
if hostname and hostname[-1] == ".":
|
if hostname and hostname[-1] == ".":
|
||||||
hostname = hostname[:-1]
|
hostname = hostname[:-1]
|
||||||
return all(SEGMENT_REGEX.match(x) for x in hostname.split("."))
|
return all(SEGMENT_REGEX.match(x) for x in hostname.split("."))
|
||||||
|
|
||||||
|
def protocol_tuple(s):
|
||||||
|
'''Converts a protocol version number, such as "1.0" to a tuple (1, 0).
|
||||||
|
|
||||||
|
If the version number is bad, (0, ) indicating version 0 is returned.'''
|
||||||
|
try:
|
||||||
|
return tuple(int(part) for part in s.split('.'))
|
||||||
|
except Exception:
|
||||||
|
return (0, )
|
||||||
|
|||||||
@ -100,7 +100,7 @@ class Controller(util.LoggedClass):
|
|||||||
'address.get_balance address.get_history address.get_mempool '
|
'address.get_balance address.get_history address.get_mempool '
|
||||||
'address.get_proof address.listunspent '
|
'address.get_proof address.listunspent '
|
||||||
'block.get_header estimatefee relayfee '
|
'block.get_header estimatefee relayfee '
|
||||||
'transaction.get transaction.get_merkle utxo.get_address'),
|
'transaction.get_merkle utxo.get_address'),
|
||||||
('server', 'donation_address'),
|
('server', 'donation_address'),
|
||||||
]
|
]
|
||||||
self.electrumx_handlers = {'.'.join([prefix, suffix]):
|
self.electrumx_handlers = {'.'.join([prefix, suffix]):
|
||||||
@ -672,14 +672,14 @@ class Controller(util.LoggedClass):
|
|||||||
pass
|
pass
|
||||||
raise RPCError('{} is not a valid address'.format(address))
|
raise RPCError('{} is not a valid address'.format(address))
|
||||||
|
|
||||||
def script_hash_to_hashX(self, script_hash):
|
def scripthash_to_hashX(self, scripthash):
|
||||||
try:
|
try:
|
||||||
bin_hash = hex_str_to_hash(script_hash)
|
bin_hash = hex_str_to_hash(scripthash)
|
||||||
if len(bin_hash) == 32:
|
if len(bin_hash) == 32:
|
||||||
return bin_hash[:self.coin.HASHX_LEN]
|
return bin_hash[:self.coin.HASHX_LEN]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise RPCError('{} is not a valid script hash'.format(script_hash))
|
raise RPCError('{} is not a valid script hash'.format(scripthash))
|
||||||
|
|
||||||
def assert_tx_hash(self, value):
|
def assert_tx_hash(self, value):
|
||||||
'''Raise an RPCError if the value is not a valid transaction
|
'''Raise an RPCError if the value is not a valid transaction
|
||||||
@ -844,16 +844,22 @@ class Controller(util.LoggedClass):
|
|||||||
to the daemon's memory pool.'''
|
to the daemon's memory pool.'''
|
||||||
return await self.daemon_request('relayfee')
|
return await self.daemon_request('relayfee')
|
||||||
|
|
||||||
async def transaction_get(self, tx_hash, height=None):
|
async def transaction_get(self, tx_hash):
|
||||||
|
'''Return the serialized raw transaction given its hash
|
||||||
|
|
||||||
|
tx_hash: the transaction hash as a hexadecimal string
|
||||||
|
'''
|
||||||
|
self.assert_tx_hash(tx_hash)
|
||||||
|
return await self.daemon_request('getrawtransaction', tx_hash)
|
||||||
|
|
||||||
|
async def transaction_get_1_0(self, tx_hash, height=None):
|
||||||
'''Return the serialized raw transaction given its hash
|
'''Return the serialized raw transaction given its hash
|
||||||
|
|
||||||
tx_hash: the transaction hash as a hexadecimal string
|
tx_hash: the transaction hash as a hexadecimal string
|
||||||
height: ignored, do not use
|
height: ignored, do not use
|
||||||
'''
|
'''
|
||||||
# For some reason Electrum passes a height. We don't require
|
# For some reason Electrum protocol 1.0 passes a height.
|
||||||
# it in anticipation it might be dropped in the future.
|
return await self.transaction_get(tx_hash)
|
||||||
self.assert_tx_hash(tx_hash)
|
|
||||||
return await self.daemon_request('getrawtransaction', tx_hash)
|
|
||||||
|
|
||||||
async def transaction_get_merkle(self, tx_hash, height):
|
async def transaction_get_merkle(self, tx_hash, height):
|
||||||
'''Return the markle tree to a confirmed transaction given its hash
|
'''Return the markle tree to a confirmed transaction given its hash
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from functools import partial
|
|||||||
|
|
||||||
from lib.hash import sha256, hash_to_str
|
from lib.hash import sha256, hash_to_str
|
||||||
from lib.jsonrpc import JSONSession, RPCError, JSONRPCv2, JSONRPC
|
from lib.jsonrpc import JSONSession, RPCError, JSONRPCv2, JSONRPC
|
||||||
|
import lib.util as util
|
||||||
from server.daemon import DaemonError
|
from server.daemon import DaemonError
|
||||||
import server.version as version
|
import server.version as version
|
||||||
|
|
||||||
@ -35,7 +36,6 @@ class SessionBase(JSONSession):
|
|||||||
self.daemon = self.bp.daemon
|
self.daemon = self.bp.daemon
|
||||||
self.client = 'unknown'
|
self.client = 'unknown'
|
||||||
self.client_version = (1, )
|
self.client_version = (1, )
|
||||||
self.protocol_version = '1.0'
|
|
||||||
self.anon_logs = self.env.anon_logs
|
self.anon_logs = self.env.anon_logs
|
||||||
self.last_delay = 0
|
self.last_delay = 0
|
||||||
self.txs_sent = 0
|
self.txs_sent = 0
|
||||||
@ -113,19 +113,7 @@ class ElectrumX(SessionBase):
|
|||||||
self.hashX_subs = {}
|
self.hashX_subs = {}
|
||||||
self.mempool_statuses = {}
|
self.mempool_statuses = {}
|
||||||
self.chunk_indices = []
|
self.chunk_indices = []
|
||||||
self.electrumx_handlers = {
|
self.set_protocol_handlers(None)
|
||||||
'blockchain.address.subscribe': self.address_subscribe,
|
|
||||||
'blockchain.block.get_chunk': self.block_get_chunk,
|
|
||||||
'blockchain.headers.subscribe': self.headers_subscribe,
|
|
||||||
'blockchain.numblocks.subscribe': self.numblocks_subscribe,
|
|
||||||
'blockchain.script_hash.subscribe': self.script_hash_subscribe,
|
|
||||||
'blockchain.transaction.broadcast': self.transaction_broadcast,
|
|
||||||
'server.add_peer': self.add_peer,
|
|
||||||
'server.banner': self.banner,
|
|
||||||
'server.features': self.server_features,
|
|
||||||
'server.peers.subscribe': self.peers_subscribe,
|
|
||||||
'server.version': self.server_version,
|
|
||||||
}
|
|
||||||
|
|
||||||
def sub_count(self):
|
def sub_count(self):
|
||||||
return len(self.hashX_subs)
|
return len(self.hashX_subs)
|
||||||
@ -164,7 +152,7 @@ class ElectrumX(SessionBase):
|
|||||||
|
|
||||||
for alias_status in changed:
|
for alias_status in changed:
|
||||||
if len(alias_status[0]) == 64:
|
if len(alias_status[0]) == 64:
|
||||||
method = 'blockchain.script_hash.subscribe'
|
method = 'blockchain.scripthash.subscribe'
|
||||||
else:
|
else:
|
||||||
method = 'blockchain.address.subscribe'
|
method = 'blockchain.address.subscribe'
|
||||||
pairs.append((method, alias_status))
|
pairs.append((method, alias_status))
|
||||||
@ -247,12 +235,12 @@ class ElectrumX(SessionBase):
|
|||||||
hashX = self.controller.address_to_hashX(address)
|
hashX = self.controller.address_to_hashX(address)
|
||||||
return await self.hashX_subscribe(hashX, address)
|
return await self.hashX_subscribe(hashX, address)
|
||||||
|
|
||||||
async def script_hash_subscribe(self, script_hash):
|
async def scripthash_subscribe(self, scripthash):
|
||||||
'''Subscribe to a script hash.
|
'''Subscribe to a script hash.
|
||||||
|
|
||||||
script_hash: the SHA256 hash of the script to subscribe to'''
|
scripthash: the SHA256 hash of the script to subscribe to'''
|
||||||
hashX = self.controller.script_hash_to_hashX(script_hash)
|
hashX = self.controller.scripthash_to_hashX(scripthash)
|
||||||
return await self.hashX_subscribe(hashX, script_hash)
|
return await self.hashX_subscribe(hashX, scripthash)
|
||||||
|
|
||||||
def server_features(self):
|
def server_features(self):
|
||||||
'''Returns a dictionary of server features.'''
|
'''Returns a dictionary of server features.'''
|
||||||
@ -333,47 +321,98 @@ class ElectrumX(SessionBase):
|
|||||||
in self.client.split('.'))
|
in self.client.split('.'))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if protocol_version is not None:
|
|
||||||
self.protocol_version = protocol_version
|
self.log_info('protocol version {} requested'.format(protocol_version))
|
||||||
|
self.set_protocol_handlers(protocol_version)
|
||||||
|
|
||||||
return version.VERSION
|
return version.VERSION
|
||||||
|
|
||||||
async def transaction_broadcast(self, raw_tx):
|
async def transaction_broadcast(self, raw_tx):
|
||||||
'''Broadcast a raw transaction to the network.
|
'''Broadcast a raw transaction to the network.
|
||||||
|
|
||||||
|
raw_tx: the raw transaction as a hexadecimal string'''
|
||||||
|
# This returns errors as JSON RPC errors, as is natural
|
||||||
|
try:
|
||||||
|
tx_hash = await self.daemon.sendrawtransaction([raw_tx])
|
||||||
|
self.txs_sent += 1
|
||||||
|
self.log_info('sent tx: {}'.format(tx_hash))
|
||||||
|
self.controller.sent_tx(tx_hash)
|
||||||
|
return tx_hash
|
||||||
|
except DaemonError as e:
|
||||||
|
error, = e.args
|
||||||
|
message = error['message']
|
||||||
|
self.log_info('sendrawtransaction: {}'.format(message),
|
||||||
|
throttle=True)
|
||||||
|
raise RPCError('the transaction was rejected by network rules.'
|
||||||
|
'\n\n{}\n[{}]'.format(message, raw_tx))
|
||||||
|
|
||||||
|
async def transaction_broadcast_1_0(self, raw_tx):
|
||||||
|
'''Broadcast a raw transaction to the network.
|
||||||
|
|
||||||
raw_tx: the raw transaction as a hexadecimal string'''
|
raw_tx: the raw transaction as a hexadecimal string'''
|
||||||
# An ugly API: current Electrum clients only pass the raw
|
# An ugly API: current Electrum clients only pass the raw
|
||||||
# transaction in hex and expect error messages to be returned in
|
# transaction in hex and expect error messages to be returned in
|
||||||
# the result field. And the server shouldn't be doing the client's
|
# the result field. And the server shouldn't be doing the client's
|
||||||
# user interface job here.
|
# user interface job here.
|
||||||
try:
|
try:
|
||||||
tx_hash = await self.daemon.sendrawtransaction([raw_tx])
|
return await self.transaction_broadcast(raw_tx)
|
||||||
self.txs_sent += 1
|
except RPCError as e:
|
||||||
self.log_info('sent tx: {}'.format(tx_hash))
|
message, = e.args
|
||||||
self.controller.sent_tx(tx_hash)
|
|
||||||
return tx_hash
|
|
||||||
except DaemonError as e:
|
|
||||||
error = e.args[0]
|
|
||||||
message = error['message']
|
|
||||||
self.log_info('sendrawtransaction: {}'.format(message),
|
|
||||||
throttle=True)
|
|
||||||
if 'non-mandatory-script-verify-flag' in message:
|
if 'non-mandatory-script-verify-flag' in message:
|
||||||
return (
|
message = (
|
||||||
'Your client produced a transaction that is not accepted '
|
'Your client produced a transaction that is not accepted '
|
||||||
'by the network any more. Please upgrade to Electrum '
|
'by the network any more. Please upgrade to Electrum '
|
||||||
'2.5.1 or newer.'
|
'2.5.1 or newer.'
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return message
|
||||||
'The transaction was rejected by network rules. ({})\n[{}]'
|
|
||||||
.format(message, raw_tx)
|
def set_protocol_handlers(self, version_str):
|
||||||
)
|
controller = self.controller
|
||||||
|
if version_str is None:
|
||||||
|
version_str = version.PROTOCOL_MIN
|
||||||
|
ptuple = util.protocol_tuple(version_str)
|
||||||
|
# Disconnect if requested protocol version in unsupported
|
||||||
|
if (ptuple < util.protocol_tuple(version.PROTOCOL_MIN)
|
||||||
|
or ptuple > util.protocol_tuple(version.PROTOCOL_MAX)):
|
||||||
|
self.log_info('unsupported protocol version {}'
|
||||||
|
.format(version_str))
|
||||||
|
raise RPCError('unsupported protocol version: {}'
|
||||||
|
.format(version_str), JSONRPC.FATAL_ERROR)
|
||||||
|
|
||||||
|
handlers = {
|
||||||
|
'blockchain.address.subscribe': self.address_subscribe,
|
||||||
|
'blockchain.block.get_chunk': self.block_get_chunk,
|
||||||
|
'blockchain.headers.subscribe': self.headers_subscribe,
|
||||||
|
'blockchain.numblocks.subscribe': self.numblocks_subscribe,
|
||||||
|
'blockchain.transaction.broadcast': self.transaction_broadcast_1_0,
|
||||||
|
'blockchain.transaction.get': controller.transaction_get_1_0,
|
||||||
|
'server.add_peer': self.add_peer,
|
||||||
|
'server.banner': self.banner,
|
||||||
|
'server.features': self.server_features,
|
||||||
|
'server.peers.subscribe': self.peers_subscribe,
|
||||||
|
'server.version': self.server_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers.update(controller.electrumx_handlers)
|
||||||
|
|
||||||
|
if ptuple >= (1, 1):
|
||||||
|
# Remove deprecated methods
|
||||||
|
del handlers['blockchain.address.get_proof']
|
||||||
|
del handlers['blockchain.numblocks.subscribe']
|
||||||
|
del handlers['blockchain.utxo.get_address']
|
||||||
|
# Add new handlers
|
||||||
|
handlers.update({
|
||||||
|
'blockchain.scripthash.subscribe': self.scripthash_subscribe,
|
||||||
|
'blockchain.transaction.broadcast': self.transaction_broadcast,
|
||||||
|
'blockchain.transaction.get': controller.transaction_get,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.electrumx_handlers = handlers
|
||||||
|
|
||||||
def request_handler(self, method):
|
def request_handler(self, method):
|
||||||
'''Return the async handler for the given request method.'''
|
'''Return the async handler for the given request method.'''
|
||||||
handler = self.electrumx_handlers.get(method)
|
return self.electrumx_handlers.get(method)
|
||||||
if not handler:
|
|
||||||
handler = self.controller.electrumx_handlers.get(method)
|
|
||||||
return handler
|
|
||||||
|
|
||||||
|
|
||||||
class LocalRPC(SessionBase):
|
class LocalRPC(SessionBase):
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Server name and protocol versions
|
# Server name and protocol versions
|
||||||
|
|
||||||
VERSION = 'ElectrumX 1.0.17'
|
VERSION = 'ElectrumX 1.0.17'
|
||||||
PROTOCOL_MIN = '1.0'
|
PROTOCOL_MIN = '0.10'
|
||||||
PROTOCOL_MAX = '1.0'
|
PROTOCOL_MAX = '1.1'
|
||||||
|
|||||||
@ -83,3 +83,13 @@ def test_is_valid_hostname():
|
|||||||
len255 = ('a' * 62 + '.') * 4 + 'abc'
|
len255 = ('a' * 62 + '.') * 4 + 'abc'
|
||||||
assert is_valid_hostname(len255)
|
assert is_valid_hostname(len255)
|
||||||
assert not is_valid_hostname(len255 + 'd')
|
assert not is_valid_hostname(len255 + 'd')
|
||||||
|
|
||||||
|
|
||||||
|
def test_protocol_tuple():
|
||||||
|
assert util.protocol_tuple(None) == (0, )
|
||||||
|
assert util.protocol_tuple("foo") == (0, )
|
||||||
|
assert util.protocol_tuple(1) == (0, )
|
||||||
|
assert util.protocol_tuple("1") == (1, )
|
||||||
|
assert util.protocol_tuple("0.1") == (0, 1)
|
||||||
|
assert util.protocol_tuple("0.10") == (0, 10)
|
||||||
|
assert util.protocol_tuple("2.5.3") == (2, 5, 3)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user