diff --git a/cryptarchia/sync/full_sync.py b/cryptarchia/sync/full_sync.py index afb25c3..5ba7363 100644 --- a/cryptarchia/sync/full_sync.py +++ b/cryptarchia/sync/full_sync.py @@ -7,7 +7,7 @@ SLOT_TOLERANCE = 1 def full_sync(local: Follower, remotes: list[Follower], start_slot: Slot): - while groups := group_sync_targets(remotes, start_slot): + while groups := group_targets(remotes, start_slot): for _, group in groups.items(): remote = group[0] range_sync(local, remote, start_slot, remote.tip().slot) @@ -19,7 +19,7 @@ def range_sync(local: Follower, remote: Follower, from_slot: Slot, to_slot: Slot local.on_block(block) -def group_sync_targets( +def group_targets( targets: list[Follower], start_slot: Slot ) -> dict[Id, list[Follower]]: groups: dict[Id, list[Follower]] = defaultdict(list) diff --git a/cryptarchia/sync/test_full_sync.py b/cryptarchia/sync/test_full_sync.py index f796d45..2538d7e 100644 --- a/cryptarchia/sync/test_full_sync.py +++ b/cryptarchia/sync/test_full_sync.py @@ -1,3 +1,4 @@ +from copy import deepcopy from unittest import TestCase from cryptarchia.cryptarchia import Coin, Follower @@ -5,8 +6,8 @@ from cryptarchia.sync.full_sync import full_sync from cryptarchia.test_common import mk_block, mk_config, mk_genesis_state -class TestRangeSync(TestCase): - def test_no_fork(self): +class TestFullSync(TestCase): + def test_sync_single_chain(self): # b0 - b1 - b2 coin = Coin(sk=0, value=10) config = mk_config([coin]) @@ -22,5 +23,173 @@ class TestRangeSync(TestCase): new_follower = Follower(genesis, config) full_sync(new_follower, [follower], genesis.block.slot) - assert new_follower.tip() == b2 - assert new_follower.forks == [] + assert new_follower.tip() == follower.tip() + assert new_follower.forks == follower.forks + + def test_continue_syncing_single_chain(self): + # b0 - b1 - b2 + coin = Coin(sk=0, value=10) + config = mk_config([coin]) + genesis = mk_genesis_state([coin]) + follower = Follower(genesis, config) + b0, coin = mk_block(genesis.block, 1, coin), coin.evolve() + b1, coin = mk_block(b0, 2, coin), coin.evolve() + b2, coin = mk_block(b1, 3, coin), coin.evolve() + for b in [b0, b1, b2]: + follower.on_block(b) + assert follower.tip() == b2 + assert follower.forks == [] + + new_follower = deepcopy(follower) + + # follower grows + # b0 - b1 - b2 - b3 - b4 + b3, coin = mk_block(b2, 4, coin), coin.evolve() + b4, coin = mk_block(b3, 5, coin), coin.evolve() + for b in [b3, b4]: + follower.on_block(b) + assert follower.tip() == b4 + + # new_follower starts syncing from its tip slot + full_sync(new_follower, [follower], new_follower.tip().slot) + assert new_follower.tip() == follower.tip() + assert new_follower.forks == follower.forks + + def test_sync_forks(self): + # b0 - b1 - b2 == tip + # \ + # b3 - b4 + c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) + config = mk_config([c_a, c_b]) + genesis = mk_genesis_state([c_a, c_b]) + follower = Follower(genesis, config) + b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve() + b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() + b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() + b3, c_b = mk_block(b0, 2, c_b), c_b.evolve() + b4, c_b = mk_block(b3, 3, c_b), c_b.evolve() + for b in [b0, b1, b2, b3, b4]: + follower.on_block(b) + assert follower.tip() == b2 + assert follower.forks == [b4.id()] + + new_follower = Follower(genesis, config) + full_sync(new_follower, [follower], genesis.block.slot) + assert new_follower.tip() == follower.tip() + assert new_follower.forks == follower.forks + + def test_continue_syncing_forks(self): + # b0 - b1 - b2 == tip + # \ + # b3 - b4 + c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) + config = mk_config([c_a, c_b]) + genesis = mk_genesis_state([c_a, c_b]) + follower = Follower(genesis, config) + b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve() + b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() + b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() + b3, c_b = mk_block(b0, 2, c_b), c_b.evolve() + b4, c_b = mk_block(b3, 3, c_b), c_b.evolve() + for b in [b0, b1, b2, b3, b4]: + follower.on_block(b) + assert follower.tip() == b2 + assert follower.forks == [b4.id()] + + new_follower = deepcopy(follower) + + # all forks grow. the tip is switched. + # b0 - b1 - b2 - b5 + # \ + # b3 - b4 - b6 - b7 == tip + b5, c_a = mk_block(b2, 4, c_a), c_a.evolve() + b6, c_b = mk_block(b4, 4, c_b), c_b.evolve() + b7, c_b = mk_block(b6, 5, c_b), c_b.evolve() + for b in [b5, b6, b7]: + follower.on_block(b) + assert follower.tip() == b7 + assert follower.forks == [b5.id()] + + # new_follower starts syncing from its tip slot + full_sync(new_follower, [follower], new_follower.tip().slot) + assert new_follower.tip() == follower.tip() + assert new_follower.forks == follower.forks + + def test_sync_two_different_trees(self): + c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) + config = mk_config([c_a, c_b]) + genesis = mk_genesis_state([c_a, c_b]) + + # Peer 0 + # b0 - b1 - b2 + peer_0 = Follower(genesis, config) + b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve() + b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() + b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() + for b in [b0, b1, b2]: + peer_0.on_block(b) + assert peer_0.tip() == b2 + assert peer_0.forks == [] + + # Peer 1 + # b0 - b3 - b4 - b5 + peer_1 = Follower(genesis, config) + b0, c_b = mk_block(genesis.block, 1, c_b), c_b.evolve() + b3, c_b = mk_block(b0, 2, c_b), c_b.evolve() + b4, c_b = mk_block(b3, 3, c_b), c_b.evolve() + b5, c_b = mk_block(b4, 3, c_b), c_b.evolve() + for b in [b0, b3, b4, b5]: + peer_1.on_block(b) + assert peer_1.tip() == b5 + assert peer_1.forks == [] + + # new_follower should have: + # b0 - b1 - b2 + # \ + # b3 - b4 - b5 == tip + new_follower = Follower(genesis, config) + full_sync(new_follower, [peer_0, peer_1], genesis.block.slot) + assert new_follower.tip() == peer_1.tip() + assert new_follower.forks == [peer_0.tip_id()] + + def test_ignore_blocks_missing_parents(self): + c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) + config = mk_config([c_a, c_b]) + genesis = mk_genesis_state([c_a, c_b]) + + # Peer 0 + # b0 - b1 - b2 + peer_0 = Follower(genesis, config) + b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve() + b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() + b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() + for b in [b0, b1, b2]: + peer_0.on_block(b) + assert peer_0.tip() == b2 + assert peer_0.forks == [] + + # Peer 1 + # b0 - b3 - b4 - b5 + peer_1 = Follower(genesis, config) + b0, c_b = mk_block(genesis.block, 1, c_b), c_b.evolve() + b3, c_b = mk_block(b0, 2, c_b), c_b.evolve() + b4, c_b = mk_block(b3, 3, c_b), c_b.evolve() + b5, c_b = mk_block(b4, 3, c_b), c_b.evolve() + for b in [b0, b3, b4, b5]: + peer_1.on_block(b) + assert peer_1.tip() == b5 + assert peer_1.forks == [] + + # new follower syncs the peer 0 first + new_follower = Follower(genesis, config) + full_sync(new_follower, [peer_0], genesis.block.slot) + assert new_follower.tip() == peer_0.tip() + assert new_follower.forks == peer_0.forks + + # new follower tries to sync the peer 1, but from its tip slot (3). + # causing all blocks from the peer 1 to be ignored + # because the peer 1 has a fork different from the peer 0, + # and the new follower hasn't synced the peer 1 fork until the slot 3. + full_sync(new_follower, [peer_1], new_follower.tip().slot) + assert new_follower.tip() == peer_0.tip() + assert new_follower.forks == peer_0.forks