160 lines
4.6 KiB
Python
Raw Normal View History

2025-05-12 02:14:51 +09:00
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]