URI: 
       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