Add explicit LIB

This commit is contained in:
Giacomo Pasini 2025-05-28 16:50:49 +02:00
parent ada1ee2d5a
commit a7cf2c354a
No known key found for this signature in database
GPG Key ID: FC08489D2D895D4B
2 changed files with 96 additions and 20 deletions

View File

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

View File

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