Add Boostrapping/Online modes

Add Boostrapping and Online modes to cryptarchia, including
relevant tests. The Boostrap mode uses the Genesis fc rule, while
Online uses Praos. Swtitching between the two rules is left to
the implementation and is specified in the public Notion as linked
in the comment
This commit is contained in:
Giacomo Pasini 2025-05-28 12:43:06 +02:00
parent 2c5c3860f0
commit ada1ee2d5a
No known key found for this signature in database
GPG Key ID: FC08489D2D895D4B
2 changed files with 116 additions and 8 deletions

View File

@ -7,6 +7,7 @@ from dataclasses import dataclass, field, replace
from hashlib import blake2b, sha256
from math import floor
from typing import Dict, Generator, List, TypeAlias
from enum import Enum
import numpy as np
@ -308,6 +309,9 @@ class EpochState:
def nonce(self) -> bytes:
return self.nonce_snapshot.nonce
class State(Enum):
ONLINE = 1
BOOTSTRAPPING = 2
class Follower:
def __init__(self, genesis_state: LedgerState, config: Config):
@ -317,6 +321,17 @@ class Follower:
self.genesis_state = genesis_state
self.ledger_state = {genesis_state.block.id(): genesis_state.copy()}
self.epoch_state = {}
self.state = State.BOOTSTRAPPING
def to_online(self):
"""
Call this method when the follower has finished bootstrapping. While this is somewhat left to implementations
https://www.notion.so/Cryptarchia-v1-Bootstrapping-Synchronization-1fd261aa09df81ac94b5fb6a4eff32a6 contains a great deal
of information and is the reference for the Rust implementation.
"""
if self.state != State.BOOTSTRAPPING:
raise RuntimeError("Follower is not in BOOTSTRAPPING state")
self.state = State.ONLINE
def validate_header(self, block: BlockHeader):
# TODO: verify blocks are not in the 'future'
@ -368,13 +383,23 @@ class Follower:
# Evaluate the fork choice rule and return the chain we should be following
def fork_choice(self) -> Hash:
return maxvalid_bg(
self.local_chain,
self.forks,
k=self.config.k,
s=self.config.s,
states=self.ledger_state,
)
if self.state == State.BOOTSTRAPPING:
return maxvalid_bg(
self.local_chain,
self.forks,
k=self.config.k,
s=self.config.s,
states=self.ledger_state,
)
elif self.state == State.ONLINE:
return maxvalid_mc(
self.local_chain,
self.forks,
k=self.config.k,
states=self.ledger_state,
)
else:
raise RuntimeError(f"Unknown follower state: {self.state}")
def tip(self) -> BlockHeader:
return self.tip_state().block
@ -592,7 +617,7 @@ def block_children(states: Dict[Hash, LedgerState]) -> Dict[Hash, set[Hash]]:
return children
# Implementation of the Cryptarchia fork choice rule (following Ouroborous Genesis).
# Implementation of the Ouroboros Genesis fork choice rule.
# The fork choice has two phases:
# 1. if the chain is not forking too deeply, we apply the longest chain fork choice rule
# 2. otherwise we look at the chain density immidiately following the fork
@ -633,6 +658,33 @@ def maxvalid_bg(
return cmax
# Implementation of the Ouroboros Praos fork choice rule.
# The fork choice has two phases:
# 1. if the chain is not forking too deeply, we apply the longest chain fork choice rule
# 2. otherwise we discard the fork
#
# k defines the forking depth of a chain at which point we switch phases.
def maxvalid_mc(
local_chain: Hash,
forks: List[Hash],
k: int,
states: Dict[Hash, LedgerState],
) -> Hash:
assert type(local_chain) == Hash, type(local_chain)
assert all(type(f) == Hash for f in forks)
cmax = local_chain
for fork in forks:
cmax_depth, cmax_suffix, fork_depth, fork_suffix = common_prefix_depth(
cmax, fork, states
)
if cmax_depth <= k:
# Longest chain fork choice rule
if cmax_depth < fork_depth:
cmax = fork
return cmax
class ParentNotFound(Exception):
def __str__(self):
return "Parent not found"

View File

@ -3,6 +3,7 @@ from unittest import TestCase
from copy import deepcopy
from cryptarchia.cryptarchia import (
maxvalid_bg,
maxvalid_mc,
Slot,
Note,
Follower,
@ -200,6 +201,11 @@ class TestForkChoice(TestCase):
== short_chain[-1].id()
)
assert (
maxvalid_mc(short_chain[-1].id(), [long_chain[-1].id()], k,states)
== short_chain[-1].id()
)
# However, if we set k to the fork length, it will be accepted
k = len(long_chain)
assert (
@ -207,6 +213,11 @@ class TestForkChoice(TestCase):
== long_chain[-1].id()
)
assert (
maxvalid_mc(short_chain[-1].id(), [long_chain[-1].id()], k, states)
== long_chain[-1].id()
)
def test_fork_choice_long_dense_chain(self):
# The longest chain is also the densest after the fork
short_note, long_note = Note(sk=0, value=100), Note(sk=1, value=100)
@ -235,6 +246,13 @@ class TestForkChoice(TestCase):
== long_chain[-1].id()
)
# praos fc rule should not accept a chain that diverged more than k blocks,
# even if it is longer
assert (
maxvalid_mc(short_chain[-1].id(), [long_chain[-1].id()], k, states)
== short_chain[-1].id()
)
def test_fork_choice_integration(self):
n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
notes = [n_a, n_b]
@ -281,3 +299,41 @@ class TestForkChoice(TestCase):
assert follower.tip_id() == b4.id()
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
# /
# b1
# \
# b3 - b4 == tip
#
b5 = mk_block(b2, 3, n_a)
b6 = mk_block(b5, 4, n_a)
follower.on_block(b5)
follower.on_block(b6)
assert follower.tip_id() == b4.id()
assert len(follower.forks) == 1 and follower.forks[0] == b6.id()
# -- extend the main chain shallower than k --
#
#
# b2 - b5 - b6
# /
# b1
# \
# b3 - b4
# \
# - - b7 - b8 == tip
b7 = mk_block(b3, 4, n_b)
b8 = mk_block(b7, 5, n_b)
follower.on_block(b7)
follower.on_block(b8)
assert follower.tip_id() == b8.id()
assert len(follower.forks) == 2 and {b6.id(), b4.id()}.issubset(follower.forks)