From a7cf2c354a4188e6d1d63e08d8c631d32c84d3f4 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Wed, 28 May 2025 16:50:49 +0200 Subject: [PATCH] Add explicit LIB --- cryptarchia/cryptarchia.py | 43 ++++++++++++++++++- cryptarchia/test_fork_choice.py | 73 +++++++++++++++++++++++++-------- 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/cryptarchia/cryptarchia.py b/cryptarchia/cryptarchia.py index 7311240..6932e45 100644 --- a/cryptarchia/cryptarchia.py +++ b/cryptarchia/cryptarchia.py @@ -1,5 +1,5 @@ import functools -import itertools +from itertools import islice import logging from collections import defaultdict from copy import deepcopy @@ -322,6 +322,7 @@ class Follower: self.ledger_state = {genesis_state.block.id(): genesis_state.copy()} self.epoch_state = {} self.state = State.BOOTSTRAPPING + self.lib = genesis_state.block.id() # Last immutable block, initially the genesis block def to_online(self): """ @@ -332,12 +333,17 @@ class Follower: if self.state != State.BOOTSTRAPPING: raise RuntimeError("Follower is not in BOOTSTRAPPING state") self.state = State.ONLINE + self.update_lib() def validate_header(self, block: BlockHeader): # TODO: verify blocks are not in the 'future' if block.parent not in self.ledger_state: raise ParentNotFound + if not is_ancestor(self.lib, block.parent, self.ledger_state): + # If the block is not an ancestor of the last immutable block, we cannot process it. + raise ImmutableFork + current_state = self.ledger_state[block.parent].copy() epoch_state = self.compute_epoch_state( @@ -381,6 +387,27 @@ class Follower: self.forks.remove(new_tip) self.local_chain = new_tip + if self.state == State.ONLINE: + self.update_lib() + + + # Update the lib, and prune forks that do not descend from it. + def update_lib(self): + """ + Computes the last immutable block, which is the k-th block in the chain. + The last immutable block is the block that is guaranteed to be part of the chain + and will not be reverted. + """ + if self.state != State.ONLINE: + return + # prune forks that do not descend from the last immutable block, this is needed to avoid Genesis rule to roll back + # past the LIB + self.lib = next(islice(iter_chain(self.local_chain, self.ledger_state), self.config.k, None), self.local_chain).block.id() + self.forks = [ + f for f in self.forks if is_ancestor(self.lib, f, self.ledger_state) + ] + + # Evaluate the fork choice rule and return the chain we should be following def fork_choice(self) -> Hash: if self.state == State.BOOTSTRAPPING: @@ -555,6 +582,14 @@ def iter_chain_blocks( for state in iter_chain(tip, states): yield state.block +def is_ancestor(a: Hash, b: Hash, states: Dict[Hash, LedgerState]) -> bool: + """ + Returns True if `a` is an ancestor of `b` in the chain. + """ + for state in iter_chain(b, states): + if state.block.id() == a: + return True + return False def common_prefix_depth( a: Hash, b: Hash, states: Dict[Hash, LedgerState] @@ -675,7 +710,7 @@ def maxvalid_mc( cmax = local_chain for fork in forks: - cmax_depth, cmax_suffix, fork_depth, fork_suffix = common_prefix_depth( + cmax_depth, _, fork_depth, _ = common_prefix_depth( cmax, fork, states ) if cmax_depth <= k: @@ -694,6 +729,10 @@ class InvalidLeaderProof(Exception): def __str__(self): return "Invalid leader proof" +class ImmutableFork(Exception): + def __str__(self): + return "Block is forking from the last immutable block" + if __name__ == "__main__": pass diff --git a/cryptarchia/test_fork_choice.py b/cryptarchia/test_fork_choice.py index 0a59dd3..c850f78 100644 --- a/cryptarchia/test_fork_choice.py +++ b/cryptarchia/test_fork_choice.py @@ -6,9 +6,11 @@ from cryptarchia.cryptarchia import ( maxvalid_mc, Slot, Note, + State, Follower, common_prefix_depth, LedgerState, + ImmutableFork, ) from .test_common import mk_chain, mk_config, mk_genesis_state, mk_block @@ -301,39 +303,74 @@ class TestForkChoice(TestCase): assert len(follower.forks) == 1 and follower.forks[0] == b2.id(), follower.forks # -- switch to online mode -- - follower.to_online() - - # -- extend a fork deeper than k -- # - # - # b2 - b5 - b6 + # b2 (does not descend from the LIB and is thus pruned) # / # b1 # \ - # b3 - b4 == tip + # b3 (LIB) - b4 == tip # - b5 = mk_block(b2, 3, n_a) - b6 = mk_block(b5, 4, n_a) - follower.on_block(b5) - follower.on_block(b6) + follower.to_online() + assert follower.lib == b3.id(), follower.lib + assert len(follower.forks) == 0, follower.forks + assert b2.id() not in follower.forks - assert follower.tip_id() == b4.id() - assert len(follower.forks) == 1 and follower.forks[0] == b6.id() + # -- extend a fork deeper than the LIB -- + # + # - - - - - - b5 + # / + # b1 + # \ + # b3 (LIB) - b4 == tip + # + b5 = mk_block(b1, 4, n_a) + with self.assertRaises(ImmutableFork): + follower.on_block(b5) # -- extend the main chain shallower than k -- # - # - # b2 - b5 - b6 - # / # b1 # \ - # b3 - b4 + # b3 - b4 (pruned) # \ - # - - b7 - b8 == tip + # - - b7 (LIB) - b8 == tip b7 = mk_block(b3, 4, n_b) b8 = mk_block(b7, 5, n_b) follower.on_block(b7) + assert len(follower.forks) == 1 and b7.id() in follower.forks + follower.on_block(b8) assert follower.tip_id() == b8.id() - assert len(follower.forks) == 2 and {b6.id(), b4.id()}.issubset(follower.forks) \ No newline at end of file + # b4 was pruned as it forks deeper than the LIB + assert len(follower.forks) == 0, follower.forks + + # Even in bootstrap mode, the follower should not accept blocks that fork deeper than k + follower.state = State.BOOTSTRAPPING + with self.assertRaises(ImmutableFork): + follower.on_block(b5) + + # But it should switch a chain diverging more than k as long as it + # descends from the LIB + # + # b1 + # \ + # b3 - - - - - - - b10 - b11 - b12 + # \ | + # - - b7 (LIB) - b8 - b9 == tip + b8 = mk_block(b7, 5, n_b) + b9 = mk_block(b8, 6, n_b) + b10 = mk_block(b7, 7, n_a) + b11 = mk_block(b10, 8, n_a) + b12 = mk_block(b11, 9, n_a) + follower.on_block(b8) + follower.on_block(b9) + + assert follower.tip_id() == b9.id() + + follower.on_block(b10) + follower.on_block(b11) + follower.on_block(b12) + + assert follower.tip_id() == b12.id() + assert follower.lib == b7.id(), follower.lib \ No newline at end of file