mirror of
https://github.com/logos-blockchain/logos-blockchain-specs.git
synced 2026-01-10 00:53:08 +00:00
add bootstrap
This commit is contained in:
parent
2c5c3860f0
commit
78f2019f35
159
cryptarchia/bootstrap.py
Normal file
159
cryptarchia/bootstrap.py
Normal file
@ -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]
|
||||
Loading…
x
Reference in New Issue
Block a user