From 78f2019f3526acd39d71781ec8f6eff3f9bb372e Mon Sep 17 00:00:00 2001 From: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com> Date: Mon, 12 May 2025 02:14:51 +0900 Subject: [PATCH] add bootstrap --- cryptarchia/bootstrap.py | 159 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 cryptarchia/bootstrap.py diff --git a/cryptarchia/bootstrap.py b/cryptarchia/bootstrap.py new file mode 100644 index 0000000..84af407 --- /dev/null +++ b/cryptarchia/bootstrap.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +from datetime import datetime, timedelta +from enum import Enum +from typing import Iterator, Optional, TypeAlias + +K = 1024 +T = timedelta(days=1) +DOWNLOAD_LIMIT = 1000 + + +def run_node(tree: BlockTree, peers: list[Peer]): + peers_by_tip = group_peers_by_tip(peers) + # Determine fork choice rule depending on how long the node has been offline. + max_peer_tip = max(peers_by_tip.keys(), key=lambda tip: tip.height) + tree.determine_fork_choice(max_peer_tip) + + # In real impl, these downloads should be run in parallel. + for _, peers in peers_by_tip.items(): + download_blocks(tree, peers[0], None) + + # Downloads are done. Listen for new blocks. + for block, peer in listen_for_new_blocks(): + # Determine fork choice depending on how far behind the node is. + tree.determine_fork_choice(block) + try: + tree.on_block(block) + except ParentNotFound: + if block.height <= tree.latest_immutable_block().height: + continue + download_blocks(tree, peer, block.id) + + +def group_peers_by_tip(peers: list[Peer]) -> dict[Block, list[Peer]]: + peers_by_tip: dict[Block, list[Peer]] = defaultdict(list) + for peer in peers: + peers_by_tip[peer.tip()].append(peer) + return peers_by_tip + + +def download_blocks(tree: BlockTree, peer: Peer, target_block: Optional[BlockId]): + latest_downloaded_block: Optional[Block] = None + while True: + # Recreate a request each time: + # - to download the next batch of blocks. + # - to handle the case where the peer's honest chain has changed (if target_block is None). + known_blocks = [latest_downloaded_block.id] if latest_downloaded_block else [] + known_blocks += [ + tree.tip().id, + tree.latest_immutable_block().id, + tree.genesis_block.id, + ] + req = DownloadBlocksRequest( + target_block, + known_blocks, + ) + + num_downloaded = 0 + for block in peer.download_blocks(req): + num_downloaded += 1 + latest_downloaded_block = block + if block.height <= tree.latest_immutable_block().height: + return + try: + tree.on_block(block) + except Exception: + return + + if num_downloaded < DOWNLOAD_LIMIT: + return + + +def listen_for_new_blocks() -> Iterator[tuple[Block, Peer]]: + # TODO + return iter([]) + + +@dataclass +class Peer: + tree: BlockTree + + def tip(self) -> Block: + return self.tree.tip() + + def download_blocks(self, req: DownloadBlocksRequest) -> Iterator[Block]: + # TODO + return iter([]) + + +BlockId: TypeAlias = bytes + + +@dataclass +class Block: + id: BlockId + height: int + + +@dataclass +class BlockTree: + fork_choice: ForkChoice + genesis_block: Block + + def tip(self) -> Block: + # TODO + return self.genesis_block + + def latest_immutable_block(self) -> Block: + # TODO: Require explicit commit. + # If the fork choice hasn't been switched to ONLINE, no blocks are immutable and we can't rely on K. + # Also, if the fork choice has been switched from ONLINE to BOOTSTRAP, we can't rely on K. + return self.genesis_block + + def determine_fork_choice(self, received_block: Block): + self.fork_choice = self.fork_choice.update( + received_block.height, self.tip().height + ) + + def on_block(self, block: Block): + pass + + +@dataclass +class ForkChoice: + rule: ForkChoiceRule + start_time: datetime + + def update(self, received_block_height: int, local_tip_height: int) -> ForkChoice: + # If the node has been offline while more than K blocks were being created, switch to BOOTSTRAP. + # It means that the node is behind the k-block of the peer. + if received_block_height - local_tip_height >= K: + return ForkChoice(rule=ForkChoiceRule.BOOTSTRAP, start_time=datetime.now()) + + # If not, basically keep the current rule. + # But, if bootstrap time has passed, switch to ONLINE. + if ( + self.rule == ForkChoiceRule.BOOTSTRAP + and datetime.now() - self.start_time > T + ): + return ForkChoice(rule=ForkChoiceRule.ONLINE, start_time=datetime.now()) + else: + return self + + +class ForkChoiceRule(Enum): + BOOTSTRAP = 1 + ONLINE = 2 + + +class ParentNotFound(Exception): + pass + + +@dataclass +class DownloadBlocksRequest: + target_block: Optional[BlockId] + known_blocks: list[BlockId]