cryptarchia: remove orphan proofs from block headers

This commit is contained in:
David Rusu 2025-03-21 00:57:54 +04:00
parent 058376987e
commit 236a677c86
6 changed files with 58 additions and 442 deletions

View File

@ -13,9 +13,6 @@ import numpy as np
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Id: TypeAlias = bytes
class Hash(bytes): class Hash(bytes):
def __new__(cls, dst, *data): def __new__(cls, dst, *data):
assert isinstance(dst, bytes) assert isinstance(dst, bytes)
@ -178,7 +175,7 @@ class Note:
self.zone_id, self.zone_id,
) )
def nullifier(self) -> Id: def nullifier(self) -> Hash:
return Hash(b"NOMOS_NOTE_NF", self.commitment(), self.encode_sk()) return Hash(b"NOMOS_NOTE_NF", self.commitment(), self.encode_sk())
@ -186,7 +183,7 @@ class Note:
class MockLeaderProof: class MockLeaderProof:
note: Note note: Note
slot: Slot slot: Slot
parent: Id parent: Hash
@property @property
def commitment(self): def commitment(self):
@ -200,7 +197,7 @@ class MockLeaderProof:
def evolved_commitment(self): def evolved_commitment(self):
return self.note.evolve().commitment() return self.note.evolve().commitment()
def verify(self, slot: Slot, parent: Id): def verify(self, slot: Slot, parent: Hash):
# TODO: verification not implemented # TODO: verification not implemented
return slot == self.slot and parent == self.parent return slot == self.slot and parent == self.parent
@ -208,52 +205,29 @@ class MockLeaderProof:
@dataclass @dataclass
class BlockHeader: class BlockHeader:
slot: Slot slot: Slot
parent: Id parent: Hash
content_size: int content_size: int
content_id: Id content_id: Hash
leader_proof: MockLeaderProof leader_proof: MockLeaderProof
orphaned_proofs: List["BlockHeader"] = field(default_factory=list)
def update_header_hash(self, h):
# version byte
h.update(b"\x01")
# content size
h.update(int.to_bytes(self.content_size, length=4, byteorder="big"))
# content id
assert len(self.content_id) == 32
h.update(self.content_id)
# slot
h.update(self.slot.encode())
# parent
assert len(self.parent) == 32
h.update(self.parent)
# leader proof
assert len(self.leader_proof.commitment) == 32
h.update(self.leader_proof.commitment)
assert len(self.leader_proof.nullifier) == 32
h.update(self.leader_proof.nullifier)
assert len(self.leader_proof.evolved_commitment) == 32
h.update(self.leader_proof.evolved_commitment)
# orphaned proofs
h.update(int.to_bytes(len(self.orphaned_proofs), length=4, byteorder="big"))
for proof in self.orphaned_proofs:
proof.update_header_hash(h)
# **Attention**: # **Attention**:
# The ID of a block header is defined as the 32byte blake2b hash of its fields # The ID of a block header is defined as the hash of its fields
# as serialized in the format specified by the 'HEADER' rule in 'messages.abnf'. # as serialized in the format specified by the 'HEADER' rule in 'messages.abnf'.
# #
# The following code is to be considered as a reference implementation, mostly to be used for testing. # The following code is to be considered as a reference implementation, mostly to be used for testing.
def id(self) -> Id: def id(self) -> Hash:
h = blake2b(digest_size=32) return Hash(
self.update_header_hash(h) b"BLOCK_ID",
return h.digest() b"\x01", # version
int.to_bytes(self.content_size, length=4, byteorder="big"), # content size
self.content_id, # content id
self.slot.encode(), # slot
self.parent, # parent
# leader proof
self.leader_proof.commitment,
self.leader_proof.nullifier,
self.leader_proof.evolved_commitment,
)
def __hash__(self): def __hash__(self):
return hash(self.id()) return hash(self.id())
@ -273,19 +247,19 @@ class LedgerState:
# #
# NOTE that this does not prevent nonce grinding at the last slot # NOTE that this does not prevent nonce grinding at the last slot
# when the nonce snapshot is taken # when the nonce snapshot is taken
nonce: Id = None nonce: Hash = None
# set of commitments # set of commitments
commitments_spend: set[Id] = field(default_factory=set) commitments_spend: set[Hash] = field(default_factory=set)
# set of commitments eligible to lead # set of commitments eligible to lead
commitments_lead: set[Id] = field(default_factory=set) commitments_lead: set[Hash] = field(default_factory=set)
# set of nullified notes # set of nullified notes
nullifiers: set[Id] = field(default_factory=set) nullifiers: set[Hash] = field(default_factory=set)
# -- Stake Relativization State # -- Stake Relativization State
# The number of observed leaders (blocks + orphans), this measurement is # The number of observed leaders, this measurement is
# used in inferring total active stake in the network. # used in inferring total active stake in the network.
leader_count: int = 0 leader_count: int = 0
@ -302,13 +276,13 @@ class LedgerState:
def replace(self, **kwarg) -> "LedgerState": def replace(self, **kwarg) -> "LedgerState":
return replace(self, **kwarg) return replace(self, **kwarg)
def verify_eligible_to_spend(self, commitment: Id) -> bool: def verify_eligible_to_spend(self, commitment: Hash) -> bool:
return commitment in self.commitments_spend return commitment in self.commitments_spend
def verify_eligible_to_lead(self, commitment: Id) -> bool: def verify_eligible_to_lead(self, commitment: Hash) -> bool:
return commitment in self.commitments_lead return commitment in self.commitments_lead
def verify_unspent(self, nullifier: Id) -> bool: def verify_unspent(self, nullifier: Hash) -> bool:
return nullifier not in self.nullifiers return nullifier not in self.nullifiers
def apply(self, block: BlockHeader): def apply(self, block: BlockHeader):
@ -320,9 +294,8 @@ class LedgerState:
block.leader_proof.nullifier, block.leader_proof.nullifier,
block.slot.encode(), block.slot.encode(),
) )
self.apply_leader_proof(block.leader_proof)
self.block = 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): def apply_leader_proof(self, proof: MockLeaderProof):
self.nullifiers.add(proof.nullifier) self.nullifiers.add(proof.nullifier)
@ -347,7 +320,7 @@ class EpochState:
# leadership lottery. # leadership lottery.
inferred_total_active_stake: int inferred_total_active_stake: int
def verify_eligible_to_lead_due_to_age(self, commitment: Id) -> bool: def verify_eligible_to_lead_due_to_age(self, commitment: Hash) -> bool:
# A note is eligible to lead if it was committed to before the the stake # A note is eligible to lead if it was committed to before the the stake
# distribution snapshot was taken or it was produced by a leader proof # distribution snapshot was taken or it was produced by a leader proof
# since the snapshot was taken. # since the snapshot was taken.
@ -371,7 +344,7 @@ class EpochState:
class Follower: class Follower:
def __init__(self, genesis_state: LedgerState, config: Config): def __init__(self, genesis_state: LedgerState, config: Config):
self.config = config self.config = config
self.forks: list[Id] = [] self.forks: list[Hash] = []
self.local_chain = genesis_state.block.id() self.local_chain = genesis_state.block.id()
self.genesis_state = genesis_state self.genesis_state = genesis_state
self.ledger_state = {genesis_state.block.id(): genesis_state.copy()} self.ledger_state = {genesis_state.block.id(): genesis_state.copy()}
@ -384,39 +357,10 @@ class Follower:
current_state = self.ledger_state[block.parent].copy() 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( epoch_state = self.compute_epoch_state(
block.slot.epoch(self.config), block.parent 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
# 1. ensure they are valid locally in their original branch
# 2. ensure it does not conflict with current state
# We take a shortcut for (1.) by restricting orphans to proofs we've
# already processed in other branches.
if orphan.id() not in self.ledger_state:
raise MissingOrphanProof
# (2.) is satisfied by verifying the proof against current state ensuring:
# - it is a valid proof
# - and the nullifier has not already been spent
if not self.verify_slot_leader(
orphan.slot,
orphan.parent,
orphan.leader_proof,
epoch_state,
current_state,
):
raise InvalidOrphanProof
# if an adopted leadership proof is valid we need to apply its
# effects to the ledger state
current_state.apply_leader_proof(orphan.leader_proof)
# TODO: this is not the full block validation spec, only slot leader is verified # TODO: this is not the full block validation spec, only slot leader is verified
if not self.verify_slot_leader( if not self.verify_slot_leader(
block.slot, block.slot,
@ -430,7 +374,7 @@ class Follower:
def verify_slot_leader( def verify_slot_leader(
self, self,
slot: Slot, slot: Slot,
parent: Id, parent: Hash,
proof: MockLeaderProof, proof: MockLeaderProof,
# notes are old enough if their commitment is in the stake distribution snapshot # notes are old enough if their commitment is in the stake distribution snapshot
epoch_state: EpochState, epoch_state: EpochState,
@ -491,29 +435,8 @@ class Follower:
self.forks.remove(checkpoint_block_id) self.forks.remove(checkpoint_block_id)
self.local_chain = checkpoint_block_id self.local_chain = checkpoint_block_id
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.tip_state().copy()
tip = tip_state.block.id()
orphans = []
for fork in self.forks:
_, _, fork_depth, fork_suffix = common_prefix_depth(
tip, fork, self.ledger_state
)
for b in fork_suffix:
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 # Evaluate the fork choice rule and return the chain we should be following
def fork_choice(self) -> Id: def fork_choice(self) -> Hash:
return maxvalid_bg( return maxvalid_bg(
self.local_chain, self.local_chain,
self.forks, self.forks,
@ -525,13 +448,13 @@ class Follower:
def tip(self) -> BlockHeader: def tip(self) -> BlockHeader:
return self.tip_state().block return self.tip_state().block
def tip_id(self) -> Id: def tip_id(self) -> Hash:
return self.local_chain return self.local_chain
def tip_state(self) -> LedgerState: def tip_state(self) -> LedgerState:
return self.ledger_state[self.tip_id()] return self.ledger_state[self.tip_id()]
def state_at_slot_beginning(self, tip: Id, slot: Slot) -> LedgerState: def state_at_slot_beginning(self, tip: Hash, slot: Slot) -> LedgerState:
for state in iter_chain(tip, self.ledger_state): for state in iter_chain(tip, self.ledger_state):
if state.block.slot < slot: if state.block.slot < slot:
return state return state
@ -540,7 +463,7 @@ class Follower:
def epoch_start_slot(self, epoch) -> Slot: def epoch_start_slot(self, epoch) -> Slot:
return Slot(epoch.epoch * self.config.epoch_length) return Slot(epoch.epoch * self.config.epoch_length)
def stake_distribution_snapshot(self, epoch, tip: Id): def stake_distribution_snapshot(self, epoch, tip: Hash):
# stake distribution snapshot happens at the beginning of the previous epoch, # 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 # 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) slot = Slot(epoch.prev().epoch * self.config.epoch_length)
@ -555,7 +478,7 @@ class Follower:
) )
return self.state_at_slot_beginning(tip, slot) return self.state_at_slot_beginning(tip, slot)
def compute_epoch_state(self, epoch: Epoch, tip: Id) -> EpochState: def compute_epoch_state(self, epoch: Epoch, tip: Hash) -> EpochState:
if epoch.epoch == 0: if epoch.epoch == 0:
return EpochState( return EpochState(
stake_distribution_snapshot=self.genesis_state, stake_distribution_snapshot=self.genesis_state,
@ -663,7 +586,7 @@ class Leader:
note: Note note: Note
def try_prove_slot_leader( def try_prove_slot_leader(
self, epoch: EpochState, slot: Slot, parent: Id self, epoch: EpochState, slot: Slot, parent: Hash
) -> MockLeaderProof | None: ) -> MockLeaderProof | None:
if self._is_slot_leader(epoch, slot): if self._is_slot_leader(epoch, slot):
return MockLeaderProof(self.note, slot, parent) return MockLeaderProof(self.note, slot, parent)
@ -679,7 +602,7 @@ class Leader:
def iter_chain( def iter_chain(
tip: Id, states: Dict[Id, LedgerState] tip: Hash, states: Dict[Hash, LedgerState]
) -> Generator[LedgerState, None, None]: ) -> Generator[LedgerState, None, None]:
while tip in states: while tip in states:
yield states[tip] yield states[tip]
@ -687,14 +610,14 @@ def iter_chain(
def iter_chain_blocks( def iter_chain_blocks(
tip: Id, states: Dict[Id, LedgerState] tip: Hash, states: Dict[Hash, LedgerState]
) -> Generator[BlockHeader, None, None]: ) -> Generator[BlockHeader, None, None]:
for state in iter_chain(tip, states): for state in iter_chain(tip, states):
yield state.block yield state.block
def common_prefix_depth( def common_prefix_depth(
a: Id, b: Id, states: Dict[Id, LedgerState] a: Hash, b: Hash, states: Dict[Hash, LedgerState]
) -> tuple[int, list[BlockHeader], int, list[BlockHeader]]: ) -> tuple[int, list[BlockHeader], int, list[BlockHeader]]:
return common_prefix_depth_from_chains( return common_prefix_depth_from_chains(
iter_chain_blocks(a, states), iter_chain_blocks(b, states) iter_chain_blocks(a, states), iter_chain_blocks(b, states)
@ -752,7 +675,7 @@ def chain_density(chain: list[BlockHeader], slot: Slot) -> int:
return sum(1 for b in chain if b.slot < slot) return sum(1 for b in chain if b.slot < slot)
def block_children(states: Dict[Id, LedgerState]) -> Dict[Id, set[Id]]: def block_children(states: Dict[Hash, LedgerState]) -> Dict[Hash, set[Hash]]:
children = defaultdict(set) children = defaultdict(set)
for c, state in states.items(): for c, state in states.items():
children[state.block.parent].add(c) children[state.block.parent].add(c)
@ -768,14 +691,14 @@ def block_children(states: Dict[Id, LedgerState]) -> Dict[Id, set[Id]]:
# k defines the forking depth of a chain at which point we switch phases. # 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 # s defines the length of time (unit of slots) after the fork happened we will inspect for chain density
def maxvalid_bg( def maxvalid_bg(
local_chain: Id, local_chain: Hash,
forks: List[Id], forks: List[Hash],
k: int, k: int,
s: int, s: int,
states: Dict[Id, LedgerState], states: Dict[Hash, LedgerState],
) -> Id: ) -> Hash:
assert type(local_chain) == Id assert type(local_chain) == Hash, type(local_chain)
assert all(type(f) == Id for f in forks) assert all(type(f) == Hash for f in forks)
cmax = local_chain cmax = local_chain
for fork in forks: for fork in forks:
@ -806,16 +729,6 @@ class ParentNotFound(Exception):
return "Parent not found" return "Parent not found"
class MissingOrphanProof(Exception):
def __str__(self):
return "Missing orphan proof"
class InvalidOrphanProof(Exception):
def __str__(self):
return "Invalid orphan proof"
class InvalidLeaderProof(Exception): class InvalidLeaderProof(Exception):
def __str__(self): def __str__(self):
return "Invalid leader proof" return "Invalid leader proof"

View File

@ -4,7 +4,7 @@ from typing import Generator
from cryptarchia.cryptarchia import ( from cryptarchia.cryptarchia import (
BlockHeader, BlockHeader,
Follower, Follower,
Id, Hash,
ParentNotFound, ParentNotFound,
Slot, Slot,
common_prefix_depth_from_chains, common_prefix_depth_from_chains,
@ -19,7 +19,7 @@ def sync(local: Follower, peers: list[Follower]):
# Repeat the sync process until no peer has a tip ahead of the local tip, # Repeat the sync process until no peer has a tip ahead of the local tip,
# because peers' tips may advance during the sync process. # because peers' tips may advance during the sync process.
block_fetcher = BlockFetcher(peers) block_fetcher = BlockFetcher(peers)
rejected_blocks: set[Id] = set() rejected_blocks: set[Hash] = set()
while True: while True:
# Fetch blocks from the peers in the range of slots from the local tip to the latest tip. # Fetch blocks from the peers in the range of slots from the local tip to the latest tip.
# Gather orphaned blocks, which are blocks from forks that are absent in the local block tree. # Gather orphaned blocks, which are blocks from forks that are absent in the local block tree.
@ -145,7 +145,7 @@ class BlockFetcher:
continue continue
def fetch_chain_backward( def fetch_chain_backward(
self, tip: Id, local: Follower self, tip: Hash, local: Follower
) -> Generator[BlockHeader, None, None]: ) -> Generator[BlockHeader, None, None]:
# Fetches a chain of blocks from the peers, starting from the given tip to the genesis. # Fetches a chain of blocks from the peers, starting from the given tip to the genesis.
# Attempts to extend the chain as much as possible by querying multiple peers, # Attempts to extend the chain as much as possible by querying multiple peers,

View File

@ -7,6 +7,7 @@ from .cryptarchia import (
MockLeaderProof, MockLeaderProof,
Leader, Leader,
Follower, Follower,
Hash,
) )
@ -29,7 +30,6 @@ class TestNode:
return BlockHeader( return BlockHeader(
parent=parent, parent=parent,
slot=slot, slot=slot,
orphaned_proofs=self.follower.unimported_orphans(),
leader_proof=leader_proof, leader_proof=leader_proof,
content_size=0, content_size=0,
content_id=bytes(32), content_id=bytes(32),
@ -67,19 +67,17 @@ def mk_genesis_state(initial_stake_distribution: list[Note]) -> LedgerState:
def mk_block( def mk_block(
parent: BlockHeader, slot: int, note: Note, content=bytes(32), orphaned_proofs=[] parent: BlockHeader, slot: int, note: Note, content=bytes(32)
) -> BlockHeader: ) -> BlockHeader:
assert type(parent) == BlockHeader, type(parent) assert type(parent) == BlockHeader, type(parent)
assert type(slot) == int, type(slot) assert type(slot) == int, type(slot)
from hashlib import sha256
return BlockHeader( return BlockHeader(
slot=Slot(slot), slot=Slot(slot),
parent=parent.id(), parent=parent.id(),
content_size=len(content), content_size=len(content),
content_id=sha256(content).digest(), content_id=Hash(b"CONTENT_ID", content),
leader_proof=MockLeaderProof(note, Slot(slot), parent=parent.id()), leader_proof=MockLeaderProof(note, Slot(slot), parent=parent.id()),
orphaned_proofs=orphaned_proofs,
) )

View File

@ -4,7 +4,6 @@ from .cryptarchia import (
Note, Note,
Follower, Follower,
InvalidLeaderProof, InvalidLeaderProof,
MissingOrphanProof,
ParentNotFound, ParentNotFound,
iter_chain, iter_chain,
) )
@ -263,46 +262,3 @@ class TestLedgerStateUpdate(TestCase):
block_2_1 = mk_block(slot=40, parent=block_2_0, note=note_new.evolve()) block_2_1 = mk_block(slot=40, parent=block_2_0, note=note_new.evolve())
follower.on_block(block_2_1) follower.on_block(block_2_1)
assert follower.tip() == block_2_1 assert follower.tip() == block_2_1
def test_orphaned_proofs(self):
note, note_orphan = Note(sk=0, value=100), Note(sk=1, value=100)
genesis = mk_genesis_state([note, note_orphan])
follower = Follower(genesis, mk_config([note, note_orphan]))
block_0_0 = mk_block(slot=0, parent=genesis.block, note=note)
follower.on_block(block_0_0)
assert follower.tip() == block_0_0
note_new = note.evolve()
note_new_new = note_new.evolve()
block_0_1 = mk_block(slot=1, parent=block_0_0, note=note_new_new)
with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_0_1)
# the note evolved twice should not be accepted as it is not in the lead commitments
assert follower.tip() == block_0_0
# An orphaned proof will not be accepted until a node first sees the corresponding block.
#
# Also, notice that the block is using the evolved orphan note which is not present on the main
# branch. The evolved orphan commitment is added from the orphan prior to validating the block
# header as part of orphan importing process
orphan = mk_block(parent=genesis.block, slot=0, note=note_orphan)
block_0_1 = mk_block(
slot=1,
parent=block_0_0,
note=note_orphan.evolve(),
orphaned_proofs=[orphan],
)
with self.assertRaises(MissingOrphanProof):
follower.on_block(block_0_1)
# since follower had not seen this orphan prior to being included as
# an orphan proof, it will be rejected
assert follower.tip() == block_0_0
# but all is fine if the follower first sees the orphan block, and then
# is imported into the main chain
follower.on_block(orphan)
follower.on_block(block_0_1)
assert follower.tip() == block_0_1

View File

@ -1,251 +0,0 @@
from unittest import TestCase
from cryptarchia.cryptarchia import Note, Follower
from .test_common import mk_config, mk_genesis_state, mk_block
class TestOrphanedProofs(TestCase):
def test_simple_orphan_import(self):
n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
coins = [n_a, n_b]
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
# -- fork --
#
# b2 == tip
# /
# b1
# \
# b3
#
b1, n_a = mk_block(genesis.block, 1, n_a), n_a.evolve()
b2, n_a = mk_block(b1, 2, n_a), n_a.evolve()
b3, n_b = mk_block(b1, 2, n_b), n_b.evolve()
for b in [b1, b2, b3]:
follower.on_block(b)
assert follower.tip() == b2
assert [f for f in follower.forks] == [b3.id()]
assert follower.unimported_orphans() == [b3]
# -- extend with import --
#
# b2 - b4
# / /
# b1 /
# \ /
# b3
#
b4, n_a = mk_block(b2, 3, n_a, orphaned_proofs=[b3]), n_a.evolve()
follower.on_block(b4)
assert follower.tip() == b4
assert [f for f in follower.forks] == [b3.id()]
assert follower.unimported_orphans() == []
def test_orphan_proof_import_from_long_running_fork(self):
n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
coins = [n_a, n_b]
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
# -- fork --
#
# b2 - b3 == tip
# /
# b1
# \
# b4 - b5
#
b1, n_a = mk_block(genesis.block, 1, n_a), n_a.evolve()
b2, n_a = mk_block(b1, 2, n_a), n_a.evolve()
b3, n_a = mk_block(b2, 3, n_a), n_a.evolve()
b4, n_b = mk_block(b1, 2, n_b), n_b.evolve()
b5, n_b = mk_block(b4, 3, n_b), n_b.evolve()
for b in [b1, b2, b3, b4, b5]:
follower.on_block(b)
assert follower.tip() == b3
assert [f for f in follower.forks] == [b5.id()]
assert follower.unimported_orphans() == [b4, b5]
# -- extend b3, importing the fork --
#
# b2 - b3 - b6 == tip
# / ___/
# b1 ___/ /
# \ / /
# b4 - b5
b6, n_a = mk_block(b3, 4, n_a, orphaned_proofs=[b4, b5]), n_a.evolve()
follower.on_block(b6)
assert follower.tip() == b6
assert [f for f in follower.forks] == [b5.id()]
def test_orphan_proof_import_from_fork_without_direct_shared_parent(self):
coins = [Note(sk=i, value=10) for i in range(2)]
n_a, n_b = coins
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
# -- forks --
#
# b2 - b3 - b4 == tip
# /
# b1
# \
# b5 - b6 - b7
b1, n_a = mk_block(genesis.block, 1, n_a), n_a.evolve()
b2, n_a = mk_block(b1, 2, n_a), n_a.evolve()
b3, n_a = mk_block(b2, 3, n_a), n_a.evolve()
b4, n_a = mk_block(b3, 4, n_a), n_a.evolve()
b5, n_b = mk_block(b1, 2, n_b), n_b.evolve()
b6, n_b = mk_block(b5, 3, n_b), n_b.evolve()
b7, n_b = mk_block(b6, 4, n_b), n_b.evolve()
for b in [b1, b2, b3, b4, b5, b6, b7]:
follower.on_block(b)
assert follower.tip() == b4
assert [f for f in follower.forks] == [b7.id()]
assert follower.unimported_orphans() == [b5, b6, b7]
# -- extend b4, importing the forks --
#
# b2 - b3 - b4 - b8 == tip
# / _______/
# b1 ____/______/
# \ / / /
# b5 - b6 - b7
#
# Earlier implementations of orphan proof validation failed to
# validate b7 as an orphan here.
b8, n_a = mk_block(b4, 5, n_a, orphaned_proofs=[b5, b6, b7]), n_a.evolve()
follower.on_block(b8)
assert follower.tip() == b8
assert [f for f in follower.forks] == [b7.id()]
assert follower.unimported_orphans() == []
def test_unimported_orphans(self):
# Given the following fork graph:
#
# b2 - b3
# /
# b1
# \
# b4 - b5
# \
# -- b6
#
# Orphans w.r.t. to b3 are b4..6, thus extending from b3 with b7 would
# give the following fork graph
#
# b2 - b3 --- b7== tip
# / ____/
# b1 ____/ __/
# \ / / /
# b4 - b5 /
# \ /
# -- b6
#
coins = [Note(sk=i, value=10) for i in range(3)]
n_a, n_b, n_c = coins
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
b1, n_a = mk_block(genesis.block, 1, n_a), n_a.evolve()
b2, n_a = mk_block(b1, 2, n_a), n_a.evolve()
b3, n_a = mk_block(b2, 3, n_a), n_a.evolve()
b4, n_b = mk_block(b1, 2, n_b), n_b.evolve()
b5, n_b = mk_block(b4, 3, n_b), n_b.evolve()
b6, n_c = mk_block(b4, 3, n_c), n_c.evolve()
for b in [b1, b2, b3, b4, b5, b6]:
follower.on_block(b)
assert follower.tip() == b3
assert [f for f in follower.forks] == [b5.id(), b6.id()]
assert follower.unimported_orphans() == [b4, b5, b6]
b7, n_a = mk_block(b3, 4, n_a, orphaned_proofs=[b4, b5, b6]), n_a.evolve()
follower.on_block(b7)
assert follower.tip() == b7
def test_transitive_orphan_reimports(self):
# Two forks, one after the other, with some complicated orphan imports.
# I don't have different line colors to differentiate orphans from parents
# so I've added o=XX to differentiate orphans from parents.
#
# - The first fork at b3(a) is not too interesting.
# - The second fork at b4(b) has both b6 and b7 importing b5
# - crucially b7 uses the evolved commitment from b5
# - Then finally b8 imports b7.
#
# proper orphan proof importing will be able to deal with the fact that
# b7's commitment was produced outside of the main branch AND the commitment
# is not part of the current list of orphans in b8
# (b5 had already been imported, therefore it is not included as an orphan in b8)
#
# b1(a) - b2(a) - b3(a) - b4(b) - b6(b, o=b5) - b8(b, o=b7)
# \ \___ __/ __/
# \ _x_ __/
# \ / \_ /
# -b5(a)-----\-b7(a, o=b5)
coins = [Note(sk=i, value=10) for i in range(2)]
n_a, n_b = coins
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
b1, n_a = mk_block(genesis.block, 1, n_a), n_a.evolve()
b2, n_a = mk_block(b1, 2, n_a), n_a.evolve()
b3, n_a = mk_block(b2, 3, n_a), n_a.evolve()
b4, n_b = mk_block(b3, 4, n_b), n_b.evolve()
b5, n_a = mk_block(b3, 4, n_a), n_a.evolve()
b6, n_b = mk_block(b4, 5, n_b, orphaned_proofs=[b5]), n_b.evolve()
b7, n_a = mk_block(b4, 5, n_a, orphaned_proofs=[b5]), n_a.evolve()
b8, n_b = mk_block(b6, 6, n_b, orphaned_proofs=[b7]), n_b.evolve()
for b in [b1, b2, b3, b4, b5]:
follower.on_block(b)
assert follower.tip() == b4
assert follower.unimported_orphans() == [b5]
for b in [b6, b7]:
follower.on_block(b)
assert follower.tip() == b6
assert follower.unimported_orphans() == [b7]
follower.on_block(b8)
assert follower.tip() == b8
assert follower.unimported_orphans() == []

View File

@ -26,16 +26,16 @@ class TestStakeRelativization(TestCase):
assert follower.tip_state().leader_count == 1 assert follower.tip_state().leader_count == 1
# on fork, tip state is not updated # on fork, tip state is not updated
orphan = mk_block(genesis.block, slot=1, note=n_b) fork = mk_block(genesis.block, slot=1, note=n_b)
follower.on_block(orphan) follower.on_block(fork)
assert follower.tip_state().block == b1 assert follower.tip_state().block == b1
assert follower.tip_state().leader_count == 1 assert follower.tip_state().leader_count == 1
# after orphan is adopted, leader count should jumpy by 2 (each orphan counts as a leader) # continuing the chain increments the leader count
b2 = mk_block(b1, slot=2, note=n_a.evolve(), orphaned_proofs=[orphan]) b2 = mk_block(b1, slot=2, note=n_a.evolve())
follower.on_block(b2) follower.on_block(b2)
assert follower.tip_state().block == b2 assert follower.tip_state().block == b2
assert follower.tip_state().leader_count == 3 assert follower.tip_state().leader_count == 2
def test_inference_on_empty_genesis_epoch(self): def test_inference_on_empty_genesis_epoch(self):
note = Note(sk=0, value=10) note = Note(sk=0, value=10)