mirror of
https://github.com/logos-blockchain/logos-blockchain-specs.git
synced 2026-01-05 22:53:11 +00:00
160 lines
4.6 KiB
Python
160 lines
4.6 KiB
Python
|
|
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]
|