mirror of
https://github.com/logos-blockchain/logos-blockchain-specs.git
synced 2026-01-02 13:13:06 +00:00
Cryptarchia: rework specification (#116)
* cryptarchia/ghost: prep for move to weight based fork choice * cryptarchia/ghost: remove common_prefix_len helper * cryptarchia/ghost: common_prefix_depth returns depth of both chains * cryptarchia/ghost: fix chain density calculation * cryptarchia/ghost: maxvalid_bg uses block ids rather than chains * cryptarchia/ghost: unimported_orphans returns orphans w.r.t. to tip * cryptarchia/ghost: remove redundant check * cryptarchia/ghost: rewrite unimported_orphan w/ common_prefix_depths * cryptarchia/ghost: validate_header w.r.t. block parent * cryptachia/ghost: rewrite on_block to remove dependency on Chain * cryptarchia/ghost: remove Chain abstraction * cryptarchia/ghost: remove local / fork naming in common_prefix_depth * cryptarchia/ghost: rewrite common_prefix_depth in terms of iter_chain * cryptarchia/ghost: impl GHOST fork choice rule * cryptarchia/ghost: integrate GHOST with maxvalid fork choice * cryptarchia: remove unused imports * cryptarchia: cleanup * cryptarchia: cleanup * cryptarchia: remove height from ledger state * cryptachia/ghost: update fork choice rule comments * cryptarchia: switch back to longest chain * cryptarchia: update tests * cryptarchia: remove debug log
This commit is contained in:
parent
3f3427ee9f
commit
5c64a0bd11
@ -1,11 +1,12 @@
|
||||
from typing import TypeAlias, List, Optional
|
||||
from typing import TypeAlias, List, Dict
|
||||
from hashlib import sha256, blake2b
|
||||
from math import floor
|
||||
from copy import deepcopy
|
||||
from itertools import chain
|
||||
import itertools
|
||||
import functools
|
||||
from dataclasses import dataclass, field, replace
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
|
||||
@ -248,36 +249,13 @@ class BlockHeader:
|
||||
return h.digest()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Chain:
|
||||
blocks: List[BlockHeader]
|
||||
genesis: Id
|
||||
|
||||
def tip_id(self) -> Id:
|
||||
if len(self.blocks) == 0:
|
||||
return self.genesis
|
||||
return self.tip().id()
|
||||
|
||||
def tip(self) -> BlockHeader:
|
||||
return self.blocks[-1]
|
||||
|
||||
def length(self) -> int:
|
||||
return len(self.blocks)
|
||||
|
||||
def block_position(self, block: Id) -> Optional[int]:
|
||||
for i, b in enumerate(self.blocks):
|
||||
if b.id() == block:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerState:
|
||||
"""
|
||||
A snapshot of the ledger state up to some block
|
||||
"""
|
||||
|
||||
block: Id = None
|
||||
block: BlockHeader
|
||||
|
||||
# This nonce is used to derive the seed for the slot leader lottery.
|
||||
# It's updated at every block by hashing the previous nonce with the
|
||||
@ -324,7 +302,7 @@ class LedgerState:
|
||||
return nullifier not in self.nullifiers
|
||||
|
||||
def apply(self, block: BlockHeader):
|
||||
assert block.parent == self.block
|
||||
assert block.parent == self.block.id()
|
||||
|
||||
h = blake2b(digest_size=32)
|
||||
h.update("epoch-nonce".encode(encoding="utf-8"))
|
||||
@ -333,8 +311,8 @@ class LedgerState:
|
||||
h.update(block.slot.encode())
|
||||
|
||||
self.nonce = h.digest()
|
||||
self.block = block.id()
|
||||
for proof in chain(block.orphaned_proofs, [block]):
|
||||
self.block = block
|
||||
for proof in itertools.chain(block.orphaned_proofs, [block]):
|
||||
self.apply_leader_proof(proof.leader_proof)
|
||||
|
||||
def apply_leader_proof(self, proof: MockLeaderProof):
|
||||
@ -385,19 +363,25 @@ class Follower:
|
||||
def __init__(self, genesis_state: LedgerState, config: Config):
|
||||
self.config = config
|
||||
self.forks = []
|
||||
self.local_chain = Chain([], genesis=genesis_state.block)
|
||||
self.local_chain = genesis_state.block.id()
|
||||
self.genesis_state = genesis_state
|
||||
self.ledger_state = {genesis_state.block: genesis_state.copy()}
|
||||
self.ledger_state = {genesis_state.block.id(): genesis_state.copy()}
|
||||
self.epoch_state = {}
|
||||
|
||||
def validate_header(self, block: BlockHeader, chain: Chain) -> bool:
|
||||
def validate_header(self, block: BlockHeader) -> bool:
|
||||
# TODO: verify blocks are not in the 'future'
|
||||
if block.parent != chain.tip_id():
|
||||
logger.warning("block parent is not chain tip")
|
||||
if block.parent not in self.ledger_state:
|
||||
logger.warning("We have not seen block parent")
|
||||
return False
|
||||
|
||||
current_state = self.ledger_state[block.parent].copy()
|
||||
|
||||
# We use the proposed block epoch state to validate orphans as well.
|
||||
# For very old orphans, these states may be different.
|
||||
epoch_state = self.compute_epoch_state(
|
||||
block.slot.epoch(self.config), block.parent
|
||||
)
|
||||
|
||||
# first, we verify adopted leadership transactions
|
||||
for orphan in block.orphaned_proofs:
|
||||
# orphan proofs are checked in two ways
|
||||
@ -410,10 +394,6 @@ class Follower:
|
||||
logger.warning("missing orphan proof")
|
||||
return False
|
||||
|
||||
# we use the proposed block epoch state here instead of the orphan's
|
||||
# epoch state. For very old orphans, these states may be different.
|
||||
epoch_state = self.compute_epoch_state(block.slot.epoch(self.config), chain)
|
||||
|
||||
# (2.) is satisfied by verifying the proof against current state ensuring:
|
||||
# - it is a valid proof
|
||||
# - and the nullifier has not already been spent
|
||||
@ -431,7 +411,6 @@ class Follower:
|
||||
# effects to the ledger state
|
||||
current_state.apply_leader_proof(orphan.leader_proof)
|
||||
|
||||
epoch_state = self.compute_epoch_state(block.slot.epoch(self.config), chain)
|
||||
# TODO: this is not the full block validation spec, only slot leader is verified
|
||||
return self.verify_slot_leader(
|
||||
block.slot,
|
||||
@ -462,129 +441,100 @@ class Follower:
|
||||
and current_state.verify_unspent(proof.nullifier)
|
||||
)
|
||||
|
||||
# Try appending this block to an existing chain and return whether
|
||||
# the operation was successful
|
||||
def try_extend_chains(self, block: BlockHeader) -> Optional[Chain]:
|
||||
if self.tip_id() == block.parent:
|
||||
return self.local_chain
|
||||
|
||||
for chain in self.forks:
|
||||
if chain.tip_id() == block.parent:
|
||||
return chain
|
||||
|
||||
return None
|
||||
|
||||
def try_create_fork(self, block: BlockHeader) -> Optional[Chain]:
|
||||
if self.genesis_state.block == block.parent:
|
||||
# this block is forking off the genesis state
|
||||
return Chain(blocks=[], genesis=self.genesis_state.block)
|
||||
|
||||
chains = self.forks + [self.local_chain]
|
||||
for chain in chains:
|
||||
block_position = chain.block_position(block.parent)
|
||||
if block_position is not None:
|
||||
return Chain(
|
||||
blocks=chain.blocks[: block_position + 1],
|
||||
genesis=self.genesis_state.block,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def on_block(self, block: BlockHeader):
|
||||
# check if the new block extends an existing chain
|
||||
new_chain = self.try_extend_chains(block)
|
||||
if new_chain is None:
|
||||
# we failed to extend one of the existing chains,
|
||||
# therefore we might need to create a new fork
|
||||
new_chain = self.try_create_fork(block)
|
||||
if new_chain is not None:
|
||||
self.forks.append(new_chain)
|
||||
else:
|
||||
logger.warning("missing parent block")
|
||||
# otherwise, we're missing the parent block
|
||||
# in that case, just ignore the block
|
||||
return
|
||||
|
||||
if not self.validate_header(block, new_chain):
|
||||
logger.warning("invalid header")
|
||||
if block.id() in self.ledger_state:
|
||||
logger.warning("dropping already processed block")
|
||||
return
|
||||
|
||||
new_chain.blocks.append(block)
|
||||
|
||||
# We may need to switch forks, lets run the fork choice rule to check.
|
||||
new_chain = self.fork_choice()
|
||||
if new_chain != self.local_chain:
|
||||
self.forks.remove(new_chain)
|
||||
self.forks.append(self.local_chain)
|
||||
self.local_chain = new_chain
|
||||
if not self.validate_header(block):
|
||||
logger.warning("invalid header")
|
||||
return
|
||||
|
||||
new_state = self.ledger_state[block.parent].copy()
|
||||
new_state.apply(block)
|
||||
self.ledger_state[block.id()] = new_state
|
||||
|
||||
def unimported_orphans(self, tip: Id) -> list[BlockHeader]:
|
||||
if block.parent == self.local_chain:
|
||||
# simply extending the local chain
|
||||
self.local_chain = block.id()
|
||||
else:
|
||||
# otherwise, this block creates a fork
|
||||
self.forks.append(block.id())
|
||||
|
||||
# remove any existing fork that is superceded by this block
|
||||
if block.parent in self.forks:
|
||||
self.forks.remove(block.parent)
|
||||
|
||||
# We may need to switch forks, lets run the fork choice rule to check.
|
||||
new_tip = self.fork_choice()
|
||||
self.forks.append(self.local_chain)
|
||||
self.forks.remove(new_tip)
|
||||
self.local_chain = new_tip
|
||||
|
||||
def unimported_orphans(self) -> list[BlockHeader]:
|
||||
"""
|
||||
Returns all unimported orphans w.r.t. the given tip's state.
|
||||
Orphans are returned in the order that they should be imported.
|
||||
"""
|
||||
tip_state = self.ledger_state[tip].copy()
|
||||
tip_state = self.tip_state().copy()
|
||||
tip = tip_state.block.id()
|
||||
|
||||
orphans = []
|
||||
for fork in [self.local_chain, *self.forks]:
|
||||
if fork.block_position(tip) is not None:
|
||||
# the tip is a member of this fork, it doesn't make sense
|
||||
# to take orphans from this fork as they are all already "imported"
|
||||
continue
|
||||
|
||||
for block in fork.blocks:
|
||||
for b in [*block.orphaned_proofs, block]:
|
||||
if b.leader_proof.nullifier not in tip_state.nullifiers:
|
||||
tip_state.nullifiers.add(b.leader_proof.nullifier)
|
||||
orphans += [b]
|
||||
for fork in self.forks:
|
||||
_, fork_depth = common_prefix_depth(tip, fork, self.ledger_state)
|
||||
for block_state in chain_suffix(fork, fork_depth, self.ledger_state):
|
||||
b = block_state.block
|
||||
if b.leader_proof.nullifier not in tip_state.nullifiers:
|
||||
tip_state.nullifiers.add(b.leader_proof.nullifier)
|
||||
orphans += [b]
|
||||
|
||||
return orphans
|
||||
|
||||
# Evaluate the fork choice rule and return the chain we should be following
|
||||
def fork_choice(self) -> Chain:
|
||||
def fork_choice(self) -> Id:
|
||||
return maxvalid_bg(
|
||||
self.local_chain, self.forks, k=self.config.k, s=self.config.s
|
||||
self.local_chain,
|
||||
self.forks,
|
||||
k=self.config.k,
|
||||
s=self.config.s,
|
||||
states=self.ledger_state,
|
||||
)
|
||||
|
||||
def tip(self) -> BlockHeader:
|
||||
return self.local_chain.tip()
|
||||
return self.tip_state().block
|
||||
|
||||
def tip_id(self) -> Id:
|
||||
return self.local_chain.tip_id()
|
||||
return self.local_chain
|
||||
|
||||
def tip_state(self) -> LedgerState:
|
||||
return self.ledger_state[self.tip_id()]
|
||||
|
||||
def state_at_slot_beginning(self, chain: Chain, slot: Slot) -> LedgerState:
|
||||
for block in reversed(chain.blocks):
|
||||
if block.slot < slot:
|
||||
return self.ledger_state[block.id()]
|
||||
|
||||
def state_at_slot_beginning(self, tip: Id, slot: Slot) -> LedgerState:
|
||||
for state in iter_chain(tip, self.ledger_state):
|
||||
if state.block.slot < slot:
|
||||
return state
|
||||
return self.genesis_state
|
||||
|
||||
def epoch_start_slot(self, epoch) -> Slot:
|
||||
return Slot(epoch.epoch * self.config.epoch_length)
|
||||
|
||||
def stake_distribution_snapshot(self, epoch, chain):
|
||||
def stake_distribution_snapshot(self, epoch, tip: Id):
|
||||
# stake distribution snapshot happens at the beginning of the previous epoch,
|
||||
# i.e. for epoch e, the snapshot is taken at the last block of epoch e-2
|
||||
slot = Slot(epoch.prev().epoch * self.config.epoch_length)
|
||||
return self.state_at_slot_beginning(chain, slot)
|
||||
return self.state_at_slot_beginning(tip, slot)
|
||||
|
||||
def nonce_snapshot(self, epoch, chain):
|
||||
def nonce_snapshot(self, epoch, tip):
|
||||
# nonce snapshot happens partway through the previous epoch after the
|
||||
# stake distribution has stabilized
|
||||
slot = Slot(
|
||||
self.config.epoch_relative_nonce_slot
|
||||
+ self.epoch_start_slot(epoch.prev()).absolute_slot
|
||||
)
|
||||
return self.state_at_slot_beginning(chain, slot)
|
||||
return self.state_at_slot_beginning(tip, slot)
|
||||
|
||||
def compute_epoch_state(self, epoch: Epoch, chain: Chain) -> EpochState:
|
||||
def compute_epoch_state(self, epoch: Epoch, tip: Id) -> EpochState:
|
||||
if epoch.epoch == 0:
|
||||
return EpochState(
|
||||
stake_distribution_snapshot=self.genesis_state,
|
||||
@ -592,18 +542,18 @@ class Follower:
|
||||
inferred_total_active_stake=self.config.initial_total_active_stake,
|
||||
)
|
||||
|
||||
stake_distribution_snapshot = self.stake_distribution_snapshot(epoch, chain)
|
||||
nonce_snapshot = self.nonce_snapshot(epoch, chain)
|
||||
stake_distribution_snapshot = self.stake_distribution_snapshot(epoch, tip)
|
||||
nonce_snapshot = self.nonce_snapshot(epoch, tip)
|
||||
|
||||
# we memoize epoch states to avoid recursion killing our performance
|
||||
memo_block_id = nonce_snapshot.block
|
||||
memo_block_id = nonce_snapshot.block.id()
|
||||
if state := self.epoch_state.get((epoch, memo_block_id)):
|
||||
return state
|
||||
|
||||
# To update our inference of total stake, we need the prior estimate which
|
||||
# was calculated last epoch. Thus we recurse here to retreive the previous
|
||||
# estimate of total stake.
|
||||
prev_epoch = self.compute_epoch_state(epoch.prev(), chain)
|
||||
prev_epoch = self.compute_epoch_state(epoch.prev(), tip)
|
||||
inferred_total_active_stake = self._infer_total_active_stake(
|
||||
prev_epoch, nonce_snapshot, stake_distribution_snapshot
|
||||
)
|
||||
@ -696,45 +646,99 @@ class Leader:
|
||||
)
|
||||
|
||||
|
||||
def common_prefix_len(a: Chain, b: Chain) -> int:
|
||||
for i, (x, y) in enumerate(zip(a.blocks, b.blocks)):
|
||||
if x.id() != y.id():
|
||||
return i
|
||||
return min(len(a.blocks), len(b.blocks))
|
||||
def iter_chain(tip: Id, states: Dict[Id, LedgerState]):
|
||||
while tip in states:
|
||||
yield states[tip]
|
||||
tip = states[tip].block.parent
|
||||
|
||||
|
||||
def chain_density(chain: Chain, slot: Slot) -> int:
|
||||
return len(
|
||||
[
|
||||
block
|
||||
for block in chain.blocks
|
||||
if block.slot.absolute_slot < slot.absolute_slot
|
||||
]
|
||||
)
|
||||
def chain_suffix(tip: Id, n: int, states: Dict[Id, LedgerState]) -> list[LedgerState]:
|
||||
return list(reversed(list(itertools.islice(iter_chain(tip, states), n))))
|
||||
|
||||
|
||||
# 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
|
||||
def common_prefix_depth(a: Id, b: Id, states: Dict[Id, LedgerState]) -> (int, int):
|
||||
a_blocks = iter_chain(a, states)
|
||||
b_blocks = iter_chain(b, states)
|
||||
|
||||
seen = {}
|
||||
depth = 0
|
||||
while True:
|
||||
try:
|
||||
a_block = next(a_blocks).block.id()
|
||||
if a_block in seen:
|
||||
# we had seen this block from the fork chain
|
||||
return depth, seen[a_block]
|
||||
|
||||
seen[a_block] = depth
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
try:
|
||||
b_block = next(b_blocks).block.id()
|
||||
if b_block in seen:
|
||||
# we had seen the fork in the local chain
|
||||
return seen[b_block], depth
|
||||
seen[b_block] = depth
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
depth += 1
|
||||
|
||||
assert False
|
||||
|
||||
|
||||
def chain_density(
|
||||
head: Id, slot: Slot, reorg_depth: int, states: Dict[Id, LedgerState]
|
||||
) -> int:
|
||||
assert type(head) == Id
|
||||
chain = iter_chain(head, states)
|
||||
segment = itertools.islice(chain, reorg_depth)
|
||||
return sum(1 for b in segment if b.block.slot < slot)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# Implementation of the Cryptarchia fork choice rule (following Ouroborous Genesis).
|
||||
# The fork choice has two phases:
|
||||
# 1. if the chain is not forking too deeply, we apply the longest chain fork choice rule
|
||||
# 2. otherwise we look at the chain density immidiately following the fork
|
||||
#
|
||||
# k defines the forking depth of a chain at which point we switch phases.
|
||||
# s defines the length of time (unit of slots) after the fork happened we will inspect for chain density
|
||||
def maxvalid_bg(local_chain: Chain, forks: List[Chain], k: int, s: int) -> Chain:
|
||||
def maxvalid_bg(
|
||||
local_chain: Id,
|
||||
forks: List[Id],
|
||||
k: int,
|
||||
s: int,
|
||||
states: Dict[Id, LedgerState],
|
||||
) -> Id:
|
||||
assert type(local_chain) == Id
|
||||
assert all(type(f) == Id for f in forks)
|
||||
|
||||
cmax = local_chain
|
||||
for chain in forks:
|
||||
lowest_common_ancestor = common_prefix_len(cmax, chain)
|
||||
m = cmax.length() - lowest_common_ancestor
|
||||
if m <= k:
|
||||
# Classic longest chain rule with parameter k
|
||||
if cmax.length() < chain.length():
|
||||
cmax = chain
|
||||
for fork in forks:
|
||||
cmax_depth, fork_depth = common_prefix_depth(cmax, fork, states)
|
||||
if cmax_depth <= k:
|
||||
# Longest chain fork choice rule
|
||||
if cmax_depth < fork_depth:
|
||||
cmax = fork
|
||||
else:
|
||||
# The chain is forking too much, we need to pay a bit more attention
|
||||
# In particular, select the chain that is the densest after the fork
|
||||
forking_slot = Slot(
|
||||
cmax.blocks[lowest_common_ancestor].slot.absolute_slot + s
|
||||
)
|
||||
cmax_density = chain_density(cmax, forking_slot)
|
||||
candidate_density = chain_density(chain, forking_slot)
|
||||
if cmax_density < candidate_density:
|
||||
cmax = chain
|
||||
cmax_divergent_block = chain_suffix(cmax, cmax_depth, states)[0].block
|
||||
|
||||
forking_slot = Slot(cmax_divergent_block.slot.absolute_slot + s)
|
||||
cmax_density = chain_density(cmax, forking_slot, cmax_depth, states)
|
||||
fork_density = chain_density(fork, forking_slot, fork_depth, states)
|
||||
|
||||
if cmax_density < fork_density:
|
||||
cmax = fork
|
||||
|
||||
return cmax
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
from .cryptarchia import (
|
||||
Config,
|
||||
TimeConfig,
|
||||
Id,
|
||||
Slot,
|
||||
Coin,
|
||||
BlockHeader,
|
||||
@ -20,19 +18,18 @@ class TestNode:
|
||||
|
||||
def epoch_state(self, slot: Slot):
|
||||
return self.follower.compute_epoch_state(
|
||||
slot.epoch(self.config), self.follower.local_chain
|
||||
slot.epoch(self.config), self.follower.tip_id()
|
||||
)
|
||||
|
||||
def on_slot(self, slot: Slot) -> BlockHeader | None:
|
||||
parent = self.follower.tip_id()
|
||||
epoch_state = self.epoch_state(slot)
|
||||
if leader_proof := self.leader.try_prove_slot_leader(epoch_state, slot, parent):
|
||||
orphans = self.follower.unimported_orphans(parent)
|
||||
self.leader.coin = self.leader.coin.evolve()
|
||||
return BlockHeader(
|
||||
parent=parent,
|
||||
slot=slot,
|
||||
orphaned_proofs=orphans,
|
||||
orphaned_proofs=self.follower.unimported_orphans(),
|
||||
leader_proof=leader_proof,
|
||||
content_size=0,
|
||||
content_id=bytes(32),
|
||||
@ -53,7 +50,15 @@ def mk_config(initial_stake_distribution: list[Coin]) -> Config:
|
||||
|
||||
def mk_genesis_state(initial_stake_distribution: list[Coin]) -> LedgerState:
|
||||
return LedgerState(
|
||||
block=bytes(32),
|
||||
block=BlockHeader(
|
||||
slot=Slot(0),
|
||||
parent=bytes(32),
|
||||
content_size=0,
|
||||
content_id=bytes(32),
|
||||
leader_proof=MockLeaderProof.new(
|
||||
Coin(sk=0, value=0), Slot(0), parent=bytes(32)
|
||||
),
|
||||
),
|
||||
nonce=bytes(32),
|
||||
commitments_spend={c.commitment() for c in initial_stake_distribution},
|
||||
commitments_lead={c.commitment() for c in initial_stake_distribution},
|
||||
@ -62,26 +67,30 @@ def mk_genesis_state(initial_stake_distribution: list[Coin]) -> LedgerState:
|
||||
|
||||
|
||||
def mk_block(
|
||||
parent: Id, slot: int, coin: Coin, content=bytes(32), orphaned_proofs=[]
|
||||
parent: BlockHeader, slot: int, coin: Coin, content=bytes(32), orphaned_proofs=[]
|
||||
) -> BlockHeader:
|
||||
assert len(parent) == 32
|
||||
assert type(parent) == BlockHeader, type(parent)
|
||||
assert type(slot) == int, type(slot)
|
||||
from hashlib import sha256
|
||||
|
||||
return BlockHeader(
|
||||
slot=Slot(slot),
|
||||
parent=parent,
|
||||
parent=parent.id(),
|
||||
content_size=len(content),
|
||||
content_id=sha256(content).digest(),
|
||||
leader_proof=MockLeaderProof.new(coin, Slot(slot), parent=parent),
|
||||
leader_proof=MockLeaderProof.new(coin, Slot(slot), parent=parent.id()),
|
||||
orphaned_proofs=orphaned_proofs,
|
||||
)
|
||||
|
||||
|
||||
def mk_chain(parent, coin: Coin, slots: list[int]) -> tuple[list[BlockHeader], Coin]:
|
||||
def mk_chain(
|
||||
parent: BlockHeader, coin: Coin, slots: list[int]
|
||||
) -> tuple[list[BlockHeader], Coin]:
|
||||
assert type(parent) == BlockHeader
|
||||
chain = []
|
||||
for s in slots:
|
||||
block = mk_block(parent=parent, slot=s, coin=coin)
|
||||
chain.append(block)
|
||||
parent = block.id()
|
||||
parent = block
|
||||
coin = coin.evolve()
|
||||
return chain, coin
|
||||
|
||||
@ -1,40 +1,82 @@
|
||||
from unittest import TestCase
|
||||
from itertools import repeat
|
||||
import numpy as np
|
||||
import hashlib
|
||||
|
||||
from copy import deepcopy
|
||||
from cryptarchia.cryptarchia import (
|
||||
maxvalid_bg,
|
||||
Chain,
|
||||
BlockHeader,
|
||||
Slot,
|
||||
Id,
|
||||
MockLeaderProof,
|
||||
Coin,
|
||||
Follower,
|
||||
common_prefix_depth,
|
||||
LedgerState,
|
||||
)
|
||||
|
||||
from .test_common import mk_chain, mk_config, mk_genesis_state, mk_block
|
||||
|
||||
|
||||
class TestForkChoice(TestCase):
|
||||
def test_common_prefix_depth(self):
|
||||
# 6 - 7
|
||||
# /
|
||||
# 0 - 1 - 2 - 3
|
||||
# \
|
||||
# 4 - 5
|
||||
|
||||
coin = Coin(sk=1, value=100)
|
||||
|
||||
b0 = mk_genesis_state([]).block
|
||||
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]
|
||||
}
|
||||
|
||||
assert (d := common_prefix_depth(b0.id(), b0.id(), states)) == (0, 0), d
|
||||
assert (d := common_prefix_depth(b1.id(), b0.id(), states)) == (1, 0), d
|
||||
assert (d := common_prefix_depth(b0.id(), b1.id(), states)) == (0, 1), d
|
||||
assert (d := common_prefix_depth(b1.id(), b1.id(), states)) == (0, 0), d
|
||||
assert (d := common_prefix_depth(b2.id(), b0.id(), states)) == (2, 0), d
|
||||
assert (d := common_prefix_depth(b0.id(), b2.id(), states)) == (0, 2), d
|
||||
assert (d := common_prefix_depth(b3.id(), b0.id(), states)) == (3, 0), d
|
||||
assert (d := common_prefix_depth(b0.id(), b3.id(), states)) == (0, 3), d
|
||||
assert (d := common_prefix_depth(b1.id(), b4.id(), states)) == (1, 1), d
|
||||
assert (d := common_prefix_depth(b4.id(), b1.id(), states)) == (1, 1), d
|
||||
assert (d := common_prefix_depth(b1.id(), b5.id(), states)) == (1, 2), d
|
||||
assert (d := common_prefix_depth(b5.id(), b1.id(), states)) == (2, 1), d
|
||||
assert (d := common_prefix_depth(b2.id(), b5.id(), states)) == (2, 2), d
|
||||
assert (d := common_prefix_depth(b5.id(), b2.id(), states)) == (2, 2), d
|
||||
assert (d := common_prefix_depth(b3.id(), b5.id(), states)) == (3, 2), d
|
||||
assert (d := common_prefix_depth(b5.id(), b3.id(), states)) == (2, 3), d
|
||||
assert (d := common_prefix_depth(b3.id(), b6.id(), states)) == (1, 1), d
|
||||
assert (d := common_prefix_depth(b6.id(), b3.id(), states)) == (1, 1), d
|
||||
assert (d := common_prefix_depth(b3.id(), b7.id(), states)) == (1, 2), d
|
||||
assert (d := common_prefix_depth(b7.id(), b3.id(), states)) == (2, 1), d
|
||||
assert (d := common_prefix_depth(b5.id(), b7.id(), states)) == (2, 4), d
|
||||
assert (d := common_prefix_depth(b7.id(), b5.id(), states)) == (4, 2), d
|
||||
|
||||
def test_fork_choice_long_sparse_chain(self):
|
||||
# The longest chain is not dense after the fork
|
||||
genesis = mk_genesis_state([]).block
|
||||
|
||||
short_coin, long_coin = Coin(sk=0, value=100), Coin(sk=1, value=100)
|
||||
common, long_coin = mk_chain(parent=bytes(32), coin=long_coin, slots=range(50))
|
||||
common, long_coin = mk_chain(parent=genesis, coin=long_coin, slots=range(50))
|
||||
|
||||
long_chain_sparse_ext, long_coin = mk_chain(
|
||||
parent=common[-1].id(), coin=long_coin, slots=range(50, 100, 2)
|
||||
parent=common[-1], coin=long_coin, slots=range(50, 100, 2)
|
||||
)
|
||||
|
||||
short_chain_dense_ext, _ = mk_chain(
|
||||
parent=common[-1].id(), coin=short_coin, slots=range(50, 100)
|
||||
parent=common[-1], coin=short_coin, slots=range(50, 100)
|
||||
)
|
||||
|
||||
# add more blocks to the long chain to ensure the long chain is indeed longer
|
||||
long_chain_further_ext, _ = mk_chain(
|
||||
parent=long_chain_sparse_ext[-1].id(), coin=long_coin, slots=range(100, 126)
|
||||
parent=long_chain_sparse_ext[-1], coin=long_coin, slots=range(100, 126)
|
||||
)
|
||||
|
||||
long_chain = deepcopy(common) + long_chain_sparse_ext + long_chain_further_ext
|
||||
@ -45,26 +87,34 @@ class TestForkChoice(TestCase):
|
||||
k = 1
|
||||
s = 50
|
||||
|
||||
short_chain = Chain(short_chain, genesis=bytes(32))
|
||||
long_chain = Chain(long_chain, genesis=bytes(32))
|
||||
assert maxvalid_bg(short_chain, [long_chain], k, s) == short_chain
|
||||
states = {b.id(): LedgerState(block=b) for b in short_chain + long_chain}
|
||||
|
||||
assert (
|
||||
maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states)
|
||||
== short_chain[-1].id()
|
||||
)
|
||||
|
||||
# However, if we set k to the fork length, it will be accepted
|
||||
k = long_chain.length()
|
||||
assert maxvalid_bg(short_chain, [long_chain], k, s) == long_chain
|
||||
k = len(long_chain)
|
||||
assert (
|
||||
maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states)
|
||||
== long_chain[-1].id()
|
||||
)
|
||||
|
||||
def test_fork_choice_long_dense_chain(self):
|
||||
# The longest chain is also the densest after the fork
|
||||
short_coin, long_coin = Coin(sk=0, value=100), Coin(sk=1, value=100)
|
||||
common, long_coin = mk_chain(
|
||||
parent=bytes(32), coin=long_coin, slots=range(1, 50)
|
||||
parent=mk_genesis_state([]).block,
|
||||
coin=long_coin,
|
||||
slots=range(1, 50),
|
||||
)
|
||||
|
||||
long_chain_dense_ext, _ = mk_chain(
|
||||
parent=common[-1].id(), coin=long_coin, slots=range(50, 100)
|
||||
parent=common[-1], coin=long_coin, slots=range(50, 100)
|
||||
)
|
||||
short_chain_sparse_ext, _ = mk_chain(
|
||||
parent=common[-1].id(), coin=short_coin, slots=range(50, 100, 2)
|
||||
parent=common[-1], coin=short_coin, slots=range(50, 100, 2)
|
||||
)
|
||||
|
||||
long_chain = deepcopy(common) + long_chain_dense_ext
|
||||
@ -72,9 +122,12 @@ class TestForkChoice(TestCase):
|
||||
|
||||
k = 1
|
||||
s = 50
|
||||
short_chain = Chain(short_chain, genesis=bytes(32))
|
||||
long_chain = Chain(long_chain, genesis=bytes(32))
|
||||
assert maxvalid_bg(short_chain, [long_chain], k, s) == long_chain
|
||||
states = {b.id(): LedgerState(block=b) for b in short_chain + long_chain}
|
||||
|
||||
assert (
|
||||
maxvalid_bg(short_chain[-1].id(), [long_chain[-1].id()], k, s, states)
|
||||
== long_chain[-1].id()
|
||||
)
|
||||
|
||||
def test_fork_choice_integration(self):
|
||||
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10)
|
||||
@ -88,7 +141,7 @@ class TestForkChoice(TestCase):
|
||||
follower.on_block(b1)
|
||||
|
||||
assert follower.tip_id() == b1.id()
|
||||
assert follower.forks == []
|
||||
assert follower.forks == [], follower.forks
|
||||
|
||||
# -- then we fork --
|
||||
#
|
||||
@ -99,14 +152,14 @@ class TestForkChoice(TestCase):
|
||||
# b3
|
||||
#
|
||||
|
||||
b2, c_a = mk_block(b1.id(), 2, c_a), c_a.evolve()
|
||||
b3, c_b = mk_block(b1.id(), 2, c_b), c_b.evolve()
|
||||
b2, c_a = mk_block(b1, 2, c_a), c_a.evolve()
|
||||
b3, c_b = mk_block(b1, 2, c_b), c_b.evolve()
|
||||
|
||||
follower.on_block(b2)
|
||||
follower.on_block(b3)
|
||||
|
||||
assert follower.tip_id() == b2.id()
|
||||
assert len(follower.forks) == 1 and follower.forks[0].tip_id() == b3.id()
|
||||
assert len(follower.forks) == 1 and follower.forks[0] == b3.id()
|
||||
|
||||
# -- extend the fork causing a re-org --
|
||||
#
|
||||
@ -117,8 +170,8 @@ class TestForkChoice(TestCase):
|
||||
# b3 - b4 == tip
|
||||
#
|
||||
|
||||
b4, c_b = mk_block(b3.id(), 3, c_b), c_a.evolve()
|
||||
b4, c_b = mk_block(b3, 3, c_b), c_a.evolve()
|
||||
follower.on_block(b4)
|
||||
|
||||
assert follower.tip_id() == b4.id()
|
||||
assert len(follower.forks) == 1 and follower.forks[0].tip_id() == b2.id()
|
||||
assert len(follower.forks) == 1 and follower.forks[0] == b2.id(), follower.forks
|
||||
|
||||
@ -2,24 +2,15 @@ from unittest import TestCase
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .cryptarchia import (
|
||||
Leader,
|
||||
Config,
|
||||
EpochState,
|
||||
LedgerState,
|
||||
Coin,
|
||||
phi,
|
||||
TimeConfig,
|
||||
Slot,
|
||||
)
|
||||
from .cryptarchia import Leader, EpochState, LedgerState, Coin, phi, Slot
|
||||
from .test_common import mk_config
|
||||
|
||||
|
||||
class TestLeader(TestCase):
|
||||
def test_slot_leader_statistics(self):
|
||||
epoch = EpochState(
|
||||
stake_distribution_snapshot=LedgerState(),
|
||||
nonce_snapshot=LedgerState(nonce=b"1010101010"),
|
||||
stake_distribution_snapshot=LedgerState(block=None),
|
||||
nonce_snapshot=LedgerState(block=None, nonce=b"1010101010"),
|
||||
inferred_total_active_stake=1000,
|
||||
)
|
||||
|
||||
|
||||
@ -2,22 +2,33 @@ from unittest import TestCase
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .cryptarchia import (
|
||||
Follower,
|
||||
TimeConfig,
|
||||
BlockHeader,
|
||||
Config,
|
||||
Coin,
|
||||
LedgerState,
|
||||
MockLeaderProof,
|
||||
Slot,
|
||||
Id,
|
||||
)
|
||||
from .cryptarchia import Follower, Coin, iter_chain
|
||||
|
||||
from .test_common import mk_config, mk_block, mk_genesis_state
|
||||
|
||||
|
||||
class TestLedgerStateUpdate(TestCase):
|
||||
def test_on_block_idempotent(self):
|
||||
leader_coin = Coin(sk=0, value=100)
|
||||
genesis = mk_genesis_state([leader_coin])
|
||||
|
||||
follower = Follower(genesis, mk_config([leader_coin]))
|
||||
|
||||
block = mk_block(slot=0, parent=genesis.block, coin=leader_coin)
|
||||
follower.on_block(block)
|
||||
|
||||
# Follower should have accepted the block
|
||||
assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 2
|
||||
assert follower.tip() == block
|
||||
|
||||
follower.on_block(block)
|
||||
|
||||
# Should have been a No-op
|
||||
assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 2
|
||||
assert follower.tip() == block
|
||||
assert len(follower.ledger_state) == 2
|
||||
assert len(follower.forks) == 0
|
||||
|
||||
def test_ledger_state_prevents_coin_reuse(self):
|
||||
leader_coin = Coin(sk=0, value=100)
|
||||
genesis = mk_genesis_state([leader_coin])
|
||||
@ -28,17 +39,17 @@ class TestLedgerStateUpdate(TestCase):
|
||||
follower.on_block(block)
|
||||
|
||||
# Follower should have accepted the block
|
||||
assert follower.local_chain.length() == 1
|
||||
assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 2
|
||||
assert follower.tip() == block
|
||||
|
||||
# Follower should have updated their ledger state to mark the leader coin as spent
|
||||
assert follower.tip_state().verify_unspent(leader_coin.nullifier()) == False
|
||||
|
||||
reuse_coin_block = mk_block(slot=1, parent=block.id(), coin=leader_coin)
|
||||
follower.on_block(block)
|
||||
reuse_coin_block = mk_block(slot=1, parent=block, coin=leader_coin)
|
||||
follower.on_block(reuse_coin_block)
|
||||
|
||||
# Follower should *not* have accepted the block
|
||||
assert follower.local_chain.length() == 1
|
||||
assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 2
|
||||
assert follower.tip() == block
|
||||
|
||||
def test_ledger_state_is_properly_updated_on_reorg(self):
|
||||
@ -67,7 +78,7 @@ class TestLedgerStateUpdate(TestCase):
|
||||
|
||||
# 4) then coin[2] wins slot 1 and chooses to extend from block_2
|
||||
|
||||
block_3 = mk_block(parent=block_2.id(), slot=1, coin=coin[2])
|
||||
block_3 = mk_block(parent=block_2, slot=1, coin=coin[2])
|
||||
follower.on_block(block_3)
|
||||
# the follower should have switched over to the block_2 fork
|
||||
assert follower.tip() == block_3
|
||||
@ -89,37 +100,37 @@ class TestLedgerStateUpdate(TestCase):
|
||||
follower.on_block(block_2)
|
||||
assert follower.tip() == block_1
|
||||
assert len(follower.forks) == 1, f"{len(follower.forks)}"
|
||||
assert follower.forks[0].tip() == block_2
|
||||
assert follower.forks[0] == block_2.id()
|
||||
|
||||
# coin_2 wins slot 1 and chooses to extend from block_1
|
||||
# coin_3 also wins slot 1 and but chooses to extend from block_2
|
||||
# Both blocks are accepted. Both the local chain and the fork grow. No fork is newly created.
|
||||
block_3 = mk_block(parent=block_1.id(), slot=1, coin=coins[2])
|
||||
block_4 = mk_block(parent=block_2.id(), slot=1, coin=coins[3])
|
||||
block_3 = mk_block(parent=block_1, slot=1, coin=coins[2])
|
||||
block_4 = mk_block(parent=block_2, slot=1, coin=coins[3])
|
||||
follower.on_block(block_3)
|
||||
follower.on_block(block_4)
|
||||
assert follower.tip() == block_3
|
||||
assert len(follower.forks) == 1, f"{len(follower.forks)}"
|
||||
assert follower.forks[0].tip() == block_4
|
||||
assert follower.forks[0] == block_4.id()
|
||||
|
||||
# coin_4 wins slot 1 and but chooses to extend from block_2 as well
|
||||
# The block is accepted. A new fork is created "from the block_2".
|
||||
block_5 = mk_block(parent=block_2.id(), slot=1, coin=coins[4])
|
||||
block_5 = mk_block(parent=block_2, slot=1, coin=coins[4])
|
||||
follower.on_block(block_5)
|
||||
assert follower.tip() == block_3
|
||||
assert len(follower.forks) == 2, f"{len(follower.forks)}"
|
||||
assert follower.forks[0].tip() == block_4
|
||||
assert follower.forks[1].tip() == block_5
|
||||
assert follower.forks[0] == block_4.id()
|
||||
assert follower.forks[1] == block_5.id()
|
||||
|
||||
# A block based on an unknown parent is not accepted.
|
||||
# Nothing changes from the local chain and forks.
|
||||
unknown_block = mk_block(parent=block_5.id(), slot=2, coin=coins[5])
|
||||
block_6 = mk_block(parent=unknown_block.id(), slot=2, coin=coins[6])
|
||||
unknown_block = mk_block(parent=block_5, slot=2, coin=coins[5])
|
||||
block_6 = mk_block(parent=unknown_block, slot=2, coin=coins[6])
|
||||
follower.on_block(block_6)
|
||||
assert follower.tip() == block_3
|
||||
assert len(follower.forks) == 2, f"{len(follower.forks)}"
|
||||
assert follower.forks[0].tip() == block_4
|
||||
assert follower.forks[1].tip() == block_5
|
||||
assert follower.forks[0] == block_4.id()
|
||||
assert follower.forks[1] == block_5.id()
|
||||
|
||||
def test_epoch_transition(self):
|
||||
leader_coins = [Coin(sk=i, value=100) for i in range(4)]
|
||||
@ -138,14 +149,14 @@ class TestLedgerStateUpdate(TestCase):
|
||||
assert follower.tip() == block_1
|
||||
assert follower.tip().slot.epoch(config).epoch == 0
|
||||
|
||||
block_2 = mk_block(slot=19, parent=block_1.id(), coin=leader_coins[1])
|
||||
block_2 = mk_block(slot=19, parent=block_1, coin=leader_coins[1])
|
||||
follower.on_block(block_2)
|
||||
assert follower.tip() == block_2
|
||||
assert follower.tip().slot.epoch(config).epoch == 0
|
||||
|
||||
# ---- EPOCH 1 ----
|
||||
|
||||
block_3 = mk_block(slot=20, parent=block_2.id(), coin=leader_coins[2])
|
||||
block_3 = mk_block(slot=20, parent=block_2, coin=leader_coins[2])
|
||||
follower.on_block(block_3)
|
||||
assert follower.tip() == block_3
|
||||
assert follower.tip().slot.epoch(config).epoch == 1
|
||||
@ -157,7 +168,7 @@ class TestLedgerStateUpdate(TestCase):
|
||||
# To ensure this is the case, we add a new coin just to the state associated with that slot,
|
||||
# so that the new block can be accepted only if that is the snapshot used
|
||||
# first, verify that if we don't change the state, the block is not accepted
|
||||
block_4 = mk_block(slot=40, parent=block_3.id(), coin=Coin(sk=4, value=100))
|
||||
block_4 = mk_block(slot=40, parent=block_3, coin=Coin(sk=4, value=100))
|
||||
follower.on_block(block_4)
|
||||
assert follower.tip() == block_3
|
||||
# then we add the coin to "spendable commitments" associated with slot 9
|
||||
@ -181,12 +192,12 @@ class TestLedgerStateUpdate(TestCase):
|
||||
assert follower.tip() == block_1
|
||||
|
||||
# coin can't be reused to win following slots:
|
||||
block_2_reuse = mk_block(slot=1, parent=block_1.id(), coin=coin)
|
||||
block_2_reuse = mk_block(slot=1, parent=block_1, coin=coin)
|
||||
follower.on_block(block_2_reuse)
|
||||
assert follower.tip() == block_1
|
||||
|
||||
# but the evolved coin is eligible
|
||||
block_2_evolve = mk_block(slot=1, parent=block_1.id(), coin=coin.evolve())
|
||||
block_2_evolve = mk_block(slot=1, parent=block_1, coin=coin.evolve())
|
||||
follower.on_block(block_2_evolve)
|
||||
assert follower.tip() == block_2_evolve
|
||||
|
||||
@ -212,12 +223,12 @@ class TestLedgerStateUpdate(TestCase):
|
||||
)
|
||||
|
||||
# the new coin is not yet eligible for elections
|
||||
block_0_1_attempt = mk_block(slot=1, parent=block_0_0.id(), coin=coin_new)
|
||||
block_0_1_attempt = mk_block(slot=1, parent=block_0_0, coin=coin_new)
|
||||
follower.on_block(block_0_1_attempt)
|
||||
assert follower.tip() == block_0_0
|
||||
|
||||
# whereas the evolved coin from genesis can be spent immediately
|
||||
block_0_1 = mk_block(slot=1, parent=block_0_0.id(), coin=coin.evolve())
|
||||
block_0_1 = mk_block(slot=1, parent=block_0_0, coin=coin.evolve())
|
||||
follower.on_block(block_0_1)
|
||||
assert follower.tip() == block_0_1
|
||||
|
||||
@ -226,7 +237,7 @@ class TestLedgerStateUpdate(TestCase):
|
||||
# The newly minted coin is still not eligible in the following epoch since the
|
||||
# stake distribution snapshot is taken at the beginning of the previous epoch
|
||||
|
||||
block_1_0 = mk_block(slot=20, parent=block_0_1.id(), coin=coin_new)
|
||||
block_1_0 = mk_block(slot=20, parent=block_0_1, coin=coin_new)
|
||||
follower.on_block(block_1_0)
|
||||
assert follower.tip() == block_0_1
|
||||
|
||||
@ -234,16 +245,12 @@ class TestLedgerStateUpdate(TestCase):
|
||||
|
||||
# The coin is finally eligible 2 epochs after it was first minted
|
||||
|
||||
block_2_0 = mk_block(
|
||||
slot=40,
|
||||
parent=block_0_1.id(),
|
||||
coin=coin_new,
|
||||
)
|
||||
block_2_0 = mk_block(slot=40, parent=block_0_1, coin=coin_new)
|
||||
follower.on_block(block_2_0)
|
||||
assert follower.tip() == block_2_0
|
||||
|
||||
# And now the minted coin can freely use the evolved coin for subsequent blocks
|
||||
block_2_1 = mk_block(slot=40, parent=block_2_0.id(), coin=coin_new.evolve())
|
||||
block_2_1 = mk_block(slot=40, parent=block_2_0, coin=coin_new.evolve())
|
||||
follower.on_block(block_2_1)
|
||||
assert follower.tip() == block_2_1
|
||||
|
||||
@ -259,7 +266,7 @@ class TestLedgerStateUpdate(TestCase):
|
||||
|
||||
coin_new = coin.evolve()
|
||||
coin_new_new = coin_new.evolve()
|
||||
block_0_1 = mk_block(slot=1, parent=block_0_0.id(), coin=coin_new_new)
|
||||
block_0_1 = mk_block(slot=1, parent=block_0_0, coin=coin_new_new)
|
||||
follower.on_block(block_0_1)
|
||||
# the coin evolved twice should not be accepted as it is not in the lead commitments
|
||||
assert follower.tip() == block_0_0
|
||||
@ -272,7 +279,7 @@ class TestLedgerStateUpdate(TestCase):
|
||||
orphan = mk_block(parent=genesis.block, slot=0, coin=coin_orphan)
|
||||
block_0_1 = mk_block(
|
||||
slot=1,
|
||||
parent=block_0_0.id(),
|
||||
parent=block_0_0,
|
||||
coin=coin_orphan.evolve(),
|
||||
orphaned_proofs=[orphan],
|
||||
)
|
||||
|
||||
@ -1,21 +1,8 @@
|
||||
from unittest import TestCase
|
||||
from itertools import repeat
|
||||
import numpy as np
|
||||
import hashlib
|
||||
|
||||
from copy import deepcopy
|
||||
from cryptarchia.cryptarchia import (
|
||||
maxvalid_bg,
|
||||
Chain,
|
||||
BlockHeader,
|
||||
Slot,
|
||||
Id,
|
||||
MockLeaderProof,
|
||||
Coin,
|
||||
Follower,
|
||||
)
|
||||
from cryptarchia.cryptarchia import Coin, Follower
|
||||
|
||||
from .test_common import mk_chain, mk_config, mk_genesis_state, mk_block
|
||||
from .test_common import mk_config, mk_genesis_state, mk_block
|
||||
|
||||
|
||||
class TestOrphanedProofs(TestCase):
|
||||
@ -36,15 +23,15 @@ class TestOrphanedProofs(TestCase):
|
||||
#
|
||||
|
||||
b1, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
|
||||
b2, c_a = mk_block(b1.id(), 2, c_a), c_a.evolve()
|
||||
b3, c_b = mk_block(b1.id(), 2, c_b), c_b.evolve()
|
||||
b2, c_a = mk_block(b1, 2, c_a), c_a.evolve()
|
||||
b3, c_b = mk_block(b1, 2, c_b), c_b.evolve()
|
||||
|
||||
for b in [b1, b2, b3]:
|
||||
follower.on_block(b)
|
||||
|
||||
assert follower.tip() == b2
|
||||
assert [f.tip() for f in follower.forks] == [b3]
|
||||
assert follower.unimported_orphans(follower.tip_id()) == [b3]
|
||||
assert [f for f in follower.forks] == [b3.id()]
|
||||
assert follower.unimported_orphans() == [b3]
|
||||
|
||||
# -- extend with import --
|
||||
#
|
||||
@ -54,12 +41,12 @@ class TestOrphanedProofs(TestCase):
|
||||
# \ /
|
||||
# b3
|
||||
#
|
||||
b4, c_a = mk_block(b2.id(), 3, c_a, orphaned_proofs=[b3]), c_a.evolve()
|
||||
b4, c_a = mk_block(b2, 3, c_a, orphaned_proofs=[b3]), c_a.evolve()
|
||||
follower.on_block(b4)
|
||||
|
||||
assert follower.tip() == b4
|
||||
assert [f.tip() for f in follower.forks] == [b3]
|
||||
assert follower.unimported_orphans(follower.tip_id()) == []
|
||||
assert [f for f in follower.forks] == [b3.id()]
|
||||
assert follower.unimported_orphans() == []
|
||||
|
||||
def test_orphan_proof_import_from_long_running_fork(self):
|
||||
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10)
|
||||
@ -79,18 +66,18 @@ class TestOrphanedProofs(TestCase):
|
||||
|
||||
b1, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
|
||||
|
||||
b2, c_a = mk_block(b1.id(), 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2.id(), 3, c_a), c_a.evolve()
|
||||
b2, c_a = mk_block(b1, 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2, 3, c_a), c_a.evolve()
|
||||
|
||||
b4, c_b = mk_block(b1.id(), 2, c_b), c_b.evolve()
|
||||
b5, c_b = mk_block(b4.id(), 3, c_b), c_b.evolve()
|
||||
b4, c_b = mk_block(b1, 2, c_b), c_b.evolve()
|
||||
b5, c_b = mk_block(b4, 3, c_b), c_b.evolve()
|
||||
|
||||
for b in [b1, b2, b3, b4, b5]:
|
||||
follower.on_block(b)
|
||||
|
||||
assert follower.tip() == b3
|
||||
assert [f.tip() for f in follower.forks] == [b5]
|
||||
assert follower.unimported_orphans(follower.tip_id()) == [b4, b5]
|
||||
assert [f for f in follower.forks] == [b5.id()]
|
||||
assert follower.unimported_orphans() == [b4, b5]
|
||||
|
||||
# -- extend b3, importing the fork --
|
||||
#
|
||||
@ -100,11 +87,11 @@ class TestOrphanedProofs(TestCase):
|
||||
# \ / /
|
||||
# b4 - b5
|
||||
|
||||
b6, c_a = mk_block(b3.id(), 4, c_a, orphaned_proofs=[b4, b5]), c_a.evolve()
|
||||
b6, c_a = mk_block(b3, 4, c_a, orphaned_proofs=[b4, b5]), c_a.evolve()
|
||||
follower.on_block(b6)
|
||||
|
||||
assert follower.tip() == b6
|
||||
assert [f.tip() for f in follower.forks] == [b5]
|
||||
assert [f for f in follower.forks] == [b5.id()]
|
||||
|
||||
def test_orphan_proof_import_from_fork_without_direct_shared_parent(self):
|
||||
coins = [Coin(sk=i, value=10) for i in range(2)]
|
||||
@ -123,20 +110,20 @@ class TestOrphanedProofs(TestCase):
|
||||
|
||||
b1, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
|
||||
|
||||
b2, c_a = mk_block(b1.id(), 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2.id(), 3, c_a), c_a.evolve()
|
||||
b4, c_a = mk_block(b3.id(), 4, c_a), c_a.evolve()
|
||||
b2, c_a = mk_block(b1, 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2, 3, c_a), c_a.evolve()
|
||||
b4, c_a = mk_block(b3, 4, c_a), c_a.evolve()
|
||||
|
||||
b5, c_b = mk_block(b1.id(), 2, c_b), c_b.evolve()
|
||||
b6, c_b = mk_block(b5.id(), 3, c_b), c_b.evolve()
|
||||
b7, c_b = mk_block(b6.id(), 4, c_b), c_b.evolve()
|
||||
b5, c_b = mk_block(b1, 2, c_b), c_b.evolve()
|
||||
b6, c_b = mk_block(b5, 3, c_b), c_b.evolve()
|
||||
b7, c_b = mk_block(b6, 4, c_b), c_b.evolve()
|
||||
|
||||
for b in [b1, b2, b3, b4, b5, b6, b7]:
|
||||
follower.on_block(b)
|
||||
|
||||
assert follower.tip() == b4
|
||||
assert [f.tip() for f in follower.forks] == [b7]
|
||||
assert follower.unimported_orphans(follower.tip_id()) == [b5, b6, b7]
|
||||
assert [f for f in follower.forks] == [b7.id()]
|
||||
assert follower.unimported_orphans() == [b5, b6, b7]
|
||||
|
||||
# -- extend b4, importing the forks --
|
||||
#
|
||||
@ -149,12 +136,12 @@ class TestOrphanedProofs(TestCase):
|
||||
# Earlier implementations of orphan proof validation failed to
|
||||
# validate b7 as an orphan here.
|
||||
|
||||
b8, c_a = mk_block(b4.id(), 5, c_a, orphaned_proofs=[b5, b6, b7]), c_a.evolve()
|
||||
b8, c_a = mk_block(b4, 5, c_a, orphaned_proofs=[b5, b6, b7]), c_a.evolve()
|
||||
follower.on_block(b8)
|
||||
|
||||
assert follower.tip() == b8
|
||||
assert [f.tip() for f in follower.forks] == [b7]
|
||||
assert follower.unimported_orphans(follower.tip_id()) == []
|
||||
assert [f for f in follower.forks] == [b7.id()]
|
||||
assert follower.unimported_orphans() == []
|
||||
|
||||
def test_unimported_orphans(self):
|
||||
# Given the following fork graph:
|
||||
@ -187,22 +174,22 @@ class TestOrphanedProofs(TestCase):
|
||||
|
||||
b1, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
|
||||
|
||||
b2, c_a = mk_block(b1.id(), 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2.id(), 3, c_a), c_a.evolve()
|
||||
b2, c_a = mk_block(b1, 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2, 3, c_a), c_a.evolve()
|
||||
|
||||
b4, c_b = mk_block(b1.id(), 2, c_b), c_b.evolve()
|
||||
b5, c_b = mk_block(b4.id(), 3, c_b), c_b.evolve()
|
||||
b4, c_b = mk_block(b1, 2, c_b), c_b.evolve()
|
||||
b5, c_b = mk_block(b4, 3, c_b), c_b.evolve()
|
||||
|
||||
b6, c_c = mk_block(b4.id(), 3, c_c), c_c.evolve()
|
||||
b6, c_c = mk_block(b4, 3, c_c), c_c.evolve()
|
||||
|
||||
for b in [b1, b2, b3, b4, b5, b6]:
|
||||
follower.on_block(b)
|
||||
|
||||
assert follower.tip() == b3
|
||||
assert [f.tip() for f in follower.forks] == [b5, b6]
|
||||
assert follower.unimported_orphans(follower.tip_id()) == [b4, b5, b6]
|
||||
assert [f for f in follower.forks] == [b5.id(), b6.id()]
|
||||
assert follower.unimported_orphans() == [b4, b5, b6]
|
||||
|
||||
b7, c_a = mk_block(b3.id(), 4, c_a, orphaned_proofs=[b4, b5, b6]), c_a.evolve()
|
||||
b7, c_a = mk_block(b3, 4, c_a, orphaned_proofs=[b4, b5, b6]), c_a.evolve()
|
||||
|
||||
follower.on_block(b7)
|
||||
assert follower.tip() == b7
|
||||
@ -235,30 +222,30 @@ class TestOrphanedProofs(TestCase):
|
||||
follower = Follower(genesis, config)
|
||||
|
||||
b1, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
|
||||
b2, c_a = mk_block(b1.id(), 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2.id(), 3, c_a), c_a.evolve()
|
||||
b2, c_a = mk_block(b1, 2, c_a), c_a.evolve()
|
||||
b3, c_a = mk_block(b2, 3, c_a), c_a.evolve()
|
||||
|
||||
b4, c_b = mk_block(b3.id(), 4, c_b), c_b.evolve()
|
||||
b5, c_a = mk_block(b3.id(), 4, c_a), c_a.evolve()
|
||||
b4, c_b = mk_block(b3, 4, c_b), c_b.evolve()
|
||||
b5, c_a = mk_block(b3, 4, c_a), c_a.evolve()
|
||||
|
||||
b6, c_b = mk_block(b4.id(), 5, c_b, orphaned_proofs=[b5]), c_b.evolve()
|
||||
b7, c_a = mk_block(b4.id(), 5, c_a, orphaned_proofs=[b5]), c_a.evolve()
|
||||
b6, c_b = mk_block(b4, 5, c_b, orphaned_proofs=[b5]), c_b.evolve()
|
||||
b7, c_a = mk_block(b4, 5, c_a, orphaned_proofs=[b5]), c_a.evolve()
|
||||
|
||||
b8, c_b = mk_block(b6.id(), 6, c_b, orphaned_proofs=[b7]), c_b.evolve()
|
||||
b8, c_b = mk_block(b6, 6, c_b, orphaned_proofs=[b7]), c_b.evolve()
|
||||
|
||||
for b in [b1, b2, b3, b4, b5]:
|
||||
follower.on_block(b)
|
||||
|
||||
assert follower.tip() == b4
|
||||
assert follower.unimported_orphans(follower.tip_id()) == [b5]
|
||||
assert follower.unimported_orphans() == [b5]
|
||||
|
||||
for b in [b6, b7]:
|
||||
follower.on_block(b)
|
||||
|
||||
assert follower.tip() == b6
|
||||
assert follower.unimported_orphans(follower.tip_id()) == [b7]
|
||||
assert follower.unimported_orphans() == [b7]
|
||||
|
||||
follower.on_block(b8)
|
||||
|
||||
assert follower.tip() == b8
|
||||
assert follower.unimported_orphans(follower.tip_id()) == []
|
||||
assert follower.unimported_orphans() == []
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
from unittest import TestCase
|
||||
from dataclasses import dataclass
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
@ -29,13 +28,13 @@ class TestStakeRelativization(TestCase):
|
||||
# on fork, tip state is not updated
|
||||
orphan = mk_block(genesis.block, slot=1, coin=c_b)
|
||||
follower.on_block(orphan)
|
||||
assert follower.tip_state().block == b1.id()
|
||||
assert follower.tip_state().block == b1
|
||||
assert follower.tip_state().leader_count == 1
|
||||
|
||||
# after orphan is adopted, leader count should jumpy by 2 (each orphan counts as a leader)
|
||||
b2 = mk_block(b1.id(), slot=2, coin=c_a.evolve(), orphaned_proofs=[orphan])
|
||||
b2 = mk_block(b1, slot=2, coin=c_a.evolve(), orphaned_proofs=[orphan])
|
||||
follower.on_block(b2)
|
||||
assert follower.tip_state().block == b2.id()
|
||||
assert follower.tip_state().block == b2
|
||||
assert follower.tip_state().leader_count == 3
|
||||
|
||||
def test_inference_on_empty_genesis_epoch(self):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user