From adaeba2493711ef40e200a8dda5b863a7f963c78 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Fri, 1 Nov 2024 20:36:45 +0400 Subject: [PATCH] cryptarchia/ghost: impl GHOST fork choice rule --- cryptarchia/cryptarchia.py | 46 ++++++++ cryptarchia/test_fork_choice.py | 198 +++++++++++++++++++++++++++++++- 2 files changed, 243 insertions(+), 1 deletion(-) diff --git a/cryptarchia/cryptarchia.py b/cryptarchia/cryptarchia.py index b01f139..6a88f85 100644 --- a/cryptarchia/cryptarchia.py +++ b/cryptarchia/cryptarchia.py @@ -6,6 +6,7 @@ import itertools import functools from dataclasses import dataclass, field, replace import logging +from collections import defaultdict import numpy as np @@ -720,6 +721,51 @@ def chain_density( return density +def block_children(states: Dict[Id, LedgerState]) -> Dict[Id, set[Id]]: + children = defaultdict(set) + for c, state in states.items(): + children[state.block.parent].add(c) + + return children + + +def block_weight(states: Dict[Id, LedgerState]) -> Dict[Id, int]: + children = block_children(states) + + block_weight = {} + + pending = {b for b in states if len(children[b]) == 0} + ready = set() + + while len(pending) > 0: + new_ready = set() + for b in pending: + if children[b] <= ready: + block_weight[b] = 1 + sum(block_weight[c] for c in children[b]) + new_ready.add(b) + + for b in new_ready: + pending.remove(b) + + if states[b].block.parent in states: + pending.add(states[b].block.parent) + + ready.add(b) + + return block_weight + + +def ghost_fork_choice(finalized: Id, states: Dict[Id, LedgerState]) -> Id: + weights = block_weight(states) + children = block_children(states) + + tip = finalized + while len(children[tip]) > 0: + tip = max(children[tip], key=lambda c: weights[c]) + + return tip + + # Implementation of the fork choice rule as defined in the Ouroboros Genesis paper # k defines the forking depth of chain we accept without more analysis # s defines the length of time (unit of slots) after the fork happened we will inspect for chain density diff --git a/cryptarchia/test_fork_choice.py b/cryptarchia/test_fork_choice.py index 03c30d8..135b9e1 100644 --- a/cryptarchia/test_fork_choice.py +++ b/cryptarchia/test_fork_choice.py @@ -5,6 +5,8 @@ import hashlib from copy import deepcopy from cryptarchia.cryptarchia import ( + ghost_fork_choice, + block_weight, maxvalid_bg, BlockHeader, Slot, @@ -20,8 +22,202 @@ from .test_common import mk_chain, mk_config, mk_genesis_state, mk_block class TestForkChoice(TestCase): - def test_common_prefix_depth(self): + def test_ghost_fork_choice(self): + # Example from the GHOST paper + # + # 2D - 3F - 4C - 5B + # / + # / 3E + # / / + # 1B - 2C - 3D - 4B + # / \ \ + # 0 \ 3C + # \ \ + # \ 2B - 3B + # \ + # 1A - 2A - 3A - 4A - 5A - 6A + coin = Coin(sk=1, value=100) + + b0 = BlockHeader(slot=Slot(0), parent=bytes(32)) + + b1A = mk_block(b0, 1, coin, content=b"b1A") + b2A = mk_block(b1A, 2, coin, content=b"b2A") + b3A = mk_block(b2A, 3, coin, content=b"b3A") + b4A = mk_block(b3A, 4, coin, content=b"b4A") + b5A = mk_block(b4A, 5, coin, content=b"b5A") + b6A = mk_block(b5A, 6, coin, content=b"b6A") + b1B = mk_block(b0, 1, coin, content=b"b1B") + b2B = mk_block(b1B, 2, coin, content=b"b2B") + b3B = mk_block(b2B, 3, coin, content=b"b3B") + b2C = mk_block(b1B, 2, coin, content=b"b2C") + b3C = mk_block(b2C, 3, coin, content=b"b3C") + b2D = mk_block(b1B, 2, coin, content=b"b2D") + b3D = mk_block(b2C, 3, coin, content=b"b3D") + b4B = mk_block(b3D, 4, coin, content=b"b4B") + b3E = mk_block(b2C, 3, coin, content=b"b3E") + b3F = mk_block(b2D, 3, coin, content=b"b3F") + b4C = mk_block(b3F, 4, coin, content=b"b4C") + b5B = mk_block(b4C, 5, coin, content=b"b5B") + + states = { + b.id(): LedgerState(block=b) + for b in [ + b0, + b1A, + b2A, + b3A, + b4A, + b5A, + b6A, + b1B, + b2B, + b3B, + b4B, + b5B, + b2C, + b3C, + b4C, + b2D, + b3D, + b3E, + b3F, + ] + } + + tip = ghost_fork_choice(b0.id(), states) + assert tip == b4B.id() + + tip = ghost_fork_choice(b1A.id(), states) + assert tip == b6A.id() + + def test_block_weight_paper(self): + # Example from the GHOST paper + # + # 2D - 3F - 4C - 5B + # / + # / 3E + # / / + # 1B - 2C - 3D - 4B + # / \ \ + # 0 \ 3C + # \ \ + # \ 2B - 3B + # \ + # 1A - 2A - 3A - 4A - 5A - 6A + + coin = Coin(sk=1, value=100) + + b0 = BlockHeader(slot=Slot(0), parent=bytes(32)) + + b1A = mk_block(b0, 1, coin, content=b"b1A") + b2A = mk_block(b1A, 2, coin, content=b"b2A") + b3A = mk_block(b2A, 3, coin, content=b"b3A") + b4A = mk_block(b3A, 4, coin, content=b"b4A") + b5A = mk_block(b4A, 5, coin, content=b"b5A") + b6A = mk_block(b5A, 6, coin, content=b"b6A") + b1B = mk_block(b0, 1, coin, content=b"b1B") + b2B = mk_block(b1B, 2, coin, content=b"b2B") + b3B = mk_block(b2B, 3, coin, content=b"b3B") + b2C = mk_block(b1B, 2, coin, content=b"b2C") + b3C = mk_block(b2C, 3, coin, content=b"b3C") + b2D = mk_block(b1B, 2, coin, content=b"b2D") + b3D = mk_block(b2C, 3, coin, content=b"b3D") + b4B = mk_block(b3D, 4, coin, content=b"b4B") + b3E = mk_block(b2C, 3, coin, content=b"b3E") + b3F = mk_block(b2D, 3, coin, content=b"b3F") + b4C = mk_block(b3F, 4, coin, content=b"b4C") + b5B = mk_block(b4C, 5, coin, content=b"b5B") + + states = { + b.id(): LedgerState(block=b) + for b in [ + b0, + b1A, + b2A, + b3A, + b4A, + b5A, + b6A, + b1B, + b2B, + b3B, + b4B, + b5B, + b2C, + b3C, + b4C, + b2D, + b3D, + b3E, + b3F, + ] + } + + weight = block_weight(states) + + expected_weight = { + b0.id(): 19, + b1A.id(): 6, + b2A.id(): 5, + b3A.id(): 4, + b4A.id(): 3, + b5A.id(): 2, + b6A.id(): 1, + b1B.id(): 12, + b2B.id(): 2, + b3B.id(): 1, + b4B.id(): 1, + b5B.id(): 1, + b2C.id(): 5, + b3C.id(): 1, + b4C.id(): 2, + b2D.id(): 4, + b3D.id(): 2, + b3E.id(): 1, + b3F.id(): 3, + } + + assert weight == expected_weight + + def test_block_weight(self): + # 6 - 7 + # / + # 0 - 1 - 2 - 3 + # \ + # 4 - 5 + + coin = Coin(sk=1, value=100) + + b0 = BlockHeader(slot=Slot(0), parent=bytes(32)) + b1 = mk_block(b0, 1, coin) + b2 = mk_block(b1, 2, coin) + b3 = mk_block(b2, 3, coin) + b4 = mk_block(b0, 1, coin, content=b"b4") + b5 = mk_block(b4, 2, coin) + b6 = mk_block(b2, 3, coin, content=b"b6") + b7 = mk_block(b6, 4, coin) + + states = { + b.id(): LedgerState(block=b) for b in [b0, b1, b2, b3, b4, b5, b6, b7] + } + + weights = block_weight(states) + + expected_weights = { + b0.id(): 8, + b1.id(): 5, + b2.id(): 4, + b3.id(): 1, + b4.id(): 2, + b5.id(): 1, + b6.id(): 2, + b7.id(): 1, + } + + assert weights == expected_weights, weights + + def test_common_prefix_depth(self): # 6 - 7 # / # 0 - 1 - 2 - 3