electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 7221fb3231a736dc5d3ebd5ed5e0723c9b8203f2
parent b3a2bce213451036ed49e4af4780ae2bbb90ac30
Author: SomberNight <somber.night@protonmail.com>
Date:   Mon, 17 Sep 2018 22:21:55 +0200

interface: further simplifications for fork resolution

Diffstat:
Melectrum/interface.py | 57++++++++++++++++++++++++++++++++++-----------------------
Melectrum/tests/test_network.py | 54++++++++++++++++++------------------------------------
2 files changed, 52 insertions(+), 59 deletions(-)

diff --git a/electrum/interface.py b/electrum/interface.py @@ -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 @@ -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)) @@ -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(): diff --git a/electrum/tests/test_network.py b/electrum/tests/test_network.py @@ -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): @@ -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): @@ -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 @@ -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()