Browse Source

Require Electrum protocol at least 1.4

patch-2
Neil Booth 6 years ago
parent
commit
f89cea536c
  1. 2
      .travis.yml
  2. 56
      electrumx/lib/coins.py
  3. 37
      electrumx/server/peers.py
  4. 130
      electrumx/server/session.py
  5. 24
      tests/lib/test_coins.py
  6. 7
      tests/test_blocks.py

2
.travis.yml

@ -36,7 +36,7 @@ install:
- pip install quark_hash
- pip install groestlcoin_hash
- pip install git+https://github.com/goacoincore/neoscrypt
- pip install git+https://github.com/motioncrypto/x16r_hash
- pip install x16r_hash
- pip install pycryptodomex
- pip install git+https://github.com/Electra-project/nist5_hash
# command to run tests

56
electrumx/lib/coins.py

@ -258,16 +258,6 @@ class Coin(object):
'''
return Decimal(value) / cls.VALUE_PER_COIN
@classmethod
def electrum_header(cls, header, height):
h = dict(zip(cls.HEADER_VALUES, cls.HEADER_UNPACK(header)))
# Add the height that is not present in the header itself
h['block_height'] = height
# Convert bytes to str
h['prev_block_hash'] = hash_to_hex_str(h['prev_block_hash'])
h['merkle_root'] = hash_to_hex_str(h['merkle_root'])
return h
@classmethod
def warn_old_client_on_tx_broadcast(cls, client_ver):
return False
@ -302,18 +292,6 @@ class EquihashMixin(object):
'timestamp', 'bits', 'nonce')
HEADER_UNPACK = struct.Struct('< I 32s 32s 32s I I 32s').unpack_from
@classmethod
def electrum_header(cls, header, height):
h = dict(zip(cls.HEADER_VALUES, cls.HEADER_UNPACK(header)))
# Add the height that is not present in the header itself
h['block_height'] = height
# Convert bytes to str
h['prev_block_hash'] = hash_to_hex_str(h['prev_block_hash'])
h['merkle_root'] = hash_to_hex_str(h['merkle_root'])
h['reserved'] = hash_to_hex_str(h['reserved'])
h['nonce'] = hash_to_hex_str(h['nonce'])
return h
@classmethod
def block_header(cls, block, height):
'''Return the block header bytes'''
@ -482,13 +460,6 @@ class BitcoinGold(EquihashMixin, BitcoinMixin, Coin):
else:
return double_sha256(header[:68] + header[100:112])
@classmethod
def electrum_header(cls, header, height):
h = super().electrum_header(header, height)
h['reserved'] = hash_to_hex_str(header[72:100])
h['solution'] = hash_to_hex_str(header[140:])
return h
class BitcoinGoldTestnet(BitcoinGold):
FORK_HEIGHT = 1
@ -1192,12 +1163,6 @@ class FairCoin(Coin):
else:
return Block(raw_block, cls.block_header(raw_block, height), [])
@classmethod
def electrum_header(cls, header, height):
h = super().electrum_header(header, height)
h['payload_hash'] = hash_to_hex_str(h['payload_hash'])
return h
class Zcash(EquihashMixin, Coin):
NAME = "Zcash"
@ -2072,14 +2037,6 @@ class Decred(Coin):
else:
return Block(raw_block, cls.block_header(raw_block, height), [])
@classmethod
def electrum_header(cls, header, height):
h = super().electrum_header(header, height)
h['stake_root'] = hash_to_hex_str(h['stake_root'])
h['final_state'] = hash_to_hex_str(h['final_state'])
h['extra_data'] = hash_to_hex_str(h['extra_data'])
return h
class DecredTestnet(Decred):
SHORTNAME = "tDCR"
@ -2167,13 +2124,6 @@ class Xuez(Coin):
else:
return xevan_hash.getPoWHash(header)
@classmethod
def electrum_header(cls, header, height):
h = super().electrum_header(header, height)
if h['version'] > 1:
h['nAccumulatorCheckpoint'] = hash_to_hex_str(header[80:])
return h
class Pac(Coin):
NAME = "PAC"
@ -2447,12 +2397,6 @@ class Minexcoin(EquihashMixin, Coin):
'eu.minexpool.nl s t'
]
@classmethod
def electrum_header(cls, header, height):
h = super().electrum_header(header, height)
h['solution'] = hash_to_hex_str(header[cls.HEADER_SIZE_NO_SOLUTION:])
return h
@classmethod
def block_header(cls, block, height):
'''Return the block header bytes'''

37
electrumx/server/peers.py

@ -22,7 +22,7 @@ from aiorpcx import (Connector, RPCSession, SOCKSProxy,
sleep, ignore_after, timeout_after)
from electrumx.lib.peer import Peer
from electrumx.lib.util import class_logger, protocol_tuple
from electrumx.lib.util import class_logger
PEER_GOOD, PEER_STALE, PEER_NEVER, PEER_BAD = range(4)
STALE_SECS = 3 * 3600
@ -362,10 +362,9 @@ class PeerManager(object):
server_version, protocol_version = result
peer.server_version = server_version
peer.features['server_version'] = server_version
ptuple = protocol_tuple(protocol_version)
async with TaskGroup() as g:
await g.spawn(self._send_headers_subscribe(session, peer, ptuple))
await g.spawn(self._send_headers_subscribe(session, peer))
await g.spawn(self._send_server_features(session, peer))
peers_task = await g.spawn(self._send_peers_subscribe
(session, peer))
@ -380,16 +379,13 @@ class PeerManager(object):
# We only care to wait for the response
await session.send_request('server.add_peer', [features])
async def _send_headers_subscribe(self, session, peer, ptuple):
async def _send_headers_subscribe(self, session, peer):
message = 'blockchain.headers.subscribe'
result = await session.send_request(message)
assert_good(message, result, dict)
our_height = self.db.db_height
if ptuple < (1, 3):
their_height = result.get('block_height')
else:
their_height = result.get('height')
their_height = result.get('height')
if not isinstance(their_height, int):
raise BadPeerError(f'invalid height {their_height}')
if abs(our_height - their_height) > 5:
@ -399,24 +395,13 @@ class PeerManager(object):
# Check prior header too in case of hard fork.
check_height = min(our_height, their_height)
raw_header = await self.db.raw_header(check_height)
if ptuple >= (1, 4):
ours = raw_header.hex()
message = 'blockchain.block.header'
theirs = await session.send_request(message, [check_height])
assert_good(message, theirs, str)
if ours != theirs:
raise BadPeerError(f'our header {ours} and '
f'theirs {theirs} differ')
else:
ours = self.env.coin.electrum_header(raw_header, check_height)
ours = ours.get('prev_block_hash')
message = 'blockchain.block.get_header'
theirs = await session.send_request(message, [check_height])
assert_good(message, theirs, dict)
theirs = theirs.get('prev_block_hash')
if ours != theirs:
raise BadPeerError(f'our header hash {ours} and '
f'theirs {theirs} differ')
ours = raw_header.hex()
message = 'blockchain.block.header'
theirs = await session.send_request(message, [check_height])
assert_good(message, theirs, str)
if ours != theirs:
raise BadPeerError(f'our header {ours} and '
f'theirs {theirs} differ')
async def _send_server_features(self, session, peer):
message = 'server.features'

130
electrumx/server/session.py

@ -326,19 +326,14 @@ class SessionManager(object):
])
return result
async def _electrum_and_raw_headers(self, height):
raw_header = await self.raw_header(height)
electrum_header = self.env.coin.electrum_header(raw_header, height)
return electrum_header, raw_header
async def _refresh_hsub_results(self, height):
'''Refresh the cached header subscription responses to be for height,
and record that as notified_height.
'''
# Paranoia: a reorg could race and leave db_height lower
height = min(height, self.db.db_height)
electrum, raw = await self._electrum_and_raw_headers(height)
self.hsub_results = (electrum, {'hex': raw.hex(), 'height': height})
raw = await self.raw_header(height)
self.hsub_results = {'hex': raw.hex(), 'height': height}
self.notified_height = height
# --- LocalRPC command handlers
@ -547,11 +542,6 @@ class SessionManager(object):
raise RPCError(BAD_REQUEST, f'height {height:,d} '
'out of range') from None
async def electrum_header(self, height):
'''Return the deserialized header at the given height.'''
electrum_header, _ = await self._electrum_and_raw_headers(height)
return electrum_header
async def broadcast_transaction(self, raw_tx):
hex_hash = await self.daemon.broadcast_transaction(raw_tx)
self.txs_sent += 1
@ -728,13 +718,12 @@ class SessionBase(RPCSession):
class ElectrumX(SessionBase):
'''A TCP server that handles incoming Electrum connections.'''
PROTOCOL_MIN = (1, 2)
PROTOCOL_MIN = (1, 4)
PROTOCOL_MAX = (1, 4, 1)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.subscribe_headers = False
self.subscribe_headers_raw = False
self.connection.max_response_size = self.env.max_send
self.hashX_subs = {}
self.sv_seen = False
@ -821,26 +810,13 @@ class ElectrumX(SessionBase):
async def subscribe_headers_result(self):
'''The result of a header subscription or notification.'''
return self.session_mgr.hsub_results[self.subscribe_headers_raw]
async def _headers_subscribe(self, raw):
'''Subscribe to get headers of new blocks.'''
self.bump_cost(0.25)
self.subscribe_headers_raw = assert_boolean(raw)
self.subscribe_headers = True
return await self.subscribe_headers_result()
return self.session_mgr.hsub_results
async def headers_subscribe(self):
'''Subscribe to get raw headers of new blocks.'''
return await self._headers_subscribe(True)
async def headers_subscribe_True(self, raw=True):
'''Subscribe to get headers of new blocks.'''
return await self._headers_subscribe(raw)
async def headers_subscribe_False(self, raw=False):
'''Subscribe to get headers of new blocks.'''
return await self._headers_subscribe(raw)
self.subscribe_headers = True
self.bump_cost(0.25)
return await self.subscribe_headers_result()
async def add_peer(self, features):
'''Add a peer (but only if the peer resolves to the source).'''
@ -903,40 +879,6 @@ class ElectrumX(SessionBase):
self.hashX_subs[hashX] = alias
return await self.address_status(hashX)
def address_to_hashX(self, address):
try:
return self.coin.address_to_hashX(address)
except Exception:
pass
raise RPCError(BAD_REQUEST, f'{address} is not a valid address')
async def address_get_balance(self, address):
'''Return the confirmed and unconfirmed balance of an address.'''
hashX = self.address_to_hashX(address)
return await self.get_balance(hashX)
async def address_get_history(self, address):
'''Return the confirmed and unconfirmed history of an address.'''
hashX = self.address_to_hashX(address)
return await self.confirmed_and_unconfirmed_history(hashX)
async def address_get_mempool(self, address):
'''Return the mempool transactions touching an address.'''
hashX = self.address_to_hashX(address)
return await self.unconfirmed_history(hashX)
async def address_listunspent(self, address):
'''Return the list of UTXOs of an address.'''
hashX = self.address_to_hashX(address)
return await self.hashX_listunspent(hashX)
async def address_subscribe(self, address):
'''Subscribe to an address.
address: the address to subscribe to'''
hashX = self.address_to_hashX(address)
return await self.hashX_subscribe(hashX, address)
async def get_balance(self, hashX):
utxos = await self.db.all_utxos(hashX)
confirmed = sum(utxo.value for utxo in utxos)
@ -1016,12 +958,6 @@ class ElectrumX(SessionBase):
result.update(await self._merkle_proof(cp_height, height))
return result
async def block_header_13(self, height):
'''Return a raw block header as a hexadecimal string.
height: the header's height'''
return await self.block_header(height)
async def block_headers(self, start_height, count, cp_height=0):
'''Return count concatenated block headers as hex for the main chain;
starting at start_height.
@ -1045,28 +981,6 @@ class ElectrumX(SessionBase):
self.bump_cost(cost)
return result
async def block_headers_12(self, start_height, count):
return await self.block_headers(start_height, count)
async def block_get_chunk(self, index):
'''Return a chunk of block headers as a hexadecimal string.
index: the chunk index'''
index = non_negative_integer(index)
size = self.coin.CHUNK_SIZE
start_height = index * size
headers, _ = await self.db.read_headers(start_height, size)
self.bump_cost(2016 / 50)
return headers.hex()
async def block_get_header(self, height):
'''The deserialized header at a given height.
height: the header's height'''
height = non_negative_integer(height)
self.bump_cost(0.25)
return await self.session_mgr.electrum_header(height)
def is_tor(self):
'''Try to detect if the connection is to a tor hidden service we are
running.'''
@ -1295,10 +1209,10 @@ class ElectrumX(SessionBase):
self.protocol_tuple = ptuple
handlers = {
'blockchain.block.get_chunk': self.block_get_chunk,
'blockchain.block.get_header': self.block_get_header,
'blockchain.block.headers': self.block_headers_12,
'blockchain.block.header': self.block_header,
'blockchain.block.headers': self.block_headers,
'blockchain.estimatefee': self.estimatefee,
'blockchain.headers.subscribe': self.headers_subscribe,
'blockchain.relayfee': self.relayfee,
'blockchain.scripthash.get_balance': self.scripthash_get_balance,
'blockchain.scripthash.get_history': self.scripthash_get_history,
@ -1308,6 +1222,7 @@ class ElectrumX(SessionBase):
'blockchain.transaction.broadcast': self.transaction_broadcast,
'blockchain.transaction.get': self.transaction_get,
'blockchain.transaction.get_merkle': self.transaction_merkle,
'blockchain.transaction.id_from_pos': self.transaction_id_from_pos,
'mempool.get_fee_histogram': self.mempool.compact_fee_histogram,
'server.add_peer': self.add_peer,
'server.banner': self.banner,
@ -1318,29 +1233,6 @@ class ElectrumX(SessionBase):
'server.version': self.server_version,
}
if ptuple >= (1, 4):
handlers.update({
'blockchain.block.header': self.block_header,
'blockchain.block.headers': self.block_headers,
'blockchain.headers.subscribe': self.headers_subscribe,
'blockchain.transaction.id_from_pos':
self.transaction_id_from_pos,
})
elif ptuple >= (1, 3):
handlers.update({
'blockchain.block.header': self.block_header_13,
'blockchain.headers.subscribe': self.headers_subscribe_True,
})
else:
handlers.update({
'blockchain.headers.subscribe': self.headers_subscribe_False,
'blockchain.address.get_balance': self.address_get_balance,
'blockchain.address.get_history': self.address_get_history,
'blockchain.address.get_mempool': self.address_get_mempool,
'blockchain.address.listunspent': self.address_listunspent,
'blockchain.address.subscribe': self.address_subscribe,
})
self.request_handlers = handlers

24
tests/lib/test_coins.py

@ -1,24 +0,0 @@
import electrumx.lib.coins as coins
def test_bitcoin_cash():
raw_header = bytes.fromhex(
"00000020df975c121dcbc18bbb7ddfd0419fc368b45db86b48c87e0"
"1000000000000000036ae3dd40a10a40d3050de13ca546a2f81589d"
"e2d2f317925a43a115437e2381f5bf535b94da0118ac8df8c5"
)
height = 540000
electrum_header = {
'block_height': 540000,
'version': 536870912,
'prev_block_hash':
'0000000000000000017ec8486bb85db468c39f41d0df7dbb8bc1cb1d125c97df',
'merkle_root':
'81237e4315a1435a9217f3d2e29d58812f6a54ca13de50300da4100ad43dae36',
'timestamp': 1532215285,
'bits': 402774676,
'nonce': 3321400748
}
assert coins.BitcoinCash.electrum_header(
raw_header, height) == electrum_header

7
tests/test_blocks.py

@ -61,13 +61,6 @@ def test_block(block_details):
raw_block = unhexlify(block_info['block'])
block = coin.block(raw_block, block_info['height'])
h = coin.electrum_header(block.header, block_info['height'])
assert block_info['merkleroot'] == h['merkle_root']
assert block_info['time'] == h['timestamp']
assert block_info['previousblockhash'] == h['prev_block_hash']
assert block_info['height'] == h['block_height']
assert block_info['nonce'] == h['nonce']
assert block_info['bits'] == pack_be_uint32(h['bits']).hex()
assert coin.header_hash(
block.header) == hex_str_to_hash(block_info['hash'])

Loading…
Cancel
Save