Browse Source

blockchain.py: generalise fork ids to get rid of conflicts

3.3.3.1
SomberNight 6 years ago
parent
commit
141ff99580
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 252
      electrum/blockchain.py
  2. 8
      electrum/gui/kivy/main_window.py
  3. 21
      electrum/gui/qt/network_dialog.py
  4. 65
      electrum/interface.py
  5. 38
      electrum/network.py
  6. 2
      electrum/storage.py
  7. 239
      electrum/tests/test_blockchain.py
  8. 20
      electrum/tests/test_network.py
  9. 1
      electrum/tests/test_wallet.py

252
electrum/blockchain.py

@ -79,26 +79,67 @@ def hash_raw_header(header: str) -> str:
return hash_encode(sha256d(bfh(header))) return hash_encode(sha256d(bfh(header)))
blockchains = {} # type: Dict[int, Blockchain] # key: blockhash hex at forkpoint
blockchains_lock = threading.Lock() # the chain at some key is the best chain that includes the given hash
blockchains = {} # type: Dict[str, Blockchain]
blockchains_lock = threading.RLock()
def read_blockchains(config: 'SimpleConfig') -> Dict[int, 'Blockchain']:
blockchains[0] = Blockchain(config, 0, None)
def read_blockchains(config: 'SimpleConfig'):
blockchains[constants.net.GENESIS] = Blockchain(config=config,
forkpoint=0,
parent=None,
forkpoint_hash=constants.net.GENESIS,
prev_hash=None)
fdir = os.path.join(util.get_headers_dir(config), 'forks') fdir = os.path.join(util.get_headers_dir(config), 'forks')
util.make_dir(fdir) util.make_dir(fdir)
l = filter(lambda x: x.startswith('fork_'), os.listdir(fdir)) # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash}
l = sorted(l, key = lambda x: int(x.split('_')[1])) l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir))
for filename in l: l = sorted(l, key=lambda x: int(x.split('_')[1])) # sort by forkpoint
forkpoint = int(filename.split('_')[2])
parent_id = int(filename.split('_')[1]) def delete_chain(filename, reason):
b = Blockchain(config, forkpoint, parent_id) util.print_error("[blockchain]", reason, filename)
h = b.read_header(b.forkpoint) os.unlink(os.path.join(fdir, filename))
if b.parent().can_connect(h, check_height=False):
blockchains[b.forkpoint] = b def instantiate_chain(filename):
__, forkpoint, prev_hash, first_hash = filename.split('_')
forkpoint = int(forkpoint)
prev_hash = (64-len(prev_hash)) * "0" + prev_hash # left-pad with zeroes
first_hash = (64-len(first_hash)) * "0" + first_hash
# forks below the max checkpoint are not allowed
if forkpoint <= constants.net.max_checkpoint():
delete_chain(filename, "deleting fork below max checkpoint")
return
# find parent (sorting by forkpoint guarantees it's already instantiated)
for parent in blockchains.values():
if parent.check_hash(forkpoint - 1, prev_hash):
break
else: else:
util.print_error("cannot connect", filename) delete_chain(filename, "cannot find parent for chain")
return blockchains return
b = Blockchain(config=config,
forkpoint=forkpoint,
parent=parent,
forkpoint_hash=first_hash,
prev_hash=prev_hash)
# consistency checks
h = b.read_header(b.forkpoint)
if first_hash != hash_header(h):
delete_chain(filename, "incorrect first hash for chain")
return
if not b.parent.can_connect(h, check_height=False):
delete_chain(filename, "cannot connect chain to parent")
return
chain_id = b.get_id()
assert first_hash == chain_id, (first_hash, chain_id)
blockchains[chain_id] = b
for filename in l:
instantiate_chain(filename)
def get_best_chain() -> 'Blockchain':
return blockchains[constants.net.GENESIS]
class Blockchain(util.PrintError): class Blockchain(util.PrintError):
@ -106,15 +147,20 @@ class Blockchain(util.PrintError):
Manages blockchain headers and their verification Manages blockchain headers and their verification
""" """
def __init__(self, config: SimpleConfig, forkpoint: int, parent_id: Optional[int]): def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'],
forkpoint_hash: str, prev_hash: Optional[str]):
assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash
assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash
# assert (parent is None) == (forkpoint == 0)
if 0 < forkpoint <= constants.net.max_checkpoint():
raise Exception(f"cannot fork below max checkpoint. forkpoint: {forkpoint}")
self.config = config self.config = config
self.forkpoint = forkpoint self.forkpoint = forkpoint # height of first header
self.checkpoints = constants.net.CHECKPOINTS self.parent = parent
self.parent_id = parent_id self._forkpoint_hash = forkpoint_hash # blockhash at forkpoint. "first hash"
assert parent_id != forkpoint self._prev_hash = prev_hash # blockhash immediately before forkpoint
self.lock = threading.RLock() self.lock = threading.RLock()
with self.lock: self.update_size()
self.update_size()
def with_lock(func): def with_lock(func):
def func_wrapper(self, *args, **kwargs): def func_wrapper(self, *args, **kwargs):
@ -122,12 +168,13 @@ class Blockchain(util.PrintError):
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return func_wrapper return func_wrapper
def parent(self) -> 'Blockchain': @property
return blockchains[self.parent_id] def checkpoints(self):
return constants.net.CHECKPOINTS
def get_max_child(self) -> Optional[int]: def get_max_child(self) -> Optional[int]:
with blockchains_lock: chains = list(blockchains.values()) with blockchains_lock: chains = list(blockchains.values())
children = list(filter(lambda y: y.parent_id==self.forkpoint, chains)) children = list(filter(lambda y: y.parent==self, chains))
return max([x.forkpoint for x in children]) if children else None return max([x.forkpoint for x in children]) if children else None
def get_max_forkpoint(self) -> int: def get_max_forkpoint(self) -> int:
@ -137,11 +184,12 @@ class Blockchain(util.PrintError):
mc = self.get_max_child() mc = self.get_max_child()
return mc if mc is not None else self.forkpoint return mc if mc is not None else self.forkpoint
@with_lock
def get_branch_size(self) -> int: def get_branch_size(self) -> int:
return self.height() - self.get_max_forkpoint() + 1 return self.height() - self.get_max_forkpoint() + 1
def get_name(self) -> str: def get_name(self) -> str:
return self.get_hash(self.get_max_forkpoint()).lstrip('00')[0:10] return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10]
def check_header(self, header: dict) -> bool: def check_header(self, header: dict) -> bool:
header_hash = hash_header(header) header_hash = hash_header(header)
@ -159,24 +207,38 @@ class Blockchain(util.PrintError):
return False return False
def fork(parent, header: dict) -> 'Blockchain': def fork(parent, header: dict) -> 'Blockchain':
if not parent.can_connect(header, check_height=False):
raise Exception("forking header does not connect to parent chain")
forkpoint = header.get('block_height') forkpoint = header.get('block_height')
self = Blockchain(parent.config, forkpoint, parent.forkpoint) self = Blockchain(config=parent.config,
forkpoint=forkpoint,
parent=parent,
forkpoint_hash=hash_header(header),
prev_hash=parent.get_hash(forkpoint-1))
open(self.path(), 'w+').close() open(self.path(), 'w+').close()
self.save_header(header) self.save_header(header)
# put into global dict
chain_id = self.get_id()
with blockchains_lock:
assert chain_id not in blockchains, (chain_id, list(blockchains))
blockchains[chain_id] = self
return self return self
@with_lock
def height(self) -> int: def height(self) -> int:
return self.forkpoint + self.size() - 1 return self.forkpoint + self.size() - 1
@with_lock
def size(self) -> int: def size(self) -> int:
with self.lock: return self._size
return self._size
@with_lock
def update_size(self) -> None: def update_size(self) -> None:
p = self.path() p = self.path()
self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0 self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0
def verify_header(self, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None: @classmethod
def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None:
_hash = hash_header(header) _hash = hash_header(header)
if expected_header_hash and expected_header_hash != _hash: if expected_header_hash and expected_header_hash != _hash:
raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash)) raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash))
@ -184,7 +246,7 @@ class Blockchain(util.PrintError):
raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
if constants.net.TESTNET: if constants.net.TESTNET:
return return
bits = self.target_to_bits(target) bits = cls.target_to_bits(target)
if bits != header.get('bits'): if bits != header.get('bits'):
raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits'))) raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits')))
if int('0x' + _hash, 16) > target: if int('0x' + _hash, 16) > target:
@ -206,21 +268,26 @@ class Blockchain(util.PrintError):
self.verify_header(header, prev_hash, target, expected_header_hash) self.verify_header(header, prev_hash, target, expected_header_hash)
prev_hash = hash_header(header) prev_hash = hash_header(header)
@with_lock
def path(self): def path(self):
d = util.get_headers_dir(self.config) d = util.get_headers_dir(self.config)
if self.parent_id is None: if self.parent is None:
filename = 'blockchain_headers' filename = 'blockchain_headers'
else: else:
basename = 'fork_%d_%d' % (self.parent_id, self.forkpoint) assert self.forkpoint > 0, self.forkpoint
prev_hash = self._prev_hash.lstrip('0')
first_hash = self._forkpoint_hash.lstrip('0')
basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}'
filename = os.path.join('forks', basename) filename = os.path.join('forks', basename)
return os.path.join(d, filename) return os.path.join(d, filename)
@with_lock @with_lock
def save_chunk(self, index: int, chunk: bytes): def save_chunk(self, index: int, chunk: bytes):
assert index >= 0, index
chunk_within_checkpoint_region = index < len(self.checkpoints) chunk_within_checkpoint_region = index < len(self.checkpoints)
# chunks in checkpoint region are the responsibility of the 'main chain' # chunks in checkpoint region are the responsibility of the 'main chain'
if chunk_within_checkpoint_region and self.parent_id is not None: if chunk_within_checkpoint_region and self.parent is not None:
main_chain = blockchains[0] main_chain = get_best_chain()
main_chain.save_chunk(index, chunk) main_chain.save_chunk(index, chunk)
return return
@ -235,18 +302,36 @@ class Blockchain(util.PrintError):
self.write(chunk, delta_bytes, truncate) self.write(chunk, delta_bytes, truncate)
self.swap_with_parent() self.swap_with_parent()
@with_lock
def swap_with_parent(self) -> None: def swap_with_parent(self) -> None:
if self.parent_id is None: parent_lock = self.parent.lock if self.parent is not None else threading.Lock()
return with parent_lock, self.lock, blockchains_lock: # this order should not deadlock
parent_branch_size = self.parent().height() - self.forkpoint + 1 # do the swap; possibly multiple ones
if parent_branch_size >= self.size(): cnt = 0
return while self._swap_with_parent():
self.print_error("swap", self.forkpoint, self.parent_id) cnt += 1
parent_id = self.parent_id if cnt > len(blockchains): # make sure we are making progress
forkpoint = self.forkpoint raise Exception(f'swapping fork with parent too many times: {cnt}')
parent = self.parent()
def _swap_with_parent(self) -> bool:
"""Check if this chain became stronger than its parent, and swap
the underlying files if so. The Blockchain instances will keep
'containing' the same headers, but their ids change and so
they will be stored in different files."""
if self.parent is None:
return False
parent_branch_size = self.parent.height() - self.forkpoint + 1
if parent_branch_size >= self.size(): # FIXME most work, not length
return False
self.print_error("swap", self.forkpoint, self.parent.forkpoint)
forkpoint = self.forkpoint # type: Optional[int]
parent = self.parent # type: Optional[Blockchain]
child_old_id = self.get_id()
parent_old_id = parent.get_id()
# swap files
# child takes parent's name
# parent's new name will be something new (not child's old name)
self.assert_headers_file_available(self.path()) self.assert_headers_file_available(self.path())
child_old_name = self.path()
with open(self.path(), 'rb') as f: with open(self.path(), 'rb') as f:
my_data = f.read() my_data = f.read()
self.assert_headers_file_available(parent.path()) self.assert_headers_file_available(parent.path())
@ -255,24 +340,28 @@ class Blockchain(util.PrintError):
parent_data = f.read(parent_branch_size*HEADER_SIZE) parent_data = f.read(parent_branch_size*HEADER_SIZE)
self.write(parent_data, 0) self.write(parent_data, 0)
parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE) parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE)
# store file path
with blockchains_lock: chains = list(blockchains.values())
for b in chains:
b.old_path = b.path()
# swap parameters # swap parameters
self.parent_id = parent.parent_id; parent.parent_id = parent_id self.parent, parent.parent = parent.parent, self # type: Optional[Blockchain], Optional[Blockchain]
self.forkpoint = parent.forkpoint; parent.forkpoint = forkpoint self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint
self._size = parent._size; parent._size = parent_branch_size self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE]))
# move files self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash
for b in chains: # parent's new name
if b in [self, parent]: continue try:
if b.old_path != b.path(): os.rename(child_old_name, parent.path())
self.print_error("renaming", b.old_path, b.path()) except OSError:
os.rename(b.old_path, b.path()) os.remove(parent.path())
os.rename(child_old_name, parent.path())
self.update_size()
parent.update_size()
# update pointers # update pointers
with blockchains_lock: blockchains.pop(child_old_id, None)
blockchains[self.forkpoint] = self blockchains.pop(parent_old_id, None)
blockchains[parent.forkpoint] = parent blockchains[self.get_id()] = self
blockchains[parent.get_id()] = parent
return True
def get_id(self) -> str:
return self._forkpoint_hash
def assert_headers_file_available(self, path): def assert_headers_file_available(self, path):
if os.path.exists(path): if os.path.exists(path):
@ -282,19 +371,19 @@ class Blockchain(util.PrintError):
else: else:
raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path)) raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path))
@with_lock
def write(self, data: bytes, offset: int, truncate: bool=True) -> None: def write(self, data: bytes, offset: int, truncate: bool=True) -> None:
filename = self.path() filename = self.path()
with self.lock: self.assert_headers_file_available(filename)
self.assert_headers_file_available(filename) with open(filename, 'rb+') as f:
with open(filename, 'rb+') as f: if truncate and offset != self._size * HEADER_SIZE:
if truncate and offset != self._size * HEADER_SIZE:
f.seek(offset)
f.truncate()
f.seek(offset) f.seek(offset)
f.write(data) f.truncate()
f.flush() f.seek(offset)
os.fsync(f.fileno()) f.write(data)
self.update_size() f.flush()
os.fsync(f.fileno())
self.update_size()
@with_lock @with_lock
def save_header(self, header: dict) -> None: def save_header(self, header: dict) -> None:
@ -306,12 +395,12 @@ class Blockchain(util.PrintError):
self.write(data, delta*HEADER_SIZE) self.write(data, delta*HEADER_SIZE)
self.swap_with_parent() self.swap_with_parent()
@with_lock
def read_header(self, height: int) -> Optional[dict]: def read_header(self, height: int) -> Optional[dict]:
assert self.parent_id != self.forkpoint
if height < 0: if height < 0:
return return
if height < self.forkpoint: if height < self.forkpoint:
return self.parent().read_header(height) return self.parent.read_header(height)
if height > self.height(): if height > self.height():
return return
delta = height - self.forkpoint delta = height - self.forkpoint
@ -371,16 +460,18 @@ class Blockchain(util.PrintError):
new_target = self.bits_to_target(self.target_to_bits(new_target)) new_target = self.bits_to_target(self.target_to_bits(new_target))
return new_target return new_target
def bits_to_target(self, bits: int) -> int: @classmethod
def bits_to_target(cls, bits: int) -> int:
bitsN = (bits >> 24) & 0xff bitsN = (bits >> 24) & 0xff
if not (bitsN >= 0x03 and bitsN <= 0x1d): if not (0x03 <= bitsN <= 0x1d):
raise Exception("First part of bits should be in [0x03, 0x1d]") raise Exception("First part of bits should be in [0x03, 0x1d]")
bitsBase = bits & 0xffffff bitsBase = bits & 0xffffff
if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff): if not (0x8000 <= bitsBase <= 0x7fffff):
raise Exception("Second part of bits should be in [0x8000, 0x7fffff]") raise Exception("Second part of bits should be in [0x8000, 0x7fffff]")
return bitsBase << (8 * (bitsN-3)) return bitsBase << (8 * (bitsN-3))
def target_to_bits(self, target: int) -> int: @classmethod
def target_to_bits(cls, target: int) -> int:
c = ("%064x" % target)[2:] c = ("%064x" % target)[2:]
while c[:2] == '00' and len(c) > 6: while c[:2] == '00' and len(c) > 6:
c = c[2:] c = c[2:]
@ -416,6 +507,7 @@ class Blockchain(util.PrintError):
return True return True
def connect_chunk(self, idx: int, hexdata: str) -> bool: def connect_chunk(self, idx: int, hexdata: str) -> bool:
assert idx >= 0, idx
try: try:
data = bfh(hexdata) data = bfh(hexdata)
self.verify_chunk(idx, data) self.verify_chunk(idx, data)
@ -423,7 +515,7 @@ class Blockchain(util.PrintError):
self.save_chunk(idx, data) self.save_chunk(idx, data)
return True return True
except BaseException as e: except BaseException as e:
self.print_error('verify_chunk %d failed'%idx, str(e)) self.print_error(f'verify_chunk idx {idx} failed: {repr(e)}')
return False return False
def get_checkpoints(self): def get_checkpoints(self):

8
electrum/gui/kivy/main_window.py

@ -126,10 +126,12 @@ class ElectrumWindow(App):
chains = self.network.get_blockchains() chains = self.network.get_blockchains()
def cb(name): def cb(name):
with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items()) with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items())
for index, b in blockchain_items: for chain_id, b in blockchain_items:
if name == b.get_name(): if name == b.get_name():
self.network.run_from_another_thread(self.network.follow_chain_given_id(index)) self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
names = [blockchain.blockchains[b].get_name() for b in chains] chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains]
chain_objects = filter(lambda b: b is not None, chain_objects)
names = [b.get_name() for b in chain_objects]
if len(names) > 1: if len(names) > 1:
cur_chain = self.network.blockchain().get_name() cur_chain = self.network.blockchain().get_name()
ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open() ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()

21
electrum/gui/qt/network_dialog.py

@ -82,8 +82,8 @@ class NodesListWidget(QTreeWidget):
server = item.data(1, Qt.UserRole) server = item.data(1, Qt.UserRole)
menu.addAction(_("Use as server"), lambda: self.parent.follow_server(server)) menu.addAction(_("Use as server"), lambda: self.parent.follow_server(server))
else: else:
index = item.data(1, Qt.UserRole) chain_id = item.data(1, Qt.UserRole)
menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(index)) menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(chain_id))
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def keyPressEvent(self, event): def keyPressEvent(self, event):
@ -103,22 +103,23 @@ class NodesListWidget(QTreeWidget):
self.addChild = self.addTopLevelItem self.addChild = self.addTopLevelItem
chains = network.get_blockchains() chains = network.get_blockchains()
n_chains = len(chains) n_chains = len(chains)
for k, items in chains.items(): for chain_id, interfaces in chains.items():
b = blockchain.blockchains[k] b = blockchain.blockchains.get(chain_id)
if b is None: continue
name = b.get_name() name = b.get_name()
if n_chains >1: if n_chains > 1:
x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()]) x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()])
x.setData(0, Qt.UserRole, 1) x.setData(0, Qt.UserRole, 1)
x.setData(1, Qt.UserRole, b.forkpoint) x.setData(1, Qt.UserRole, b.get_id())
else: else:
x = self x = self
for i in items: for i in interfaces:
star = ' *' if i == network.interface else '' star = ' *' if i == network.interface else ''
item = QTreeWidgetItem([i.host + star, '%d'%i.tip]) item = QTreeWidgetItem([i.host + star, '%d'%i.tip])
item.setData(0, Qt.UserRole, 0) item.setData(0, Qt.UserRole, 0)
item.setData(1, Qt.UserRole, i.server) item.setData(1, Qt.UserRole, i.server)
x.addChild(item) x.addChild(item)
if n_chains>1: if n_chains > 1:
self.addTopLevelItem(x) self.addTopLevelItem(x)
x.setExpanded(True) x.setExpanded(True)
@ -410,8 +411,8 @@ class NetworkChoiceLayout(object):
self.set_protocol(p) self.set_protocol(p)
self.set_server() self.set_server()
def follow_branch(self, index): def follow_branch(self, chain_id):
self.network.run_from_another_thread(self.network.follow_chain_given_id(index)) self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
self.update() self.update()
def follow_server(self, server): def follow_server(self, server):

65
electrum/interface.py

@ -28,7 +28,7 @@ import ssl
import sys import sys
import traceback import traceback
import asyncio import asyncio
from typing import Tuple, Union, List, TYPE_CHECKING from typing import Tuple, Union, List, TYPE_CHECKING, Optional
from collections import defaultdict from collections import defaultdict
import aiorpcx import aiorpcx
@ -140,14 +140,14 @@ def serialize_server(host: str, port: Union[str, int], protocol: str) -> str:
class Interface(PrintError): class Interface(PrintError):
verbosity_filter = 'i' verbosity_filter = 'i'
def __init__(self, network: 'Network', server: str, config_path, proxy: dict): def __init__(self, network: 'Network', server: str, proxy: Optional[dict]):
self.ready = asyncio.Future() self.ready = asyncio.Future()
self.got_disconnected = asyncio.Future() self.got_disconnected = asyncio.Future()
self.server = server self.server = server
self.host, self.port, self.protocol = deserialize_server(self.server) self.host, self.port, self.protocol = deserialize_server(self.server)
self.port = int(self.port) self.port = int(self.port)
self.config_path = config_path assert network.config.path
self.cert_path = os.path.join(self.config_path, 'certs', self.host) self.cert_path = os.path.join(network.config.path, 'certs', self.host)
self.blockchain = None self.blockchain = None
self._requested_chunks = set() self._requested_chunks = set()
self.network = network self.network = network
@ -281,7 +281,7 @@ class Interface(PrintError):
assert self.tip_header assert self.tip_header
chain = blockchain.check_header(self.tip_header) chain = blockchain.check_header(self.tip_header)
if not chain: if not chain:
self.blockchain = blockchain.blockchains[0] self.blockchain = blockchain.get_best_chain()
else: else:
self.blockchain = chain self.blockchain = chain
assert self.blockchain is not None assert self.blockchain is not None
@ -502,7 +502,7 @@ class Interface(PrintError):
# bad_header connects to good_header; bad_header itself is NOT in self.blockchain. # bad_header connects to good_header; bad_header itself is NOT in self.blockchain.
bh = self.blockchain.height() bh = self.blockchain.height()
assert bh >= good assert bh >= good, (bh, good)
if bh == good: if bh == good:
height = good + 1 height = good + 1
self.print_error("catching up from {}".format(height)) self.print_error("catching up from {}".format(height))
@ -510,53 +510,12 @@ class Interface(PrintError):
# this is a new fork we don't yet have # this is a new fork we don't yet have
height = bad + 1 height = bad + 1
branch = blockchain.blockchains.get(bad) self.print_error(f"new fork at bad height {bad}")
if branch is not None: forkfun = self.blockchain.fork if 'mock' not in bad_header else bad_header['mock']['fork']
# Conflict!! As our fork handling is not completely general, b = forkfun(bad_header) # type: Blockchain
# we need to delete another fork to save this one. self.blockchain = b
# Note: This could be a potential DOS vector against Electrum. assert b.forkpoint == bad
# However, mining blocks that satisfy the difficulty requirements return 'fork', height
# is assumed to be expensive; especially as forks below the max
# checkpoint are ignored.
self.print_error("new fork at bad height {}. conflict!!".format(bad))
assert self.blockchain != branch
ismocking = type(branch) is dict
if ismocking:
self.print_error("TODO replace blockchain")
return 'fork_conflict', height
self.print_error('forkpoint conflicts with existing fork', branch.path())
self._raise_if_fork_conflicts_with_default_server(branch)
await self._disconnect_from_interfaces_on_conflicting_blockchain(branch)
branch.write(b'', 0)
branch.save_header(bad_header)
self.blockchain = branch
return 'fork_conflict', height
else:
# No conflict. Just save the new fork.
self.print_error("new fork at bad height {}. NO conflict.".format(bad))
forkfun = self.blockchain.fork if 'mock' not in bad_header else bad_header['mock']['fork']
b = forkfun(bad_header)
with blockchain.blockchains_lock:
assert bad not in blockchain.blockchains, (bad, list(blockchain.blockchains))
blockchain.blockchains[bad] = b
self.blockchain = b
assert b.forkpoint == bad
return 'fork_noconflict', height
def _raise_if_fork_conflicts_with_default_server(self, chain_to_delete: Blockchain) -> None:
main_interface = self.network.interface
if not main_interface: return
if main_interface == self: return
chain_of_default_server = main_interface.blockchain
if not chain_of_default_server: return
if chain_to_delete == chain_of_default_server:
raise GracefulDisconnect('refusing to overwrite blockchain of default server')
async def _disconnect_from_interfaces_on_conflicting_blockchain(self, chain: Blockchain) -> None:
ifaces = await self.network.disconnect_from_interfaces_on_given_blockchain(chain)
if not ifaces: return
servers = [interface.server for interface in ifaces]
self.print_error("forcing disconnect of other interfaces: {}".format(servers))
async def _search_headers_backwards(self, height, header): async def _search_headers_backwards(self, height, header):
async def iterate(): async def iterate():

38
electrum/network.py

@ -177,10 +177,10 @@ class Network(PrintError):
if config is None: if config is None:
config = {} # Do not use mutables as default values! config = {} # Do not use mutables as default values!
self.config = SimpleConfig(config) if isinstance(config, dict) else config # type: SimpleConfig self.config = SimpleConfig(config) if isinstance(config, dict) else config # type: SimpleConfig
blockchain.blockchains = blockchain.read_blockchains(self.config) blockchain.read_blockchains(self.config)
self.print_error("blockchains", list(blockchain.blockchains)) self.print_error("blockchains", list(map(lambda b: b.forkpoint, blockchain.blockchains.values())))
self._blockchain_preferred_block = self.config.get('blockchain_preferred_block', None) # type: Optional[Dict] self._blockchain_preferred_block = self.config.get('blockchain_preferred_block', None) # type: Optional[Dict]
self._blockchain_index = 0 self._blockchain = blockchain.get_best_chain()
# Server for addresses and transactions # Server for addresses and transactions
self.default_server = self.config.get('server', None) self.default_server = self.config.get('server', None)
# Sanitize default server # Sanitize default server
@ -559,17 +559,24 @@ class Network(PrintError):
filtered = list(filter(lambda iface: iface.blockchain.check_hash(pref_height, pref_hash), filtered = list(filter(lambda iface: iface.blockchain.check_hash(pref_height, pref_hash),
interfaces)) interfaces))
if filtered: if filtered:
self.print_error("switching to preferred fork")
chosen_iface = random.choice(filtered) chosen_iface = random.choice(filtered)
await self.switch_to_interface(chosen_iface.server) await self.switch_to_interface(chosen_iface.server)
return return
# try to switch to longest chain else:
if self.blockchain().parent_id is None: self.print_error("tried to switch to preferred fork but no interfaces are on it")
return # already on longest chain # try to switch to best chain
filtered = list(filter(lambda iface: iface.blockchain.parent_id is None, if self.blockchain().parent is None:
return # already on best chain
filtered = list(filter(lambda iface: iface.blockchain.parent is None,
interfaces)) interfaces))
if filtered: if filtered:
self.print_error("switching to best chain")
chosen_iface = random.choice(filtered) chosen_iface = random.choice(filtered)
await self.switch_to_interface(chosen_iface.server) await self.switch_to_interface(chosen_iface.server)
else:
# FIXME switch to best available?
self.print_error("tried to switch to best chain but no interfaces are on it")
async def switch_to_interface(self, server: str): async def switch_to_interface(self, server: str):
"""Switch to server as our main interface. If no connection exists, """Switch to server as our main interface. If no connection exists,
@ -637,7 +644,7 @@ class Network(PrintError):
@ignore_exceptions # do not kill main_taskgroup @ignore_exceptions # do not kill main_taskgroup
@log_exceptions @log_exceptions
async def _run_new_interface(self, server): async def _run_new_interface(self, server):
interface = Interface(self, server, self.config.path, self.proxy) interface = Interface(self, server, self.proxy)
timeout = 10 if not self.proxy else 20 timeout = 10 if not self.proxy else 20
try: try:
await asyncio.wait_for(interface.ready, timeout) await asyncio.wait_for(interface.ready, timeout)
@ -661,7 +668,7 @@ class Network(PrintError):
self.trigger_callback('network_updated') self.trigger_callback('network_updated')
async def _init_headers_file(self): async def _init_headers_file(self):
b = blockchain.blockchains[0] b = blockchain.get_best_chain()
filename = b.path() filename = b.path()
length = HEADER_SIZE * len(constants.net.CHECKPOINTS) * 2016 length = HEADER_SIZE * len(constants.net.CHECKPOINTS) * 2016
if not os.path.exists(filename) or os.path.getsize(filename) < length: if not os.path.exists(filename) or os.path.getsize(filename) < length:
@ -739,8 +746,8 @@ class Network(PrintError):
def blockchain(self) -> Blockchain: def blockchain(self) -> Blockchain:
interface = self.interface interface = self.interface
if interface and interface.blockchain is not None: if interface and interface.blockchain is not None:
self._blockchain_index = interface.blockchain.forkpoint self._blockchain = interface.blockchain
return blockchain.blockchains[self._blockchain_index] return self._blockchain
def get_blockchains(self): def get_blockchains(self):
out = {} # blockchain_id -> list(interfaces) out = {} # blockchain_id -> list(interfaces)
@ -752,13 +759,6 @@ class Network(PrintError):
out[chain_id] = r out[chain_id] = r
return out return out
async def disconnect_from_interfaces_on_given_blockchain(self, chain: Blockchain) -> Sequence[Interface]:
chain_id = chain.forkpoint
ifaces = self.get_blockchains().get(chain_id) or []
for interface in ifaces:
await self.connection_down(interface.server)
return ifaces
def _set_preferred_chain(self, chain: Blockchain): def _set_preferred_chain(self, chain: Blockchain):
height = chain.get_max_forkpoint() height = chain.get_max_forkpoint()
header_hash = chain.get_hash(height) header_hash = chain.get_hash(height)
@ -768,7 +768,7 @@ class Network(PrintError):
} }
self.config.set_key('blockchain_preferred_block', self._blockchain_preferred_block) self.config.set_key('blockchain_preferred_block', self._blockchain_preferred_block)
async def follow_chain_given_id(self, chain_id: int) -> None: async def follow_chain_given_id(self, chain_id: str) -> None:
bc = blockchain.blockchains.get(chain_id) bc = blockchain.blockchains.get(chain_id)
if not bc: if not bc:
raise Exception('blockchain {} not found'.format(chain_id)) raise Exception('blockchain {} not found'.format(chain_id))

2
electrum/storage.py

@ -125,7 +125,7 @@ class JsonDB(PrintError):
# perform atomic write on POSIX systems # perform atomic write on POSIX systems
try: try:
os.rename(temp_path, self.path) os.rename(temp_path, self.path)
except: except OSError:
os.remove(self.path) os.remove(self.path)
os.rename(temp_path, self.path) os.rename(temp_path, self.path)
os.chmod(self.path, mode) os.chmod(self.path, mode)

239
electrum/tests/test_blockchain.py

@ -0,0 +1,239 @@
import shutil
import tempfile
import os
from electrum import constants, blockchain
from electrum.simple_config import SimpleConfig
from electrum.blockchain import Blockchain, deserialize_header, hash_header
from electrum.util import bh2u, bfh, make_dir
from . import SequentialTestCase
class TestBlockchain(SequentialTestCase):
HEADERS = {
'A': deserialize_header(bfh("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f2002000000"), 0),
'B': deserialize_header(bfh("0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f186c8dfd970a4545f79916bc1d75c9d00432f57c89209bf3bb115b7612848f509c25f45bffff7f2000000000"), 1),
'C': deserialize_header(bfh("00000020686bdfc6a3db73d5d93e8c9663a720a26ecb1ef20eb05af11b36cdbc57c19f7ebf2cbf153013a1c54abaf70e95198fcef2f3059cc6b4d0f7e876808e7d24d11cc825f45bffff7f2000000000"), 2),
'D': deserialize_header(bfh("00000020122baa14f3ef54985ae546d1611559e3f487bd2a0f46e8dbb52fbacc9e237972e71019d7feecd9b8596eca9a67032c5f4641b23b5d731dc393e37de7f9c2f299e725f45bffff7f2000000000"), 3),
'E': deserialize_header(bfh("00000020f8016f7ef3a17d557afe05d4ea7ab6bde1b2247b7643896c1b63d43a1598b747a3586da94c71753f27c075f57f44faf913c31177a0957bbda42e7699e3a2141aed25f45bffff7f2001000000"), 4),
'F': deserialize_header(bfh("000000201d589c6643c1d121d73b0573e5ee58ab575b8fdf16d507e7e915c5fbfbbfd05e7aee1d692d1615c3bdf52c291032144ce9e3b258a473c17c745047f3431ff8e2ee25f45bffff7f2000000000"), 5),
'O': deserialize_header(bfh("00000020b833ed46eea01d4c980f59feee44a66aa1162748b6801029565d1466790c405c3a141ce635cbb1cd2b3a4fcdd0a3380517845ba41736c82a79cab535d31128066526f45bffff7f2001000000"), 6),
'P': deserialize_header(bfh("00000020abe8e119d1877c9dc0dc502d1a253fb9a67967c57732d2f71ee0280e8381ff0a9690c2fe7c1a4450c74dc908fe94dd96c3b0637d51475e9e06a78e944a0c7fe28126f45bffff7f2000000000"), 7),
'Q': deserialize_header(bfh("000000202ce41d94eb70e1518bc1f72523f84a903f9705d967481e324876e1f8cf4d3452148be228a4c3f2061bafe7efdfc4a8d5a94759464b9b5c619994d45dfcaf49e1a126f45bffff7f2000000000"), 8),
'R': deserialize_header(bfh("00000020552755b6c59f3d51e361d16281842a4e166007799665b5daed86a063dd89857415681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a462555822552221a626f45bffff7f2000000000"), 9),
'S': deserialize_header(bfh("00000020a13a491cbefc93cd1bb1938f19957e22a134faf14c7dee951c45533e2c750f239dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548fab26f45bffff7f2000000000"), 10),
'T': deserialize_header(bfh("00000020dbf3a9b55dfefbaf8b6e43a89cf833fa2e208bbc0c1c5d76c0d71b9e4a65337803b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe064b026f45bffff7f2002000000"), 11),
'U': deserialize_header(bfh("000000203d0932b3b0c78eccb39a595a28ae4a7c966388648d7783fd1305ec8d40d4fe5fd67cb902a7d807cee7676cb543feec3e053aa824d5dfb528d5b94f9760313d9db726f45bffff7f2001000000"), 12),
'G': deserialize_header(bfh("00000020b833ed46eea01d4c980f59feee44a66aa1162748b6801029565d1466790c405c3a141ce635cbb1cd2b3a4fcdd0a3380517845ba41736c82a79cab535d31128066928f45bffff7f2001000000"), 6),
'H': deserialize_header(bfh("00000020e19e687f6e7f83ca394c114144dbbbc4f3f9c9450f66331a125413702a2e1a719690c2fe7c1a4450c74dc908fe94dd96c3b0637d51475e9e06a78e944a0c7fe26a28f45bffff7f2002000000"), 7),
'I': deserialize_header(bfh("0000002009dcb3b158293c89d7cf7ceeb513add122ebc3880a850f47afbb2747f5e48c54148be228a4c3f2061bafe7efdfc4a8d5a94759464b9b5c619994d45dfcaf49e16a28f45bffff7f2000000000"), 8),
'J': deserialize_header(bfh("000000206a65f3bdd3374a5a6c4538008ba0b0a560b8566291f9ef4280ab877627a1742815681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a462555822552221c928f45bffff7f2000000000"), 9),
'K': deserialize_header(bfh("00000020bb3b421653548991998f96f8ba486b652fdb07ca16e9cee30ece033547cd1a6e9dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548fca28f45bffff7f2000000000"), 10),
'L': deserialize_header(bfh("00000020c391d74d37c24a130f4bf4737932bdf9e206dd4fad22860ec5408978eb55d46303b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe064ca28f45bffff7f2000000000"), 11),
'M': deserialize_header(bfh("000000206a65f3bdd3374a5a6c4538008ba0b0a560b8566291f9ef4280ab877627a1742815681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a4625558225522214229f45bffff7f2000000000"), 9),
'N': deserialize_header(bfh("00000020383dab38b57f98aa9b4f0d5ff868bc674b4828d76766bf048296f4c45fff680a9dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548f4329f45bffff7f2003000000"), 10),
'X': deserialize_header(bfh("0000002067f1857f54b7fef732cb4940f7d1b339472b3514660711a820330fd09d8fba6b03b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe0649b29f45bffff7f2002000000"), 11),
'Y': deserialize_header(bfh("00000020db33c9768a9e5f7c37d0f09aad88d48165946c87d08f7d63793f07b5c08c527fd67cb902a7d807cee7676cb543feec3e053aa824d5dfb528d5b94f9760313d9d9b29f45bffff7f2000000000"), 12),
'Z': deserialize_header(bfh("0000002047822b67940e337fda38be6f13390b3596e4dea2549250256879722073824e7f0f2596c29203f8a0f71ae94193092dc8f113be3dbee4579f1e649fa3d6dcc38c622ef45bffff7f2003000000"), 13),
}
# tree of headers:
# - M <- N <- X <- Y <- Z
# /
# - G <- H <- I <- J <- K <- L
# /
# A <- B <- C <- D <- E <- F <- O <- P <- Q <- R <- S <- T <- U
@classmethod
def setUpClass(cls):
super().setUpClass()
constants.set_regtest()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
constants.set_mainnet()
def setUp(self):
super().setUp()
self.data_dir = tempfile.mkdtemp()
make_dir(os.path.join(self.data_dir, 'forks'))
self.config = SimpleConfig({'electrum_path': self.data_dir})
blockchain.blockchains = {}
def tearDown(self):
super().tearDown()
shutil.rmtree(self.data_dir)
def _append_header(self, chain: Blockchain, header: dict):
self.assertTrue(chain.can_connect(header))
chain.save_header(header)
def test_forking_and_swapping(self):
blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain(
config=self.config, forkpoint=0, parent=None,
forkpoint_hash=constants.net.GENESIS, prev_hash=None)
open(chain_u.path(), 'w+').close()
self._append_header(chain_u, self.HEADERS['A'])
self._append_header(chain_u, self.HEADERS['B'])
self._append_header(chain_u, self.HEADERS['C'])
self._append_header(chain_u, self.HEADERS['D'])
self._append_header(chain_u, self.HEADERS['E'])
self._append_header(chain_u, self.HEADERS['F'])
self._append_header(chain_u, self.HEADERS['O'])
self._append_header(chain_u, self.HEADERS['P'])
self._append_header(chain_u, self.HEADERS['Q'])
self._append_header(chain_u, self.HEADERS['R'])
chain_l = chain_u.fork(self.HEADERS['G'])
self._append_header(chain_l, self.HEADERS['H'])
self._append_header(chain_l, self.HEADERS['I'])
self._append_header(chain_l, self.HEADERS['J'])
# do checks
self.assertEqual(2, len(blockchain.blockchains))
self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, "forks"))))
self.assertEqual(0, chain_u.forkpoint)
self.assertEqual(None, chain_u.parent)
self.assertEqual(constants.net.GENESIS, chain_u._forkpoint_hash)
self.assertEqual(None, chain_u._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_u.path())
self.assertEqual(10 * 80, os.stat(chain_u.path()).st_size)
self.assertEqual(6, chain_l.forkpoint)
self.assertEqual(chain_u, chain_l.parent)
self.assertEqual(hash_header(self.HEADERS['G']), chain_l._forkpoint_hash)
self.assertEqual(hash_header(self.HEADERS['F']), chain_l._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_711a2e2a701354121a33660f45c9f9f3c4bbdb4441114c39ca837f6e7f689ee1"), chain_l.path())
self.assertEqual(4 * 80, os.stat(chain_l.path()).st_size)
self._append_header(chain_l, self.HEADERS['K'])
# chains were swapped, do checks
self.assertEqual(2, len(blockchain.blockchains))
self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, "forks"))))
self.assertEqual(6, chain_u.forkpoint)
self.assertEqual(chain_l, chain_u.parent)
self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash)
self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab"), chain_u.path())
self.assertEqual(4 * 80, os.stat(chain_u.path()).st_size)
self.assertEqual(0, chain_l.forkpoint)
self.assertEqual(None, chain_l.parent)
self.assertEqual(constants.net.GENESIS, chain_l._forkpoint_hash)
self.assertEqual(None, chain_l._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_l.path())
self.assertEqual(11 * 80, os.stat(chain_l.path()).st_size)
for b in (chain_u, chain_l):
self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())]))
self._append_header(chain_u, self.HEADERS['S'])
self._append_header(chain_u, self.HEADERS['T'])
self._append_header(chain_u, self.HEADERS['U'])
self._append_header(chain_l, self.HEADERS['L'])
chain_z = chain_l.fork(self.HEADERS['M'])
self._append_header(chain_z, self.HEADERS['N'])
self._append_header(chain_z, self.HEADERS['X'])
self._append_header(chain_z, self.HEADERS['Y'])
self._append_header(chain_z, self.HEADERS['Z'])
# chain_z became best chain, do checks
self.assertEqual(3, len(blockchain.blockchains))
self.assertEqual(2, len(os.listdir(os.path.join(self.data_dir, "forks"))))
self.assertEqual(0, chain_z.forkpoint)
self.assertEqual(None, chain_z.parent)
self.assertEqual(constants.net.GENESIS, chain_z._forkpoint_hash)
self.assertEqual(None, chain_z._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_z.path())
self.assertEqual(14 * 80, os.stat(chain_z.path()).st_size)
self.assertEqual(9, chain_l.forkpoint)
self.assertEqual(chain_z, chain_l.parent)
self.assertEqual(hash_header(self.HEADERS['J']), chain_l._forkpoint_hash)
self.assertEqual(hash_header(self.HEADERS['I']), chain_l._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_9_2874a1277687ab8042eff9916256b860a5b0a08b0038456c5a4a37d3bdf3656a_6e1acd473503ce0ee3cee916ca07db2f656b48baf8968f999189545316423bbb"), chain_l.path())
self.assertEqual(3 * 80, os.stat(chain_l.path()).st_size)
self.assertEqual(6, chain_u.forkpoint)
self.assertEqual(chain_z, chain_u.parent)
self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash)
self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab"), chain_u.path())
self.assertEqual(7 * 80, os.stat(chain_u.path()).st_size)
for b in (chain_u, chain_l, chain_z):
self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())]))
self.assertEqual(constants.net.GENESIS, chain_z.get_hash(0))
self.assertEqual(hash_header(self.HEADERS['F']), chain_z.get_hash(5))
self.assertEqual(hash_header(self.HEADERS['G']), chain_z.get_hash(6))
self.assertEqual(hash_header(self.HEADERS['I']), chain_z.get_hash(8))
self.assertEqual(hash_header(self.HEADERS['M']), chain_z.get_hash(9))
self.assertEqual(hash_header(self.HEADERS['Z']), chain_z.get_hash(13))
def test_doing_multiple_swaps_after_single_new_header(self):
blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain(
config=self.config, forkpoint=0, parent=None,
forkpoint_hash=constants.net.GENESIS, prev_hash=None)
open(chain_u.path(), 'w+').close()
self._append_header(chain_u, self.HEADERS['A'])
self._append_header(chain_u, self.HEADERS['B'])
self._append_header(chain_u, self.HEADERS['C'])
self._append_header(chain_u, self.HEADERS['D'])
self._append_header(chain_u, self.HEADERS['E'])
self._append_header(chain_u, self.HEADERS['F'])
self._append_header(chain_u, self.HEADERS['O'])
self._append_header(chain_u, self.HEADERS['P'])
self._append_header(chain_u, self.HEADERS['Q'])
self._append_header(chain_u, self.HEADERS['R'])
self._append_header(chain_u, self.HEADERS['S'])
self.assertEqual(1, len(blockchain.blockchains))
self.assertEqual(0, len(os.listdir(os.path.join(self.data_dir, "forks"))))
chain_l = chain_u.fork(self.HEADERS['G'])
self._append_header(chain_l, self.HEADERS['H'])
self._append_header(chain_l, self.HEADERS['I'])
self._append_header(chain_l, self.HEADERS['J'])
self._append_header(chain_l, self.HEADERS['K'])
# now chain_u is best chain, but it's tied with chain_l
self.assertEqual(2, len(blockchain.blockchains))
self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, "forks"))))
chain_z = chain_l.fork(self.HEADERS['M'])
self._append_header(chain_z, self.HEADERS['N'])
self._append_header(chain_z, self.HEADERS['X'])
self.assertEqual(3, len(blockchain.blockchains))
self.assertEqual(2, len(os.listdir(os.path.join(self.data_dir, "forks"))))
# chain_z became best chain, do checks
self.assertEqual(0, chain_z.forkpoint)
self.assertEqual(None, chain_z.parent)
self.assertEqual(constants.net.GENESIS, chain_z._forkpoint_hash)
self.assertEqual(None, chain_z._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_z.path())
self.assertEqual(12 * 80, os.stat(chain_z.path()).st_size)
self.assertEqual(9, chain_l.forkpoint)
self.assertEqual(chain_z, chain_l.parent)
self.assertEqual(hash_header(self.HEADERS['J']), chain_l._forkpoint_hash)
self.assertEqual(hash_header(self.HEADERS['I']), chain_l._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_9_2874a1277687ab8042eff9916256b860a5b0a08b0038456c5a4a37d3bdf3656a_6e1acd473503ce0ee3cee916ca07db2f656b48baf8968f999189545316423bbb"), chain_l.path())
self.assertEqual(2 * 80, os.stat(chain_l.path()).st_size)
self.assertEqual(6, chain_u.forkpoint)
self.assertEqual(chain_z, chain_u.parent)
self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash)
self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash)
self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab"), chain_u.path())
self.assertEqual(5 * 80, os.stat(chain_u.path()).st_size)
self.assertEqual(constants.net.GENESIS, chain_z.get_hash(0))
self.assertEqual(hash_header(self.HEADERS['F']), chain_z.get_hash(5))
self.assertEqual(hash_header(self.HEADERS['G']), chain_z.get_hash(6))
self.assertEqual(hash_header(self.HEADERS['I']), chain_z.get_hash(8))
self.assertEqual(hash_header(self.HEADERS['M']), chain_z.get_hash(9))
self.assertEqual(hash_header(self.HEADERS['X']), chain_z.get_hash(11))
for b in (chain_u, chain_l, chain_z):
self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())]))

20
electrum/tests/test_network.py

@ -6,6 +6,9 @@ from electrum import constants
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum import blockchain from electrum import blockchain
from electrum.interface import Interface from electrum.interface import Interface
from electrum.crypto import sha256
from electrum.util import bh2u
class MockTaskGroup: class MockTaskGroup:
async def spawn(self, x): return async def spawn(self, x): return
@ -17,10 +20,14 @@ class MockNetwork:
class MockInterface(Interface): class MockInterface(Interface):
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
super().__init__(MockNetwork(), 'mock-server:50000:t', self.config.electrum_path(), None) network = MockNetwork()
network.config = config
super().__init__(network, 'mock-server:50000:t', None)
self.q = asyncio.Queue() self.q = asyncio.Queue()
self.blockchain = blockchain.Blockchain(self.config, 2002, None) self.blockchain = blockchain.Blockchain(config=self.config, forkpoint=0,
parent=None, forkpoint_hash=constants.net.GENESIS, prev_hash=None)
self.tip = 12 self.tip = 12
self.blockchain._size = self.tip + 1
async def get_block_header(self, height, assert_mode): async def get_block_header(self, height, assert_mode):
assert self.q.qsize() > 0, (height, assert_mode) assert self.q.qsize() > 0, (height, assert_mode)
item = await self.q.get() item = await self.q.get()
@ -56,7 +63,7 @@ class TestNetwork(unittest.TestCase):
self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
ifa = self.interface ifa = self.interface
self.assertEqual(('fork_noconflict', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7))) self.assertEqual(('fork', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
self.assertEqual(self.interface.q.qsize(), 0) self.assertEqual(self.interface.q.qsize(), 0)
def test_fork_conflict(self): def test_fork_conflict(self):
@ -70,7 +77,7 @@ class TestNetwork(unittest.TestCase):
self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
ifa = self.interface ifa = self.interface
self.assertEqual(('fork_conflict', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7))) self.assertEqual(('fork', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
self.assertEqual(self.interface.q.qsize(), 0) self.assertEqual(self.interface.q.qsize(), 0)
def test_can_connect_during_backward(self): def test_can_connect_during_backward(self):
@ -87,7 +94,10 @@ class TestNetwork(unittest.TestCase):
self.assertEqual(self.interface.q.qsize(), 0) self.assertEqual(self.interface.q.qsize(), 0)
def mock_fork(self, bad_header): def mock_fork(self, bad_header):
return blockchain.Blockchain(self.config, bad_header['block_height'], None) forkpoint = bad_header['block_height']
b = blockchain.Blockchain(config=self.config, forkpoint=forkpoint, parent=None,
forkpoint_hash=bh2u(sha256(str(forkpoint))), prev_hash=bh2u(sha256(str(forkpoint-1))))
return b
def test_chain_false_during_binary(self): def test_chain_false_during_binary(self):
blockchain.blockchains = {} blockchain.blockchains = {}

1
electrum/tests/test_wallet.py

@ -64,7 +64,6 @@ class TestWalletStorage(WalletTestCase):
storage.put(key, value) storage.put(key, value)
storage.write() storage.write()
contents = ""
with open(self.wallet_path, "r") as f: with open(self.wallet_path, "r") as f:
contents = f.read() contents = f.read()
self.assertEqual(some_dict, json.loads(contents)) self.assertEqual(some_dict, json.loads(contents))

Loading…
Cancel
Save