URI: 
       tinterface: further simplifications for fork resolution - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 7221fb3231a736dc5d3ebd5ed5e0723c9b8203f2
   DIR parent b3a2bce213451036ed49e4af4780ae2bbb90ac30
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Mon, 17 Sep 2018 22:21:55 +0200
       
       interface: further simplifications for fork resolution
       
       Diffstat:
         M electrum/interface.py               |      57 ++++++++++++++++++-------------
         M electrum/tests/test_network.py      |      54 +++++++++++---------------------
       
       2 files changed, 52 insertions(+), 59 deletions(-)
       ---
   DIR diff --git a/electrum/interface.py b/electrum/interface.py
       t@@ -448,6 +448,7 @@ class Interface(PrintError):
                return await self._resolve_potential_chain_fork_given_forkpoint(good, bad, bad_header)
        
            async def _search_headers_binary(self, height, bad, bad_header, chain):
       +        assert bad == bad_header['block_height']
                _assert_header_does_not_check_against_any_chain(bad_header)
        
                self.blockchain = chain if isinstance(chain, Blockchain) else self.blockchain
       t@@ -467,7 +468,7 @@ class Interface(PrintError):
                    if good + 1 == bad:
                        break
        
       -        mock = bad_header and 'mock' in bad_header and bad_header['mock']['connect'](height)
       +        mock = 'mock' in bad_header and bad_header['mock']['connect'](height)
                real = not mock and self.blockchain.can_connect(bad_header, check_height=False)
                if not real and not mock:
                    raise Exception('unexpected bad header during binary: {}'.format(bad_header))
       t@@ -478,39 +479,49 @@ class Interface(PrintError):
        
            async def _resolve_potential_chain_fork_given_forkpoint(self, good, bad, bad_header):
                assert good + 1 == bad
       +        assert bad == bad_header['block_height']
                _assert_header_does_not_check_against_any_chain(bad_header)
       -
       +        # 'good' is the height of a block 'good_header', somewhere in self.blockchain.
       +        # bad_header connects to good_header; bad_header itself is NOT in self.blockchain.
       +
       +        bh = self.blockchain.height()
       +        assert bh >= good
       +        if bh == good:
       +            height = good + 1
       +            self.print_error("catching up from {}".format(height))
       +            return 'no_fork', height
       +
       +        # this is a new fork we don't yet have
       +        height = bad + 1
                branch = blockchain.blockchains.get(bad)
                if branch is not None:
       -            self.print_error("existing fork found at bad height {}".format(bad))
       +            # Conflict!! As our fork handling is not completely general,
       +            # we need to delete another fork to save this one.
       +            # Note: This could be a potential DOS vector against Electrum.
       +            # However, mining blocks that satisfy the difficulty requirements
       +            # 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))
                    ismocking = type(branch) is dict
       -            height = bad + 1
                    if ismocking:
                        self.print_error("TODO replace blockchain")
       -                return 'conflict', height
       +                return 'fork_conflict', height
                    self.print_error('forkpoint conflicts with existing fork', branch.path())
                    branch.write(b'', 0)
                    branch.save_header(bad_header)
                    self.blockchain = branch
       -            return 'conflict', height
       +            return 'fork_conflict', height
                else:
       -            bh = self.blockchain.height()
       -            self.print_error("no existing fork yet at bad height {}. local chain height: {}".format(bad, bh))
       -            if bh > good:
       -                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
       -                height = b.forkpoint + 1
       -                assert b.forkpoint == bad
       -                return 'fork', height
       -            else:
       -                assert bh == good
       -                self.print_error("catching up from %d" % (bh + 1))
       -                height = bh + 1
       -                return 'no_fork', height
       +            # 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
        
            async def _search_headers_backwards(self, height, header):
                async def iterate():
   DIR diff --git a/electrum/tests/test_network.py b/electrum/tests/test_network.py
       t@@ -38,7 +38,7 @@ class TestNetwork(unittest.TestCase):
                self.config = SimpleConfig({'electrum_path': tempfile.mkdtemp(prefix="test_network")})
                self.interface = MockInterface(self.config)
        
       -    def test_new_fork(self):
       +    def test_fork_noconflict(self):
                blockchain.blockchains = {}
                self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
                def mock_connect(height):
       t@@ -49,10 +49,24 @@ 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': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}})
                ifa = self.interface
       -        self.assertEqual(('fork', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
       +        self.assertEqual(('fork_noconflict', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
                self.assertEqual(self.interface.q.qsize(), 0)
        
       -    def test_new_can_connect_during_backward(self):
       +    def test_fork_conflict(self):
       +        blockchain.blockchains = {7: {'check': lambda bad_header: False}}
       +        self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
       +        def mock_connect(height):
       +            return height == 6
       +        self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1,'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}})
       +        self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1,'check':lambda x: True, 'connect': lambda x: False}})
       +        self.interface.q.put_nowait({'block_height': 4, '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}})
       +        ifa = self.interface
       +        self.assertEqual(('fork_conflict', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
       +        self.assertEqual(self.interface.q.qsize(), 0)
       +
       +    def test_can_connect_during_backward(self):
                blockchain.blockchains = {}
                self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
                def mock_connect(height):
       t@@ -68,7 +82,7 @@ class TestNetwork(unittest.TestCase):
            def mock_fork(self, bad_header):
                return blockchain.Blockchain(self.config, bad_header['block_height'], None)
        
       -    def test_new_chain_false_during_binary(self):
       +    def test_chain_false_during_binary(self):
                blockchain.blockchains = {}
                self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
                mock_connect = lambda height: height == 3
       t@@ -82,38 +96,6 @@ class TestNetwork(unittest.TestCase):
                self.assertEqual(('catchup', 7), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=6)))
                self.assertEqual(self.interface.q.qsize(), 0)
        
       -    @unittest.skip  # FIXME test is broken
       -    def test_new_join(self):
       -        blockchain.blockchains = {7: {'check': lambda bad_header: True}}
       -        self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1, 'check': lambda x: False, 'connect': lambda height: height == 6}})
       -        self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1, 'check': lambda x: True,  'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 4, 'mock': {'binary':1, 'check': lambda x: True, 'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1, 'check': lambda x: True, 'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1, 'check': lambda x: True, 'connect': lambda x: True}})
       -        ifa = self.interface
       -        self.assertEqual(('join', 7), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=6)))
       -        self.assertEqual(self.interface.q.qsize(), 0)
       -
       -    @unittest.skip  # FIXME test is broken
       -    def test_new_reorg(self):
       -        times = 0
       -        def check(header):
       -            nonlocal times
       -            self.assertEqual(header['block_height'], 7)
       -            times += 1
       -            return False
       -        blockchain.blockchains = {7: {'check': check, 'parent': {'check': lambda x: True}}}
       -        self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 7, 'mock': {'backward':1, 'check': lambda x: False, 'connect': lambda height: height == 6}})
       -        self.interface.q.put_nowait({'block_height': 2, 'mock': {'backward':1, 'check': lambda x: 1,  'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 4, 'mock': {'binary':1, 'check': lambda x: 1, 'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1, 'check': lambda x: 1, 'connect': lambda x: False}})
       -        self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1, 'check': lambda x: 1, 'connect': lambda x: True}})
       -        ifa = self.interface
       -        self.assertEqual(('conflict', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7)))
       -        self.assertEqual(self.interface.q.qsize(), 0)
       -        self.assertEqual(times, 1)
        
        if __name__=="__main__":
            constants.set_regtest()