2026-07-30 18:51:15 +02:00

128 lines
4.3 KiB
Python

"""Block tree, latency-driven forks, and honest longest-chain fork choice.
Blocks are stored in parallel arrays (id == index). A virtual genesis is block 0 at
slot -1, height 0. Every real block is produced at an active slot by one winning node
and points at the best tip *visible to that node at production time*, which is what makes
network latency (and same-slot multi-winners) produce forks.
Fork choice is honest longest-chain with a first-seen tie-break (prefer higher height,
then earlier slot, then lower id) — no adversary is modelled, so the spec's density /
deep-fork rules never engage.
"""
from __future__ import annotations
import heapq
from dataclasses import dataclass
import numpy as np
from .latency import LatencyModel
GENESIS = 0
@dataclass
class BlockTree:
slot: np.ndarray # int64, slot of each block (genesis = -1)
parent: np.ndarray # int64, parent id (genesis = -1)
height: np.ndarray # int64, chain height (genesis = 0)
leader: np.ndarray # int64, producing node id (genesis = -1)
uncles: list[tuple[int, ...]] # referenced uncle ids per block (filled later)
@property
def n_blocks(self) -> int:
return self.slot.shape[0]
def ancestors(self, block_id: int) -> list[int]:
"""Ancestor chain of ``block_id`` from itself down to (excluding) genesis."""
out: list[int] = []
b = block_id
while b > GENESIS:
out.append(b)
b = int(self.parent[b])
return out
def canonical_chain(self) -> list[int]:
"""Honest longest-chain: ancestors of the best tip over the whole tree.
Returns real block ids (genesis excluded), tip-first.
"""
tip = self._best_over_all()
return self.ancestors(tip)
def _rank(self, bid: int) -> tuple[int, int, int]:
# Preference order for "better tip": higher height, earlier slot, lower id.
return (int(self.height[bid]), -int(self.slot[bid]), -bid)
def _best_over_all(self) -> int:
best = GENESIS
best_rank = self._rank(GENESIS)
for bid in range(1, self.n_blocks):
r = self._rank(bid)
if r > best_rank:
best_rank, best = r, bid
return best
def build_tree(
active_slots: np.ndarray,
winners_per_slot: list[np.ndarray],
latency: LatencyModel,
rng: np.random.Generator,
) -> BlockTree:
"""Construct the block tree from grouped lottery winners under a latency model."""
# Preallocate with genesis in slot 0.
slot = [-1]
parent = [-1]
height = [0]
leader = [-1]
# global_best = best publicly-visible tip so far, as (height, slot, id).
def better(a: tuple[int, int, int], b: tuple[int, int, int]) -> tuple[int, int, int]:
# higher height, then earlier slot, then lower id
ah, as_, ai = a
bh, bs, bi = b
if ah != bh:
return a if ah > bh else b
if as_ != bs:
return a if as_ < bs else b
return a if ai < bi else b
global_best = (0, -1, GENESIS)
own_best: dict[int, tuple[int, int, int]] = {}
# min-heap of (visible_at, block_id) awaiting public visibility
pending: list[tuple[int, int]] = []
next_id = 1
for si in range(active_slots.shape[0]):
t = int(active_slots[si])
# advance visibility frontier to slot t
while pending and pending[0][0] <= t:
_, bid = heapq.heappop(pending)
cand = (height[bid], slot[bid], bid)
global_best = better(global_best, cand)
for v in winners_per_slot[si].tolist():
gb = global_best
ob = own_best.get(v, (0, -1, GENESIS))
chosen = better(gb, ob)
p_id = chosen[2]
h = chosen[0] + 1
bid = next_id
next_id += 1
slot.append(t)
parent.append(p_id)
height.append(h)
leader.append(v)
own_best[v] = (h, t, bid)
va = latency.visible_at(t, rng)
heapq.heappush(pending, (va, bid))
return BlockTree(
slot=np.asarray(slot, np.int64),
parent=np.asarray(parent, np.int64),
height=np.asarray(height, np.int64),
leader=np.asarray(leader, np.int64),
uncles=[() for _ in range(next_id)],
)