diff --git a/electrum/blockchain.py b/electrum/blockchain.py index c65dba1a7..5d101a796 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -626,7 +626,7 @@ class Blockchain(Logger): work_in_last_partial_chunk = (height % CHUNK_SIZE + 1) * work_in_single_header return running_total + work_in_last_partial_chunk - def can_connect(self, header: dict, check_height: bool=True) -> bool: + def can_connect(self, header: dict, *, check_height: bool = True) -> bool: if header is None: return False height = header['block_height'] diff --git a/electrum/interface.py b/electrum/interface.py index 5515161dc..5e572ab0c 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -1052,27 +1052,26 @@ class Interface(Logger): ) header = await self.get_block_header(height, mode=ChainResolutionMode.CATCHUP) - chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header) + chain = blockchain.check_header(header) if chain: - self.blockchain = chain if isinstance(chain, Blockchain) else self.blockchain + self.blockchain = chain # note: there is an edge case here that is not handled. # we might know the blockhash (enough for check_header) but # not have the header itself. e.g. regtest chain with only genesis. # this situation resolves itself on the next block return ChainResolutionMode.CATCHUP, height+1 - can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height) + can_connect = blockchain.can_connect(header) if not can_connect: self.logger.info(f"can't connect new block: {height=}") height, header, bad, bad_header = await self._search_headers_backwards(height, header=header) - chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header) - can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height) + chain = blockchain.check_header(header) + can_connect = blockchain.can_connect(header) assert chain or can_connect if can_connect: height += 1 - if isinstance(can_connect, Blockchain): # not when mocking - self.blockchain = can_connect - self.blockchain.save_header(header) + self.blockchain = can_connect + self.blockchain.save_header(header) return ChainResolutionMode.CATCHUP, height good, bad, bad_header = await self._search_headers_binary(height, bad, bad_header, chain) @@ -1088,7 +1087,7 @@ class Interface(Logger): 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 + self.blockchain = chain good = height while True: assert 0 <= good < bad, (good, bad) @@ -1098,9 +1097,9 @@ class Interface(Logger): await self._maybe_warm_headers_cache( from_height=good, to_height=bad, mode=ChainResolutionMode.BINARY) header = await self.get_block_header(height, mode=ChainResolutionMode.BINARY) - chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header) + chain = blockchain.check_header(header) if chain: - self.blockchain = chain if isinstance(chain, Blockchain) else self.blockchain + self.blockchain = chain good = height else: bad = height @@ -1108,13 +1107,11 @@ class Interface(Logger): if good + 1 == bad: break - 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: + if not self.blockchain.can_connect(bad_header, check_height=False): raise Exception('unexpected bad header during binary: {}'.format(bad_header)) _assert_header_does_not_check_against_any_chain(bad_header) - self.logger.info(f"binary search exited. good {good}, bad {bad}") + self.logger.info(f"binary search exited. good {good}, bad {bad}. {chain=}") return good, bad, bad_header async def _resolve_potential_chain_fork_given_forkpoint( @@ -1139,8 +1136,7 @@ class Interface(Logger): # this is a new fork we don't yet have height = bad + 1 self.logger.info(f"new fork at bad height {bad}") - forkfun = self.blockchain.fork if 'mock' not in bad_header else bad_header['mock']['fork'] - b = forkfun(bad_header) # type: Blockchain + b = self.blockchain.fork(bad_header) # type: Blockchain self.blockchain = b assert b.forkpoint == bad return ChainResolutionMode.FORK, height @@ -1158,8 +1154,8 @@ class Interface(Logger): height = constants.net.max_checkpoint() checkp = True header = await self.get_block_header(height, mode=ChainResolutionMode.BACKWARD) - chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header) - can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height) + chain = blockchain.check_header(header) + can_connect = blockchain.can_connect(header) if chain or can_connect: return False if checkp: @@ -1169,18 +1165,18 @@ class Interface(Logger): bad, bad_header = height, header _assert_header_does_not_check_against_any_chain(bad_header) with blockchain.blockchains_lock: chains = list(blockchain.blockchains.values()) - local_max = max([0] + [x.height() for x in chains]) if 'mock' not in header else float('inf') + local_max = max([0] + [x.height() for x in chains]) height = min(local_max + 1, height - 1) assert height >= 0 await self._maybe_warm_headers_cache( from_height=max(0, height-10), to_height=height, mode=ChainResolutionMode.BACKWARD) + delta = 2 while await iterate(): bad, bad_header = height, header - delta = self.tip - height # FIXME why compared to tip? would be easier to cache if delta started at 1 - assert delta > 0, delta - height = self.tip - 2 * delta + height -= delta + delta *= 2 _assert_header_does_not_check_against_any_chain(bad_header) self.logger.info(f"exiting backward mode at {height}") @@ -1420,7 +1416,7 @@ class Interface(Logger): def _assert_header_does_not_check_against_any_chain(header: dict) -> None: - chain_bad = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header) + chain_bad = blockchain.check_header(header) if chain_bad: raise Exception('bad_header must not check!') diff --git a/tests/__init__.py b/tests/__init__.py index 4fac041ca..624a3e3e0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -19,7 +19,7 @@ from electrum.logging import Logger FAST_TESTS = False -electrum.logging._configure_stderr_logging() +electrum.logging._configure_stderr_logging(verbosity="*") electrum.util.AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = True diff --git a/tests/test_blockchain.py b/tests/test_blockchain.py index 3a52ac654..aaf6ca306 100644 --- a/tests/test_blockchain.py +++ b/tests/test_blockchain.py @@ -224,7 +224,7 @@ class TestBlockchain(ElectrumTestCase): self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_l.path()) self.assertEqual(11 * 80, os.stat(chain_l.path()).st_size) for b in (chain_u, chain_l): - self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())])) + self.assertTrue(all([b.can_connect(b.read_header(i), check_height=False) for i in range(b.height())])) self._append_header(chain_u, self.HEADERS['S']) self._append_header(chain_u, self.HEADERS['T']) @@ -259,7 +259,7 @@ class TestBlockchain(ElectrumTestCase): self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab"), chain_u.path()) self.assertEqual(7 * 80, os.stat(chain_u.path()).st_size) for b in (chain_u, chain_l, chain_z): - self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())])) + self.assertTrue(all([b.can_connect(b.read_header(i), check_height=False) for i in range(b.height())])) self.assertEqual(constants.net.GENESIS, chain_z.get_hash(0)) self.assertEqual(hash_header(self.HEADERS['F']), chain_z.get_hash(5)) @@ -334,7 +334,7 @@ class TestBlockchain(ElectrumTestCase): self.assertEqual(hash_header(self.HEADERS['X']), chain_z.get_hash(11)) for b in (chain_u, chain_l, chain_z): - self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())])) + self.assertTrue(all([b.can_connect(b.read_header(i), check_height=False) for i in range(b.height())])) def get_chains_that_contain_header_helper(self, header: dict): height = header['block_height'] diff --git a/tests/test_network.py b/tests/test_network.py index 4ff5132c9..5668c5612 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,6 +1,7 @@ import asyncio import tempfile import unittest +from typing import List from electrum import constants from electrum.simple_config import SimpleConfig @@ -16,6 +17,42 @@ from . import ElectrumTestCase CRM = ChainResolutionMode +class MockBlockchain: + + def __init__(self, headers: List[str]): + self._headers = headers + self.forkpoint = len(headers) + + def height(self) -> int: + return len(self._headers) - 1 + + def save_header(self, header: dict) -> None: + assert header['block_height'] == self.height()+1, f"new {header['block_height']=}, cur {self.height()=}" + self._headers.append(header['mock']['id']) + + def check_header(self, header: dict) -> bool: + return header['mock']['id'] in self._headers + + def can_connect(self, header: dict, *, check_height: bool = True) -> bool: + height = header['block_height'] + if check_height and self.height() != height - 1: + return False + if self.check_header(header): + return True + return header['mock']['prev_id'] in self._headers + + def fork(parent, header: dict) -> 'MockBlockchain': + if not parent.can_connect(header, check_height=False): + raise Exception("forking header does not connect to parent chain") + forkpoint = header.get('block_height') + self = MockBlockchain(parent._headers[:forkpoint]) + self.save_header(header) + chain_id = header['mock']['id'] + with blockchain.blockchains_lock: + blockchain.blockchains[chain_id] = self + return self + + class MockNetwork: def __init__(self, config: SimpleConfig): @@ -30,15 +67,11 @@ class MockInterface(Interface): network = MockNetwork(config) super().__init__(network=network, server=ServerAddr.from_str('mock-server:50000:t')) self.q = asyncio.Queue() - self.blockchain = blockchain.Blockchain(config=self.config, forkpoint=0, - parent=None, forkpoint_hash=constants.net.GENESIS, prev_hash=None) - self.tip = 12 - self.blockchain._size = self.tip + 1 async def get_block_header(self, height: int, *, mode: ChainResolutionMode) -> dict: assert self.q.qsize() > 0, (height, mode) item = await self.q.get() - print("step with height", height, item) + self.logger.debug(f"step with {height=}. {mode=}. will get {item=}") assert item['block_height'] == height, (item['block_height'], height) assert mode in item['mock'], (mode, item) return item @@ -50,7 +83,7 @@ class MockInterface(Interface): return -class TestNetwork(ElectrumTestCase): +class TestHeaderChainResolution(ElectrumTestCase): @classmethod def setUpClass(cls): @@ -62,75 +95,173 @@ class TestNetwork(ElectrumTestCase): super().tearDownClass() constants.BitcoinMainnet.set_as_network() + def tearDown(self): + blockchain.blockchains = {} + super().tearDown() + async def asyncSetUp(self): await super().asyncSetUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.interface = MockInterface(self.config) - async def test_fork_noconflict(self): - blockchain.blockchains = {} - self.interface.q.put_nowait({'block_height': 8, 'mock': {CRM.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': {CRM.BACKWARD:1,'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}}) - self.interface.q.put_nowait({'block_height': 2, 'mock': {CRM.BACKWARD:1,'check':lambda x: True, 'connect': lambda x: False}}) - self.interface.q.put_nowait({'block_height': 4, 'mock': {CRM.BINARY:1,'check':lambda x: True, 'connect': lambda x: True}}) - self.interface.q.put_nowait({'block_height': 5, 'mock': {CRM.BINARY:1,'check':lambda x: True, 'connect': lambda x: True}}) - self.interface.q.put_nowait({'block_height': 6, 'mock': {CRM.BINARY:1,'check':lambda x: True, 'connect': lambda x: True}}) + async def test_catchup_one_block_behind(self): + """Single chain, but client is behind. The client's height is 5, server is on block 6. + - first missing block found during *catchup* phase + """ ifa = self.interface - res = await ifa.sync_until(8, next_height=7) - self.assertEqual((CRM.FORK, 8), res) - self.assertEqual(self.interface.q.qsize(), 0) + ifa.tip = 6 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + } + ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.CATCHUP:1, 'id': '06a', 'prev_id': '05a'}}) + res = await ifa.sync_until(ifa.tip) + self.assertEqual((CRM.CATCHUP, 7), res) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 1) - async def test_fork_conflict(self): - blockchain.blockchains = {7: {'check': lambda bad_header: False}} - self.interface.q.put_nowait({'block_height': 8, 'mock': {CRM.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': {CRM.BACKWARD:1,'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}}) - self.interface.q.put_nowait({'block_height': 2, 'mock': {CRM.BACKWARD:1,'check':lambda x: True, 'connect': lambda x: False}}) - self.interface.q.put_nowait({'block_height': 4, 'mock': {CRM.BINARY:1,'check':lambda x: True, 'connect': lambda x: True}}) - self.interface.q.put_nowait({'block_height': 5, 'mock': {CRM.BINARY:1,'check':lambda x: True, 'connect': lambda x: True}}) - self.interface.q.put_nowait({'block_height': 6, 'mock': {CRM.BINARY:1,'check':lambda x: True, 'connect': lambda x: True}}) + async def test_catchup_already_up_to_date(self): + """Single chain, local chain tip already matches server tip.""" ifa = self.interface - res = await ifa.sync_until(8, next_height=7) + ifa.tip = 5 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + } + ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.CATCHUP:1, 'id': '05a', 'prev_id': '04a'}}) + res = await ifa.sync_until(ifa.tip) + self.assertEqual((CRM.CATCHUP, 6), res) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 1) + + async def test_catchup_client_ahead_of_lagging_server(self): + """Single chain, server is lagging.""" + ifa = self.interface + ifa.tip = 3 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + } + ifa.q.put_nowait({'block_height': 3, 'mock': {CRM.CATCHUP:1, 'id': '03a', 'prev_id': '02a'}}) + res = await ifa.sync_until(ifa.tip) + self.assertEqual((CRM.CATCHUP, 4), res) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 1) + + async def test_catchup_fast_forward(self): + """Single chain, but client is behind. The client's height is 5, server is already on block 12. + - first missing block found during *backward* phase + """ + ifa = self.interface + ifa.tip = 12 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + } + ifa.q.put_nowait({'block_height': 12, 'mock': {CRM.CATCHUP:1, 'id': '12a', 'prev_id': '11a'}}) + ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.BACKWARD:1, 'id': '06a', 'prev_id': '05a'}}) + ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.CATCHUP: 1, 'id': '07a', 'prev_id': '06a'}}) + ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP: 1, 'id': '08a', 'prev_id': '07a'}}) + ifa.q.put_nowait({'block_height': 9, 'mock': {CRM.CATCHUP: 1, 'id': '09a', 'prev_id': '08a'}}) + res = await ifa.sync_until(ifa.tip, next_height=9) + self.assertEqual((CRM.CATCHUP, 10), res) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 1) + + async def test_fork(self): + """client starts on main chain, has no knowledge of any fork. + server is on other side of chain split, the last common block is height 6. + - first missing block found during *binary* phase + - is *new* fork + """ + ifa = self.interface + ifa.tip = 8 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a", "06a", "07a", "08a", "09a", "10a", "11a", "12a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + } + ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'id': '08b', 'prev_id': '07b'}}) + ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'id': '07b', 'prev_id': '06a'}}) + ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.BACKWARD:1, 'id': '05a', 'prev_id': '04a'}}) + ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.BINARY:1, 'id': '06a', 'prev_id': '05a'}}) + res = await ifa.sync_until(ifa.tip, next_height=7) self.assertEqual((CRM.FORK, 8), res) - self.assertEqual(self.interface.q.qsize(), 0) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 2) async def test_can_connect_during_backward(self): - blockchain.blockchains = {} - self.interface.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'check': lambda x: False, 'connect': lambda x: False}}) - def mock_connect(height): - return height == 2 - self.interface.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}}) - self.interface.q.put_nowait({'block_height': 2, 'mock': {CRM.BACKWARD:1, 'check': lambda x: False, 'connect': mock_connect, 'fork': self.mock_fork}}) - self.interface.q.put_nowait({'block_height': 3, 'mock': {CRM.CATCHUP:1, 'check': lambda x: False, 'connect': lambda x: True}}) - self.interface.q.put_nowait({'block_height': 4, 'mock': {CRM.CATCHUP:1, 'check': lambda x: False, 'connect': lambda x: True}}) + """client starts on main chain. client already knows about another fork, which has local height 4. + server is on that fork but has more blocks. + - first missing block found during *backward* phase + - is *existing* fork + """ ifa = self.interface - res = await ifa.sync_until(8, next_height=4) - self.assertEqual((CRM.CATCHUP, 5), res) - self.assertEqual(self.interface.q.qsize(), 0) - - def mock_fork(self, bad_header): - forkpoint = bad_header['block_height'] - b = blockchain.Blockchain(config=self.config, forkpoint=forkpoint, parent=None, - forkpoint_hash=sha256(str(forkpoint)).hex(), prev_hash=sha256(str(forkpoint-1)).hex()) - return b + ifa.tip = 8 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a", "06a", "07a", "08a", "09a", "10a", "11a", "12a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + "03b": MockBlockchain(["00a", "01a", "02a", "03b", "04b"]), + } + ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'id': '08b', 'prev_id': '07b'}}) + ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'id': '07b', 'prev_id': '06b'}}) + ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.BACKWARD:1, 'id': '05b', 'prev_id': '04b'}}) + ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.CATCHUP:1, 'id': '06b', 'prev_id': '05b'}}) + res = await ifa.sync_until(ifa.tip, next_height=6) + self.assertEqual((CRM.CATCHUP, 7), res) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 2) async def test_chain_false_during_binary(self): - blockchain.blockchains = {} - self.interface.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'check': lambda x: False, 'connect': lambda x: False}}) - mock_connect = lambda height: height == 3 - self.interface.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'check': lambda x: False, 'connect': mock_connect}}) - self.interface.q.put_nowait({'block_height': 2, 'mock': {CRM.BACKWARD:1, 'check': lambda x: True, 'connect': mock_connect}}) - self.interface.q.put_nowait({'block_height': 4, 'mock': {CRM.BINARY:1, 'check': lambda x: False, 'fork': self.mock_fork, 'connect': mock_connect}}) - self.interface.q.put_nowait({'block_height': 3, 'mock': {CRM.BINARY:1, 'check': lambda x: True, 'connect': lambda x: True}}) - self.interface.q.put_nowait({'block_height': 5, 'mock': {CRM.CATCHUP:1, 'check': lambda x: False, 'connect': lambda x: True}}) - self.interface.q.put_nowait({'block_height': 6, 'mock': {CRM.CATCHUP:1, 'check': lambda x: False, 'connect': lambda x: True}}) + """client starts on main chain, has no knowledge of any fork. + server is on other side of chain split, the last common block is height 3. + - first missing block found during *binary* phase + - is *new* fork + """ ifa = self.interface - res = await ifa.sync_until(8, next_height=6) + ifa.tip = 8 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a", "06a", "07a", "08a", "09a", "10a", "11a", "12a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + } + ifa.q.put_nowait({'block_height': 8, 'mock': {CRM.CATCHUP:1, 'id': '08b', 'prev_id': '07b'}}) + ifa.q.put_nowait({'block_height': 7, 'mock': {CRM.BACKWARD:1, 'id': '07b', 'prev_id': '06b'}}) + ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.BACKWARD:1, 'id': '05b', 'prev_id': '04b'}}) + ifa.q.put_nowait({'block_height': 1, 'mock': {CRM.BACKWARD:1, 'id': '01a', 'prev_id': '00a'}}) + ifa.q.put_nowait({'block_height': 3, 'mock': {CRM.BINARY:1, 'id': '03a', 'prev_id': '02a'}}) + ifa.q.put_nowait({'block_height': 4, 'mock': {CRM.BINARY:1, 'id': '04b', 'prev_id': '03a'}}) + ifa.q.put_nowait({'block_height': 5, 'mock': {CRM.CATCHUP:1, 'id': '05b', 'prev_id': '04b'}}) + ifa.q.put_nowait({'block_height': 6, 'mock': {CRM.CATCHUP:1, 'id': '06b', 'prev_id': '05b'}}) + res = await ifa.sync_until(ifa.tip, next_height=6) self.assertEqual((CRM.CATCHUP, 7), res) - self.assertEqual(self.interface.q.qsize(), 0) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 2) + + async def test_chain_true_during_binary(self): + """client starts on main chain. client already knows about another fork, which has local height 10. + server is on that fork but has more blocks. + - first missing block found during *binary* phase + - is *existing* fork + """ + ifa = self.interface + ifa.tip = 20 + ifa.blockchain = MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a", "06a", "07a", "08a", "09a", "10a", "11a", "12a", "13a", "14a"]) + blockchain.blockchains = { + "00a": ifa.blockchain, + "07b": MockBlockchain(["00a", "01a", "02a", "03a", "04a", "05a", "06a", "07b", "08b", "09b", "10b"]), + } + ifa.q.put_nowait({'block_height': 20, 'mock': {CRM.CATCHUP:1, 'id': '20b', 'prev_id': '19b'}}) + ifa.q.put_nowait({'block_height': 15, 'mock': {CRM.BACKWARD:1, 'id': '15b', 'prev_id': '14b'}}) + ifa.q.put_nowait({'block_height': 13, 'mock': {CRM.BACKWARD:1, 'id': '13b', 'prev_id': '12b'}}) + ifa.q.put_nowait({'block_height': 9, 'mock': {CRM.BACKWARD:1, 'id': '09b', 'prev_id': '08b'}}) + ifa.q.put_nowait({'block_height': 11, 'mock': {CRM.BINARY:1, 'id': '11b', 'prev_id': '10b'}}) + ifa.q.put_nowait({'block_height': 10, 'mock': {CRM.BINARY:1, 'id': '10b', 'prev_id': '09b'}}) + ifa.q.put_nowait({'block_height': 11, 'mock': {CRM.CATCHUP:1, 'id': '11b', 'prev_id': '10b'}}) + ifa.q.put_nowait({'block_height': 12, 'mock': {CRM.CATCHUP:1, 'id': '12b', 'prev_id': '11b'}}) + ifa.q.put_nowait({'block_height': 13, 'mock': {CRM.CATCHUP:1, 'id': '13b', 'prev_id': '12b'}}) + res = await ifa.sync_until(ifa.tip, next_height=13) + self.assertEqual((CRM.CATCHUP, 14), res) + self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 2) if __name__ == "__main__":