diff --git a/cryptarchia/cryptarchia.py b/cryptarchia/cryptarchia.py index 6a88f85..1981e69 100644 --- a/cryptarchia/cryptarchia.py +++ b/cryptarchia/cryptarchia.py @@ -464,6 +464,10 @@ class Follower: return True def on_block(self, block: BlockHeader): + if block.id() in self.ledger_state: + logger.warning("dropping already processed block") + return + if not self.validate_header(block): logger.warning("invalid header") return @@ -513,12 +517,12 @@ class Follower: # Evaluate the fork choice rule and return the chain we should be following def fork_choice(self) -> Id: - return maxvalid_bg( + return ghost_maxvalid_bg( self.local_chain, self.forks, - self.ledger_state, k=self.config.k, s=self.config.s, + states=self.ledger_state, ) def tip(self) -> BlockHeader: @@ -673,7 +677,7 @@ def iter_chain(tip: Id, states: Dict[Id, LedgerState]): def chain_suffix(tip: Id, n: int, states: Dict[Id, LedgerState]) -> list[LedgerState]: - return reversed(list(itertools.islice(iter_chain(tip, states), n))) + return list(reversed(list(itertools.islice(iter_chain(tip, states), n)))) def common_prefix_depth(a: Id, b: Id, states: Dict[Id, LedgerState]) -> (int, int): @@ -755,49 +759,42 @@ def block_weight(states: Dict[Id, LedgerState]) -> Dict[Id, int]: return block_weight -def ghost_fork_choice(finalized: Id, states: Dict[Id, LedgerState]) -> Id: - weights = block_weight(states) - children = block_children(states) - - tip = finalized - while len(children[tip]) > 0: - tip = max(children[tip], key=lambda c: weights[c]) - - return tip - - # Implementation of the fork choice rule as defined in the Ouroboros Genesis paper # k defines the forking depth of chain we accept without more analysis # s defines the length of time (unit of slots) after the fork happened we will inspect for chain density -def maxvalid_bg( +def ghost_maxvalid_bg( local_chain: Id, forks: List[Id], - states: Dict[Id, LedgerState], k: int, s: int, + states: Dict[Id, LedgerState], ) -> Id: assert type(local_chain) == Id assert all(type(f) == Id for f in forks) + weights = block_weight(states) + cmax = local_chain for fork in forks: - local_depth, fork_depth = common_prefix_depth(cmax, fork, states) - if local_depth <= k: + cmax_depth, fork_depth = common_prefix_depth(cmax, fork, states) + if cmax_depth <= k: + cmax_divergent_block = chain_suffix(cmax, cmax_depth, states)[0].block.id() + fork_divergent_block = chain_suffix(fork, fork_depth, states)[0].block.id() # Classic longest chain rule with parameter k - if local_depth < fork_depth: + if weights[cmax_divergent_block] < weights[fork_divergent_block]: cmax = fork else: # The chain is forking too much, we need to pay a bit more attention # In particular, select the chain that is the densest after the fork forking_block = local_chain - for _ in range(local_depth): + for _ in range(cmax_depth): forking_block = states[forking_block].block.parent forking_slot = Slot(states[forking_block].block.slot.absolute_slot + s) - cmax_density = chain_density(cmax, forking_slot, local_depth, states) - candidate_density = chain_density(fork, forking_slot, fork_depth, states) + cmax_density = chain_density(cmax, forking_slot, cmax_depth, states) + fork_density = chain_density(fork, forking_slot, fork_depth, states) - if cmax_density < candidate_density: + if cmax_density < fork_density: cmax = fork return cmax diff --git a/cryptarchia/test_fork_choice.py b/cryptarchia/test_fork_choice.py index 135b9e1..5eaae15 100644 --- a/cryptarchia/test_fork_choice.py +++ b/cryptarchia/test_fork_choice.py @@ -5,9 +5,8 @@ import hashlib from copy import deepcopy from cryptarchia.cryptarchia import ( - ghost_fork_choice, block_weight, - maxvalid_bg, + ghost_maxvalid_bg, BlockHeader, Slot, Id, @@ -85,11 +84,14 @@ class TestForkChoice(TestCase): ] } - tip = ghost_fork_choice(b0.id(), states) - assert tip == b4B.id() + assert (d := common_prefix_depth(b5B.id(), b3E.id(), states)) == (4, 2), d - tip = ghost_fork_choice(b1A.id(), states) - assert tip == b6A.id() + k = 100 + s = int(3 * k / 0.05) + tip = ghost_maxvalid_bg( + b5B.id(), [b3E.id(), b4B.id(), b3C.id(), b3B.id(), b6A.id()], k, s, states + ) + assert tip == b4B.id() def test_block_weight_paper(self): # Example from the GHOST paper @@ -292,14 +294,14 @@ class TestForkChoice(TestCase): states = {b.id(): LedgerState(block=b) for b in short_chain + long_chain} assert ( - maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], states, k, s) + ghost_maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states) == short_chain[-1].id() ) # However, if we set k to the fork length, it will be accepted k = len(long_chain) assert ( - maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], states, k, s) + ghost_maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states) == long_chain[-1].id() ) @@ -327,7 +329,7 @@ class TestForkChoice(TestCase): states = {b.id(): LedgerState(block=b) for b in short_chain + long_chain} assert ( - maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], states, k, s) + ghost_maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states) == long_chain[-1].id() ) diff --git a/cryptarchia/test_ledger_state_update.py b/cryptarchia/test_ledger_state_update.py index 1047335..d71c7aa 100644 --- a/cryptarchia/test_ledger_state_update.py +++ b/cryptarchia/test_ledger_state_update.py @@ -18,6 +18,26 @@ from .test_common import mk_config, mk_block, mk_genesis_state class TestLedgerStateUpdate(TestCase): + def test_on_block_idempotent(self): + leader_coin = Coin(sk=0, value=100) + genesis = mk_genesis_state([leader_coin]) + + follower = Follower(genesis, mk_config([leader_coin])) + + block = mk_block(slot=0, parent=genesis.block, coin=leader_coin) + follower.on_block(block) + + # Follower should have accepted the block + assert follower.tip_state().height == 1 + assert follower.tip() == block + + follower.on_block(block) + + assert follower.tip_state().height == 1 + assert follower.tip() == block + assert len(follower.ledger_state) == 2 + assert len(follower.forks) == 0 + def test_ledger_state_prevents_coin_reuse(self): leader_coin = Coin(sk=0, value=100) genesis = mk_genesis_state([leader_coin]) @@ -35,7 +55,7 @@ class TestLedgerStateUpdate(TestCase): assert follower.tip_state().verify_unspent(leader_coin.nullifier()) == False reuse_coin_block = mk_block(slot=1, parent=block, coin=leader_coin) - follower.on_block(block) + follower.on_block(reuse_coin_block) # Follower should *not* have accepted the block assert follower.tip_state().height == 1 diff --git a/cryptarchia/test_orphaned_proofs.py b/cryptarchia/test_orphaned_proofs.py index 23ffb51..5cce7ff 100644 --- a/cryptarchia/test_orphaned_proofs.py +++ b/cryptarchia/test_orphaned_proofs.py @@ -5,7 +5,6 @@ import hashlib from copy import deepcopy from cryptarchia.cryptarchia import ( - maxvalid_bg, BlockHeader, Slot, Id,