From c2e8188568f856d8c3d283a0054b4b5c53481887 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 9 Jun 2025 20:20:47 +0000 Subject: [PATCH] tests: test_network: add more header chain resolution test cases --- electrum/interface.py | 2 +- tests/test_network.py | 114 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 8cb494f3c..5e572ab0c 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -1111,7 +1111,7 @@ class Interface(Logger): 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( diff --git a/tests/test_network.py b/tests/test_network.py index 0aeaa061d..5668c5612 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -71,7 +71,7 @@ class MockInterface(Interface): async def get_block_header(self, height: int, *, mode: ChainResolutionMode) -> dict: assert self.q.qsize() > 0, (height, mode) item = await self.q.get() - self.logger.debug(f"step with {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 @@ -83,7 +83,7 @@ class MockInterface(Interface): return -class TestNetwork(ElectrumTestCase): +class TestHeaderChainResolution(ElectrumTestCase): @classmethod def setUpClass(cls): @@ -104,10 +104,75 @@ class TestNetwork(ElectrumTestCase): self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.interface = MockInterface(self.config) - # finds forkpoint during binary, new fork + 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 + 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_catchup_already_up_to_date(self): + """Single chain, local chain tip already matches server tip.""" + ifa = self.interface + 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 @@ -119,15 +184,16 @@ class TestNetwork(ElectrumTestCase): 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(8, next_height=7) + res = await ifa.sync_until(ifa.tip, next_height=7) self.assertEqual((CRM.FORK, 8), res) self.assertEqual(ifa.q.qsize(), 0) + self.assertEqual(len(blockchain.blockchains), 2) - # finds forkpoint during backwards, existing fork async def test_can_connect_during_backward(self): """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. - client happens to ask for header at height 5 during backward search (which directly builds on top the existing fork). + - first missing block found during *backward* phase + - is *existing* fork """ ifa = self.interface ifa.tip = 8 @@ -140,14 +206,16 @@ class TestNetwork(ElectrumTestCase): 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(8, next_height=6) + 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) - # finds forkpoint during binary, new fork async def test_chain_false_during_binary(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 3. + - first missing block found during *binary* phase + - is *new* fork """ ifa = self.interface ifa.tip = 8 @@ -163,9 +231,37 @@ class TestNetwork(ElectrumTestCase): 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(8, next_height=6) + 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_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__":