tDetect blockchain splits and validate multiple chains - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit ca220d8dbbee6a7221a95e619301fe40dca2678a DIR parent 6b45070b2f7c899dd9bdd1a7bf423ad23a014374 HTML Author: ThomasV <thomasv@electrum.org> Date: Mon, 29 May 2017 09:03:39 +0200 Detect blockchain splits and validate multiple chains Diffstat: M RELEASE-NOTES | 7 +++++++ M gui/kivy/uix/dialogs/checkpoint_di… | 21 +-------------------- M gui/qt/network_dialog.py | 74 ++++++++++--------------------- M lib/blockchain.py | 164 ++++++++++++------------------- M lib/network.py | 225 ++++++++++++++++++++++--------- M lib/util.py | 16 ++++++---------- M lib/verifier.py | 2 +- 7 files changed, 259 insertions(+), 250 deletions(-) --- DIR diff --git a/RELEASE-NOTES b/RELEASE-NOTES t@@ -1,3 +1,10 @@ +# Release 2.9 - Independence + * Blockchain fork detection and management: + - The SPV module will download and verify block headers from + multiple branches + - Branching points are located using binary search + - The desired branch of a fork can be selected using the network dialog + # Release 2.8.3 * Fix crash on reading older wallet formats. * TrustedCoin: remove pay-per-tx option DIR diff --git a/gui/kivy/uix/dialogs/checkpoint_dialog.py b/gui/kivy/uix/dialogs/checkpoint_dialog.py t@@ -48,7 +48,6 @@ Builder.load_string(''' height: '36dp' size_hint_y: None text: '%d'%root.cp_height - on_focus: root.on_height_str() TopLabel: text: _('Block hash') + ':' TxHashLabel: t@@ -85,23 +84,5 @@ class CheckpointDialog(Factory.Popup): def __init__(self, network, callback): Factory.Popup.__init__(self) self.network = network - self.cp_height, self.cp_value = self.network.blockchain.get_checkpoint() self.callback = callback - - def on_height_str(self): - try: - new_height = int(self.ids.height_input.text) - except: - new_height = self.cp_height - self.ids.height_input.text = '%d'%new_height - if new_height == self.cp_height: - return - try: - header = self.network.synchronous_get(('blockchain.block.get_header', [new_height]), 5) - new_value = self.network.blockchain.hash_header(header) - except BaseException as e: - self.network.print_error(str(e)) - new_value = '' - if new_value: - self.cp_height = new_height - self.cp_value = new_value + self.is_split = len(self.network.blockchains) > 1 DIR diff --git a/gui/qt/network_dialog.py b/gui/qt/network_dialog.py t@@ -190,39 +190,10 @@ class NetworkChoiceLayout(object): from amountedit import AmountEdit grid = QGridLayout(blockchain_tab) n = len(network.get_interfaces()) - status = _("Connected to %d nodes.")%n if n else _("Not connected") - height_str = "%d "%(network.get_local_height()) + _("blocks") - self.checkpoint_height, self.checkpoint_value = network.blockchain.get_checkpoint() - self.cph_label = QLabel(_('Height')) - self.cph = QLineEdit("%d"%self.checkpoint_height) - self.cph.setFixedWidth(80) - self.cpv_label = QLabel(_('Hash')) - self.cpv = QLineEdit(self.checkpoint_value) - self.cpv.setCursorPosition(0) - self.cpv.setFocusPolicy(Qt.NoFocus) - self.cpv.setReadOnly(True) - def on_cph(): - try: - height = int(self.cph.text()) - except: - height = 0 - self.cph.setText('%d'%height) - if height == self.checkpoint_height: - return - try: - self.network.print_error("fetching header") - header = self.network.synchronous_get(('blockchain.block.get_header', [height]), 5) - _hash = self.network.blockchain.hash_header(header) - except BaseException as e: - self.network.print_error(str(e)) - _hash = '' - self.cpv.setText(_hash) - self.cpv.setCursorPosition(0) - if _hash: - self.checkpoint_height = height - self.checkpoint_value = _hash - self.cph.editingFinished.connect(on_cph) + n_chains = len(network.blockchains) + self.checkpoint_height = network.get_checkpoint() + status = _("Connected to %d nodes.")%n if n else _("Not connected") msg = ' '.join([ _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."), _("This blockchain is used to verify the transactions sent by your transaction server.") t@@ -230,23 +201,26 @@ class NetworkChoiceLayout(object): grid.addWidget(QLabel(_('Status') + ':'), 0, 0) grid.addWidget(QLabel(status), 0, 1, 1, 3) grid.addWidget(HelpButton(msg), 0, 4) - msg = _('This is the height of your local copy of the blockchain.') - grid.addWidget(QLabel(_("Height") + ':'), 1, 0) - grid.addWidget(QLabel(height_str), 1, 1) - grid.addWidget(HelpButton(msg), 1, 4) - msg = ''.join([ - _('A checkpoint can be used to verify that you are on the correct blockchain.'), ' ', - _('By default, your checkpoint is the genesis block.'), '\n\n', - _('If you edit the height field, the corresponding block hash will be fetched from your current server.'), ' ', - _('If you press OK, the checkpoint will be saved, and Electrum will only accept headers from nodes that pass this checkpoint.'), '\n\n', - _('If there is a hard fork, you will have to check the block hash from an independent source, in order to be sure that you are on the desired side of the fork.'), - ]) - grid.addWidget(QLabel(_('Checkpoint') +':'), 3, 0, 1, 2) - grid.addWidget(HelpButton(msg), 3, 4) - grid.addWidget(self.cph_label, 4, 0) - grid.addWidget(self.cph, 4, 1) - grid.addWidget(self.cpv_label, 5, 0) - grid.addWidget(self.cpv, 5, 1, 1, 4) + if n_chains == 1: + height_str = "%d "%(network.get_local_height()) + _("blocks") + msg = _('This is the height of your local copy of the blockchain.') + grid.addWidget(QLabel(_("Height") + ':'), 1, 0) + grid.addWidget(QLabel(height_str), 1, 1) + grid.addWidget(HelpButton(msg), 1, 4) + else: + checkpoint = network.get_checkpoint() + self.cph_label = QLabel(_('Chain split detected')) + grid.addWidget(self.cph_label, 4, 0) + chains_list_widget = QTreeWidget() + chains_list_widget.setHeaderLabels( [ _('Nodes'), _('Blocks'), _('Checkpoint'), _('Hash') ] ) + chains_list_widget.setMaximumHeight(150) + grid.addWidget(chains_list_widget, 5, 0, 1, 5) + for b in network.blockchains.values(): + _hash = b.get_hash(checkpoint) + height = b.height() + count = sum([i.blockchain == b for i in network.interfaces.values()]) + chains_list_widget.addTopLevelItem(QTreeWidgetItem( [ '%d'%count, '%d'%height, '%d'%checkpoint, _hash ] )) + grid.setRowStretch(7, 1) vbox = QVBoxLayout() vbox.addWidget(tabs) t@@ -328,7 +302,7 @@ class NetworkChoiceLayout(object): proxy = None auto_connect = self.autoconnect_cb.isChecked() self.network.set_parameters(host, port, protocol, proxy, auto_connect) - self.network.blockchain.set_checkpoint(self.checkpoint_height, self.checkpoint_value) + #self.network.blockchain.set_checkpoint(self.checkpoint_height, self.checkpoint_value) def suggest_proxy(self, found_proxy): self.tor_proxy = found_proxy DIR diff --git a/lib/blockchain.py b/lib/blockchain.py t@@ -32,50 +32,56 @@ from bitcoin import * MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 +def serialize_header(res): + s = int_to_hex(res.get('version'), 4) \ + + rev_hex(res.get('prev_block_hash')) \ + + rev_hex(res.get('merkle_root')) \ + + int_to_hex(int(res.get('timestamp')), 4) \ + + int_to_hex(int(res.get('bits')), 4) \ + + int_to_hex(int(res.get('nonce')), 4) + return s + +def deserialize_header(s, height): + hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16) + h = {} + h['version'] = hex_to_int(s[0:4]) + h['prev_block_hash'] = hash_encode(s[4:36]) + h['merkle_root'] = hash_encode(s[36:68]) + h['timestamp'] = hex_to_int(s[68:72]) + h['bits'] = hex_to_int(s[72:76]) + h['nonce'] = hex_to_int(s[76:80]) + h['block_height'] = height + return h + +def hash_header(header): + if header is None: + return '0' * 64 + if header.get('prev_block_hash') is None: + header['prev_block_hash'] = '00'*32 + return hash_encode(Hash(serialize_header(header).decode('hex'))) + + class Blockchain(util.PrintError): '''Manages blockchain headers and their verification''' - def __init__(self, config, network): + def __init__(self, config, checkpoint): self.config = config - self.network = network - self.checkpoint_height, self.checkpoint_hash = self.get_checkpoint() - self.check_truncate_headers() + self.checkpoint = checkpoint + self.filename = 'blockchain_headers' if checkpoint == 0 else 'blockchain_fork_%d'%checkpoint self.set_local_height() + self.catch_up = None # interface catching up def height(self): return self.local_height - def init(self): - import threading - if os.path.exists(self.path()): - self.downloading_headers = False - return - self.downloading_headers = True - t = threading.Thread(target = self.init_headers_file) - t.daemon = True - t.start() - - def pass_checkpoint(self, header): - if type(header) is not dict: - return False - if header.get('block_height') != self.checkpoint_height: - return True - if header.get('prev_block_hash') is None: - header['prev_block_hash'] = '00'*32 - try: - _hash = self.hash_header(header) - except: - return False - return _hash == self.checkpoint_hash - def verify_header(self, header, prev_header, bits, target): - prev_hash = self.hash_header(prev_header) - _hash = self.hash_header(header) + prev_hash = hash_header(prev_header) + _hash = hash_header(header) if prev_hash != header.get('prev_block_hash'): raise BaseException("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) - if not self.pass_checkpoint(header): - raise BaseException('failed checkpoint') - if self.checkpoint_height == header.get('block_height'): - self.print_error("validated checkpoint", self.checkpoint_height) + #if not self.pass_checkpoint(header): + # raise BaseException('failed checkpoint') + #if self.checkpoint_height == header.get('block_height'): + # self.print_error("validated checkpoint", self.checkpoint_height) if bitcoin.TESTNET: return if bits != header.get('bits'): t@@ -100,70 +106,31 @@ class Blockchain(util.PrintError): bits, target = self.get_target(index) for i in range(num): raw_header = data[i*80:(i+1) * 80] - header = self.deserialize_header(raw_header, index*2016 + i) + header = deserialize_header(raw_header, index*2016 + i) self.verify_header(header, prev_header, bits, target) prev_header = header - def serialize_header(self, res): - s = int_to_hex(res.get('version'), 4) \ - + rev_hex(res.get('prev_block_hash')) \ - + rev_hex(res.get('merkle_root')) \ - + int_to_hex(int(res.get('timestamp')), 4) \ - + int_to_hex(int(res.get('bits')), 4) \ - + int_to_hex(int(res.get('nonce')), 4) - return s - - def deserialize_header(self, s, height): - hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16) - h = {} - h['version'] = hex_to_int(s[0:4]) - h['prev_block_hash'] = hash_encode(s[4:36]) - h['merkle_root'] = hash_encode(s[36:68]) - h['timestamp'] = hex_to_int(s[68:72]) - h['bits'] = hex_to_int(s[72:76]) - h['nonce'] = hex_to_int(s[76:80]) - h['block_height'] = height - return h - - def hash_header(self, header): - if header is None: - return '0' * 64 - return hash_encode(Hash(self.serialize_header(header).decode('hex'))) - def path(self): - return util.get_headers_path(self.config) - - def init_headers_file(self): - filename = self.path() - try: - import urllib, socket - socket.setdefaulttimeout(30) - self.print_error("downloading ", bitcoin.HEADERS_URL) - urllib.urlretrieve(bitcoin.HEADERS_URL, filename + '.tmp') - os.rename(filename + '.tmp', filename) - self.print_error("done.") - except Exception: - self.print_error("download failed. creating file", filename) - open(filename, 'wb+').close() - self.downloading_headers = False - self.set_local_height() - self.print_error("%d blocks" % self.local_height) + d = util.get_headers_dir(self.config) + return os.path.join(d, self.filename) def save_chunk(self, index, chunk): filename = self.path() f = open(filename, 'rb+') f.seek(index * 2016 * 80) + f.truncate() h = f.write(chunk) f.close() self.set_local_height() def save_header(self, header): - data = self.serialize_header(header).decode('hex') + data = serialize_header(header).decode('hex') assert len(data) == 80 height = header.get('block_height') filename = self.path() f = open(filename, 'rb+') f.seek(height * 80) + f.truncate() h = f.write(data) f.close() self.set_local_height() t@@ -184,9 +151,12 @@ class Blockchain(util.PrintError): h = f.read(80) f.close() if len(h) == 80: - h = self.deserialize_header(h, block_height) + h = deserialize_header(h, block_height) return h + def get_hash(self, height): + return bitcoin.GENESIS if height == 0 else hash_header(self.read_header(height)) + def BIP9(self, height, flag): v = self.read_header(height)['version'] return ((v & 0xE0000000) == 0x20000000) and ((v & flag) == flag) t@@ -195,15 +165,6 @@ class Blockchain(util.PrintError): h = self.local_height return sum([self.BIP9(h-i, 2) for i in range(N)])*10000/N/100. - def check_truncate_headers(self): - checkpoint = self.read_header(self.checkpoint_height) - if checkpoint is None: - return - if self.hash_header(checkpoint) == self.checkpoint_hash: - return - self.print_error('checkpoint mismatch:', self.hash_header(checkpoint), self.checkpoint_hash) - self.truncate_headers(self.checkpoint_height) - def truncate_headers(self, height): self.print_error('Truncating headers file at height %d'%height) name = self.path() t@@ -212,6 +173,17 @@ class Blockchain(util.PrintError): f.truncate() f.close() + def fork(self, height): + import shutil + filename = "blockchain_fork_%d"%height + new_path = os.path.join(util.get_headers_dir(self.config), filename) + shutil.copy(self.path(), new_path) + with open(new_path, 'rb+') as f: + f.seek((height) * 80) + f.truncate() + f.close() + return filename + def get_target(self, index, chain=None): if bitcoin.TESTNET: return 0, 0 t@@ -255,7 +227,7 @@ class Blockchain(util.PrintError): previous_header = self.read_header(previous_height) if not previous_header: return False - prev_hash = self.hash_header(previous_header) + prev_hash = hash_header(previous_header) if prev_hash != header.get('prev_block_hash'): return False height = header.get('block_height') t@@ -270,21 +242,9 @@ class Blockchain(util.PrintError): try: data = hexdata.decode('hex') self.verify_chunk(idx, data) - self.print_error("validated chunk %d" % idx) + #self.print_error("validated chunk %d" % idx) self.save_chunk(idx, data) return True except BaseException as e: self.print_error('verify_chunk failed', str(e)) return False - - def get_checkpoint(self): - height = self.config.get('checkpoint_height', 0) - value = self.config.get('checkpoint_value', bitcoin.GENESIS) - return (height, value) - - def set_checkpoint(self, height, value): - self.checkpoint_height = height - self.checkpoint_hash = value - self.config.set_key('checkpoint_height', height) - self.config.set_key('checkpoint_value', value) - self.check_truncate_headers() DIR diff --git a/lib/network.py b/lib/network.py t@@ -30,7 +30,7 @@ import random import select import traceback from collections import defaultdict, deque -from threading import Lock +import threading import socks import socket t@@ -204,7 +204,17 @@ class Network(util.DaemonThread): util.DaemonThread.__init__(self) self.config = SimpleConfig(config) if type(config) == type({}) else config self.num_server = 8 if not self.config.get('oneserver') else 0 - self.blockchain = Blockchain(self.config, self) + self.blockchains = { 0:Blockchain(self.config, 0) } + for x in os.listdir(self.config.path): + if x.startswith('blockchain_fork_'): + n = int(x[16:]) + b = Blockchain(self.config, n) + self.blockchains[n] = b + self.print_error("blockchains", self.blockchains.keys()) + self.blockchain_index = config.get('blockchain_index', 0) + if self.blockchain_index not in self.blockchains.keys(): + self.blockchain_index = 0 + # Server for addresses and transactions self.default_server = self.config.get('server') # Sanitize default server t@@ -215,13 +225,12 @@ class Network(util.DaemonThread): if not self.default_server: self.default_server = pick_random_server() - self.lock = Lock() + self.lock = threading.Lock() self.pending_sends = [] self.message_id = 0 self.debug = False self.irc_servers = {} # returned by interface (list from irc) self.recent_servers = self.read_recent_servers() - self.catch_up = None # interface catching up self.banner = '' self.donation_address = '' t@@ -493,18 +502,15 @@ class Network(util.DaemonThread): if servers: self.switch_to_interface(random.choice(servers)) - def switch_lagging_interface(self, suggestion = None): + def switch_lagging_interface(self): '''If auto_connect and lagging, switch interface''' if self.server_is_lagging() and self.auto_connect: - if suggestion and self.protocol == deserialize_server(suggestion)[2]: - self.switch_to_interface(suggestion) - else: - # switch to one that has the correct header (not height) - header = self.get_header(self.get_local_height()) - filtered = map(lambda x:x[0], filter(lambda x: x[1]==header, self.headers.items())) - if filtered: - choice = random.choice(filtered) - self.switch_to_interface(choice) + # switch to one that has the correct header (not height) + header = self.blockchain().read_header(self.get_local_height()) + filtered = map(lambda x:x[0], filter(lambda x: x[1]==header, self.headers.items())) + if filtered: + choice = random.choice(filtered) + self.switch_to_interface(choice) def switch_to_interface(self, server): '''Switch to server as our interface. If no connection exists nor t@@ -688,15 +694,31 @@ class Network(util.DaemonThread): self.close_interface(self.interfaces[server]) self.headers.pop(server, None) self.notify('interfaces') - if server == self.catch_up: - self.catch_up = None + for b in self.blockchains.values(): + if b.catch_up == server: + b.catch_up = None + + def get_checkpoint(self): + return max(self.blockchains.keys()) + + def get_blockchain(self, header): + from blockchain import hash_header + if type(header) is not dict: + return False + header_hash = hash_header(header) + height = header.get('block_height') + for b in self.blockchains.values(): + if header_hash == b.get_hash(height): + return b + return False def new_interface(self, server, socket): self.add_recent_server(server) interface = Interface(server, socket) + interface.blockchain = None interface.mode = 'checkpoint' self.interfaces[server] = interface - self.request_header(interface, self.blockchain.checkpoint_height) + self.request_header(interface, self.get_checkpoint()) if server == self.default_server: self.switch_to_interface(server) self.notify('interfaces') t@@ -758,26 +780,27 @@ class Network(util.DaemonThread): index = response['params'][0] if interface.request != index: return - connect = self.blockchain.connect_chunk(index, response['result']) + connect = interface.blockchain.connect_chunk(index, response['result']) # If not finished, get the next chunk if not connect: return - if self.get_local_height() < interface.tip: + if interface.blockchain.height() < interface.tip: self.request_chunk(interface, index+1) else: interface.request = None + interface.mode = 'default' + interface.print_error('catch up done') + interface.blockchain.catch_up = None self.notify('updated') def request_header(self, interface, height): - interface.print_error("requesting header %d" % height) + #interface.print_error("requesting header %d" % height) self.queue_request('blockchain.block.get_header', [height], interface) interface.request = height interface.req_time = time.time() def on_get_header(self, interface, response): '''Handle receiving a single block header''' - if self.blockchain.downloading_headers: - return header = response.get('result') if not header: interface.print_error(response) t@@ -789,20 +812,27 @@ class Network(util.DaemonThread): self.connection_down(interface.server) return self.on_header(interface, header) + + def can_connect(self, header): + for blockchain in self.blockchains.values(): + if blockchain.can_connect(header): + return blockchain def on_header(self, interface, header): height = header.get('block_height') if interface.mode == 'checkpoint': - if self.blockchain.pass_checkpoint(header): + b = self.get_blockchain(header) + if b: interface.mode = 'default' + interface.blockchain = b + #interface.print_error('passed checkpoint', b.filename) self.queue_request('blockchain.headers.subscribe', [], interface) else: - if interface != self.interface or self.auto_connect: - interface.print_error("checkpoint failed") - self.connection_down(interface.server) + interface.print_error("checkpoint failed") + self.connection_down(interface.server) interface.request = None return - can_connect = self.blockchain.can_connect(header) + can_connect = self.can_connect(header) if interface.mode == 'backward': if can_connect: interface.good = height t@@ -821,36 +851,56 @@ class Network(util.DaemonThread): interface.good = height else: interface.bad = height - if interface.good == interface.bad - 1: - interface.print_error("catching up from %d"% interface.good) - interface.mode = 'default' - next_height = interface.good - else: + if interface.bad != interface.good + 1: next_height = (interface.bad + interface.good) // 2 - elif interface.mode == 'default': + else: + interface.print_error("found connection at %d"% interface.good) + delta1 = interface.blockchain.height() - interface.good + delta2 = interface.tip - interface.good + if delta1 > 10 and delta2 > 10: + interface.print_error("chain split detected: %d (%d %d)"% (interface.good, delta1, delta2)) + interface.blockchain.fork(interface.bad) + interface.blockchain = Blockchain(self.config, interface.bad) + self.blockchains[interface.bad] = interface.blockchain + if interface.blockchain.catch_up is None: + interface.blockchain.catch_up = interface.server + interface.print_error("catching up") + interface.mode = 'catch_up' + next_height = interface.good + else: + # todo: if current catch_up is too slow, queue others + next_height = None + elif interface.mode == 'catch_up': if can_connect: - self.blockchain.save_header(header) + interface.blockchain.save_header(header) self.notify('updated') next_height = height + 1 if height < interface.tip else None else: - interface.print_error("cannot connect %d"% height) - interface.mode = 'backward' - interface.bad = height - next_height = height - 1 + next_height = None + + if next_height is None: + # exit catch_up state + interface.request = None + interface.mode = 'default' + interface.print_error('catch up done', interface.blockchain.catch_up) + interface.blockchain.catch_up = None + + elif interface.mode == 'default': + assert not can_connect + interface.print_error("cannot connect %d"% height) + interface.mode = 'backward' + interface.bad = height + # save height where we failed + interface.blockchain_height = interface.blockchain.height() + next_height = height - 1 else: raise BaseException(interface.mode) # If not finished, get the next header if next_height: - if interface.mode != 'default': - self.request_header(interface, next_height) + if interface.mode == 'catch_up' and interface.tip > next_height + 50: + self.request_chunk(interface, next_height // 2016) else: - if interface.tip > next_height + 50: - self.request_chunk(interface, next_height // 2016) - else: - self.request_header(interface, next_height) - else: - interface.request = None - self.catch_up = None + self.request_header(interface, next_height) def maintain_requests(self): for interface in self.interfaces.values(): t@@ -879,8 +929,33 @@ class Network(util.DaemonThread): for interface in rout: self.process_responses(interface) + def init_headers_file(self): + filename = self.blockchains[0].path() + if os.path.exists(filename): + self.downloading_headers = False + return + def download_thread(): + try: + import urllib, socket + socket.setdefaulttimeout(30) + self.print_error("downloading ", bitcoin.HEADERS_URL) + urllib.urlretrieve(bitcoin.HEADERS_URL, filename + '.tmp') + os.rename(filename + '.tmp', filename) + self.print_error("done.") + except Exception: + self.print_error("download failed. creating file", filename) + open(filename, 'wb+').close() + self.downloading_headers = False + self.blockchains[0].set_local_height() + self.downloading_headers = True + t = threading.Thread(target = download_thread) + t.daemon = True + t.start() + def run(self): - self.blockchain.init() + self.init_headers_file() + while self.is_running() and self.downloading_headers: + time.sleep(1) while self.is_running(): self.maintain_sockets() self.wait_on_sockets() t@@ -890,35 +965,51 @@ class Network(util.DaemonThread): self.stop_network() self.on_stop() - def on_notify_header(self, i, header): + def on_notify_header(self, interface, header): height = header.get('block_height') if not height: return - self.headers[i.server] = header - i.tip = height - local_height = self.get_local_height() - - if i.tip > local_height: - i.print_error("better height", height) - # if I can connect, do it right away - if self.blockchain.can_connect(header): - self.blockchain.save_header(header) + self.headers[interface.server] = header + interface.tip = height + local_height = interface.blockchain.height() + if interface.mode != 'default': + return + if interface.tip > local_height + 1: + if interface.blockchain.catch_up is None: + interface.blockchain.catch_up = interface.server + interface.mode = 'catch_up' # must transition to search if it does not connect + self.request_header(interface, local_height + 1) + else: + # another interface is catching up + pass + elif interface.tip == local_height + 1: + if interface.blockchain.can_connect(header): + interface.blockchain.save_header(header) self.notify('updated') - # otherwise trigger a search - elif self.catch_up is None: - self.catch_up = i.server - self.on_header(i, header) - - if i == self.interface: + else: + interface.mode = 'backward' + interface.bad = height + self.request_header(interface, local_height) + else: + if not interface.blockchain.can_connect(header): + interface.mode = 'backward' + interface.bad = height + self.request_header(interface, height - 1) + else: + pass + if interface == self.interface: self.switch_lagging_interface() self.notify('updated') + def blockchain(self): + if self.interface and self.interface.blockchain is not None: + self.blockchain_index = self.interface.blockchain.checkpoint + self.config.set_key('blockchain_index', self.blockchain_index) - def get_header(self, tx_height): - return self.blockchain.read_header(tx_height) + return self.blockchains[self.blockchain_index] def get_local_height(self): - return self.blockchain.height() + return self.blockchain().height() def synchronous_get(self, request, timeout=30): queue = Queue.Queue() DIR diff --git a/lib/util.py b/lib/util.py t@@ -213,12 +213,11 @@ def android_data_dir(): PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') return PythonActivity.mActivity.getFilesDir().getPath() + '/data' -def android_headers_path(): - path = android_ext_dir() + '/org.electrum.electrum/blockchain_headers' - d = os.path.dirname(path) +def android_headers_dir(): + d = android_ext_dir() + '/org.electrum.electrum' if not os.path.exists(d): os.mkdir(d) - return path + return d def android_check_data_dir(): """ if needed, move old directory to sandbox """ t@@ -227,7 +226,7 @@ def android_check_data_dir(): old_electrum_dir = ext_dir + '/electrum' if not os.path.exists(data_dir) and os.path.exists(old_electrum_dir): import shutil - new_headers_path = android_headers_path() + new_headers_path = android_headers_dir() + '/blockchain_headers' old_headers_path = old_electrum_dir + '/blockchain_headers' if not os.path.exists(new_headers_path) and os.path.exists(old_headers_path): print_error("Moving headers file to", new_headers_path) t@@ -236,11 +235,8 @@ def android_check_data_dir(): shutil.move(old_electrum_dir, data_dir) return data_dir -def get_headers_path(config): - if 'ANDROID_DATA' in os.environ: - return android_headers_path() - else: - return os.path.join(config.path, 'blockchain_headers') +def get_headers_dir(config): + return android_headers_dir() if 'ANDROID_DATA' in os.environ else config.path def user_dir(): if 'ANDROID_DATA' in os.environ: DIR diff --git a/lib/verifier.py b/lib/verifier.py t@@ -64,7 +64,7 @@ class SPV(ThreadJob): tx_height = merkle.get('block_height') pos = merkle.get('pos') merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos) - header = self.network.get_header(tx_height) + header = self.network.blockchain().read_header(tx_height) if not header or header.get('merkle_root') != merkle_root: # FIXME: we should make a fresh connection to a server to # recover from this, as this TX will now never verify