1
0

Merge pull request #9930 from SomberNight/202506_iface_headers_backwards

interface: start headers backwards search with small delta
This commit is contained in:
ghost43
2025-06-10 12:25:05 +00:00
committed by GitHub
5 changed files with 215 additions and 88 deletions

View File

@@ -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']

View File

@@ -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!')

View File

@@ -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

View File

@@ -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']

View File

@@ -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__":