Cryptarchia/drop orphan proofs (#121)

* cryptarchia: introduce Hash class

* cryptarchia: Coin renamed to Note

* cryptarchia: simplify mock leader proof

* cryptarchia: remove orphan proofs from block headers

* cryptarchia: maintain a single commitment set in ledger state

* cryptarchia: drop note evolution

* cryptarchia: drop MOCK_LEADER_VRF

* cryptarchia fix nonce contribution derivation

* cryptarchia: mk_chain only returns list now

* fixup

* cryptarchia: shorten test cases using mk_chain
This commit is contained in:
davidrusu 2025-03-21 20:49:04 +04:00 committed by GitHub
parent f4b68f33cd
commit dcdb419648
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 361 additions and 864 deletions

View File

@ -13,7 +13,19 @@ import numpy as np
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Id: TypeAlias = bytes class Hash(bytes):
ORDER = 2**256
def __new__(cls, dst, *data):
assert isinstance(dst, bytes)
h = sha256()
h.update(dst)
for d in data:
h.update(d)
return super().__new__(cls, h.digest())
def __deepcopy__(self, memo):
return self
@dataclass(frozen=True) @dataclass(frozen=True)
@ -128,10 +140,16 @@ class Slot:
@dataclass @dataclass
class Coin: class Note:
sk: int
value: int value: int
nonce: bytes = bytes(32) sk: int # TODO: rename to nf_sk
nonce: Hash = Hash(b"nonce")
unit: Hash = Hash(b"NMO")
state: Hash = Hash(b"state")
zone_id: Hash = Hash(b"ZoneID")
def __post_init__(self):
assert 0 <= self.value <= 2**64
@property @property
def pk(self) -> int: def pk(self) -> int:
@ -143,112 +161,73 @@ class Coin:
def encode_pk(self) -> bytes: def encode_pk(self) -> bytes:
return int.to_bytes(self.pk, length=32, byteorder="big") return int.to_bytes(self.pk, length=32, byteorder="big")
def evolve(self) -> "Coin": def commitment(self) -> Hash:
h = blake2b(digest_size=32)
h.update(b"coin-evolve")
h.update(self.encode_sk())
h.update(self.nonce)
evolved_nonce = h.digest()
return Coin(nonce=evolved_nonce, sk=self.sk, value=self.value)
def commitment(self) -> Id:
# TODO: mocked until CL is understood
value_bytes = int.to_bytes(self.value, length=32, byteorder="big") value_bytes = int.to_bytes(self.value, length=32, byteorder="big")
return Hash(
b"NOMOS_NOTE_CM",
self.state,
value_bytes,
self.unit,
self.nonce,
self.encode_pk(),
self.zone_id,
)
h = sha256() def nullifier(self) -> Hash:
h.update(b"coin-commitment") return Hash(b"NOMOS_NOTE_NF", self.commitment(), self.encode_sk())
h.update(self.nonce)
h.update(self.encode_pk())
h.update(value_bytes)
return h.digest()
def nullifier(self) -> Id:
# TODO: mocked until CL is understood
value_bytes = int.to_bytes(self.value, length=32, byteorder="big")
h = sha256()
h.update(b"coin-nullifier")
h.update(self.nonce)
h.update(self.encode_pk())
h.update(value_bytes)
return h.digest()
@dataclass @dataclass
class MockLeaderProof: class MockLeaderProof:
commitment: Id note: Note
nullifier: Id
evolved_commitment: Id
slot: Slot slot: Slot
parent: Id parent: Hash
@staticmethod def epoch_nonce_contribution(self) -> Hash:
def new(coin: Coin, slot: Slot, parent: Id): return Hash(
evolved_coin = coin.evolve() b"NOMOS_NONCE_CONTRIB",
self.slot.encode(),
return MockLeaderProof( self.note.commitment(),
commitment=coin.commitment(), self.note.encode_sk(),
nullifier=coin.nullifier(),
evolved_commitment=evolved_coin.commitment(),
slot=slot,
parent=parent,
) )
def verify(self, slot: Slot, parent: Id): def verify(
# TODO: verification not implemented self, slot: Slot, parent: Hash, commitments: set[Hash], nullifiers: set[Hash]
return slot == self.slot and parent == self.parent ):
# TODO: verify slot lottery
return (
slot == self.slot
and parent == self.parent
and self.note.commitment() in commitments
and self.note.nullifier() not in nullifiers
)
@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.epoch_nonce_contribution(),
# self.leader_proof -- the proof itself needs to be include in the hash
)
def __hash__(self): def __hash__(self):
return hash(self.id()) return hash(self.id())
@ -264,23 +243,17 @@ class LedgerState:
# This nonce is used to derive the seed for the slot leader lottery. # 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 # It's updated at every block by hashing the previous nonce with the
# leader proof's nullifier. # leader proof's nonce contribution
# nonce: Hash = None
# NOTE that this does not prevent nonce grinding at the last slot
# when the nonce snapshot is taken
nonce: Id = None
# set of commitments # set of note commitments
commitments_spend: set[Id] = field(default_factory=set) commitments: set[Hash] = field(default_factory=set)
# set of commitments eligible to lead # set of nullified notes
commitments_lead: set[Id] = field(default_factory=set) nullifiers: set[Hash] = field(default_factory=set)
# set of nullified coins
nullifiers: set[Id] = 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
@ -288,8 +261,7 @@ class LedgerState:
return LedgerState( return LedgerState(
block=self.block, block=self.block,
nonce=self.nonce, nonce=self.nonce,
commitments_spend=deepcopy(self.commitments_spend), commitments=deepcopy(self.commitments),
commitments_lead=deepcopy(self.commitments_lead),
nullifiers=deepcopy(self.nullifiers), nullifiers=deepcopy(self.nullifiers),
leader_count=self.leader_count, leader_count=self.leader_count,
) )
@ -297,34 +269,17 @@ 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:
return commitment in self.commitments_spend
def verify_eligible_to_lead(self, commitment: Id) -> bool:
return commitment in self.commitments_lead
def verify_unspent(self, nullifier: Id) -> bool:
return nullifier not in self.nullifiers
def apply(self, block: BlockHeader): def apply(self, block: BlockHeader):
assert block.parent == self.block.id() assert block.parent == self.block.id()
h = blake2b(digest_size=32) self.nonce = Hash(
h.update("epoch-nonce".encode(encoding="utf-8")) b"EPOCH_NONCE",
h.update(self.nonce) self.nonce,
h.update(block.leader_proof.nullifier) block.leader_proof.epoch_nonce_contribution(),
h.update(block.slot.encode()) block.slot.encode(),
)
self.nonce = h.digest()
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):
self.nullifiers.add(proof.nullifier)
self.commitments_spend.add(proof.evolved_commitment)
self.commitments_lead.add(proof.evolved_commitment)
self.leader_count += 1 self.leader_count += 1
self.block = block
@dataclass @dataclass
@ -343,20 +298,10 @@ 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:
# A coin 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
# since the snapshot was taken.
#
# This verification is checking that first condition.
#
# NOTE: `ledger_state.commitments_spend` is a super-set of `ledger_state.commitments_lead`
return self.stake_distribution_snapshot.verify_eligible_to_spend(commitment)
def total_active_stake(self) -> int: def total_active_stake(self) -> int:
""" """
Returns the inferred total stake participating in consensus. Returns the inferred total stake participating in consensus.
Total active stake is used to reletivize a coin's value in leadership proofs. Total active stake is used to reletivize a note's value in leadership proofs.
""" """
return self.inferred_total_active_stake return self.inferred_total_active_stake
@ -367,7 +312,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()}
@ -380,70 +325,19 @@ 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 block.leader_proof.verify(
block.slot, block.slot,
block.parent, block.parent,
block.leader_proof, epoch_state.stake_distribution_snapshot.commitments,
epoch_state, current_state.nullifiers,
current_state,
): ):
raise InvalidLeaderProof raise InvalidLeaderProof
def verify_slot_leader(
self,
slot: Slot,
parent: Id,
proof: MockLeaderProof,
# coins are old enough if their commitment is in the stake distribution snapshot
epoch_state: EpochState,
# nullifiers (and commitments) are checked against the current state.
# For now, we assume proof parent state and current state are identical.
# This will change once we start putting merkle roots in headers
current_state: LedgerState,
) -> bool:
return (
proof.verify(slot, parent) # verify slot leader proof
and (
current_state.verify_eligible_to_lead(proof.commitment)
or epoch_state.verify_eligible_to_lead_due_to_age(proof.commitment)
)
and current_state.verify_unspent(proof.nullifier)
)
def apply_block_to_ledger_state(self, block: BlockHeader) -> bool: def apply_block_to_ledger_state(self, block: BlockHeader) -> bool:
if block.id() in self.ledger_state: if block.id() in self.ledger_state:
logger.warning("dropping already processed block") logger.warning("dropping already processed block")
@ -487,29 +381,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,
@ -521,13 +394,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
@ -536,7 +409,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)
@ -551,7 +424,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,
@ -632,50 +505,34 @@ def phi(f: float, alpha: float) -> float:
return 1 - (1 - f) ** alpha return 1 - (1 - f) ** alpha
class MOCK_LEADER_VRF:
"""NOT SECURE: A mock VRF function"""
ORDER = 2**256
@classmethod
def vrf(cls, coin: Coin, epoch_nonce: bytes, slot: Slot) -> int:
h = sha256()
h.update(b"lead")
h.update(epoch_nonce)
h.update(slot.encode())
h.update(coin.encode_sk())
h.update(coin.nonce)
return int.from_bytes(h.digest())
@classmethod
def verify(cls, r, pk, nonce, slot):
raise NotImplemented()
@dataclass @dataclass
class Leader: class Leader:
config: Config config: Config
coin: Coin 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.new(self.coin, slot, parent) return MockLeaderProof(self.note, slot, parent)
def _is_slot_leader(self, epoch: EpochState, slot: Slot): def _is_slot_leader(self, epoch: EpochState, slot: Slot):
relative_stake = self.coin.value / epoch.total_active_stake() relative_stake = self.note.value / epoch.total_active_stake()
r = MOCK_LEADER_VRF.vrf(self.coin, epoch.nonce(), slot) ticket = Hash(
b"LEAD",
return r < MOCK_LEADER_VRF.ORDER * phi( epoch.nonce(),
self.config.active_slot_coeff, relative_stake slot.encode(),
self.note.commitment(),
self.note.encode_sk(),
) )
ticket = int.from_bytes(ticket)
return ticket < Hash.ORDER * phi(self.config.active_slot_coeff, relative_stake)
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]
@ -683,14 +540,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)
@ -748,7 +605,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)
@ -764,14 +621,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:
@ -802,16 +659,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

@ -1,19 +1,20 @@
from .cryptarchia import ( from .cryptarchia import (
Config, Config,
Slot, Slot,
Coin, Note,
BlockHeader, BlockHeader,
LedgerState, LedgerState,
MockLeaderProof, MockLeaderProof,
Leader, Leader,
Follower, Follower,
Hash,
) )
class TestNode: class TestNode:
def __init__(self, config: Config, genesis: LedgerState, coin: Coin): def __init__(self, config: Config, genesis: LedgerState, note: Note):
self.config = config self.config = config
self.leader = Leader(coin=coin, config=config) self.leader = Leader(note=note, config=config)
self.follower = Follower(genesis, config) self.follower = Follower(genesis, config)
def epoch_state(self, slot: Slot): def epoch_state(self, slot: Slot):
@ -25,11 +26,9 @@ class TestNode:
parent = self.follower.tip_id() parent = self.follower.tip_id()
epoch_state = self.epoch_state(slot) epoch_state = self.epoch_state(slot)
if leader_proof := self.leader.try_prove_slot_leader(epoch_state, slot, parent): if leader_proof := self.leader.try_prove_slot_leader(epoch_state, slot, parent):
self.leader.coin = self.leader.coin.evolve()
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),
@ -40,57 +39,51 @@ class TestNode:
self.follower.on_block(block) self.follower.on_block(block)
def mk_config(initial_stake_distribution: list[Coin]) -> Config: def mk_config(initial_stake_distribution: list[Note]) -> Config:
initial_inferred_total_stake = sum(c.value for c in initial_stake_distribution) initial_inferred_total_stake = sum(n.value for n in initial_stake_distribution)
return Config.cryptarchia_v0_0_1(initial_inferred_total_stake).replace( return Config.cryptarchia_v0_0_1(initial_inferred_total_stake).replace(
k=1, k=1,
active_slot_coeff=0.5, active_slot_coeff=0.5,
) )
def mk_genesis_state(initial_stake_distribution: list[Coin]) -> LedgerState: def mk_genesis_state(initial_stake_distribution: list[Note]) -> LedgerState:
return LedgerState( return LedgerState(
block=BlockHeader( block=BlockHeader(
slot=Slot(0), slot=Slot(0),
parent=bytes(32), parent=bytes(32),
content_size=0, content_size=0,
content_id=bytes(32), content_id=bytes(32),
leader_proof=MockLeaderProof.new( leader_proof=MockLeaderProof(
Coin(sk=0, value=0), Slot(0), parent=bytes(32) Note(sk=0, value=0), Slot(0), parent=bytes(32)
), ),
), ),
nonce=bytes(32), nonce=bytes(32),
commitments_spend={c.commitment() for c in initial_stake_distribution}, commitments={n.commitment() for n in initial_stake_distribution},
commitments_lead={c.commitment() for c in initial_stake_distribution},
nullifiers=set(), nullifiers=set(),
) )
def mk_block( def mk_block(
parent: BlockHeader, slot: int, coin: Coin, 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.new(coin, Slot(slot), parent=parent.id()), leader_proof=MockLeaderProof(note, Slot(slot), parent=parent.id()),
orphaned_proofs=orphaned_proofs,
) )
def mk_chain( def mk_chain(parent: BlockHeader, note: Note, slots: list[int]) -> list[BlockHeader]:
parent: BlockHeader, coin: Coin, slots: list[int]
) -> tuple[list[BlockHeader], Coin]:
assert type(parent) == BlockHeader assert type(parent) == BlockHeader
chain = [] chain = []
for s in slots: for s in slots:
block = mk_block(parent=parent, slot=s, coin=coin) block = mk_block(parent=parent, slot=s, note=note)
chain.append(block) chain.append(block)
parent = block parent = block
coin = coin.evolve() return chain
return chain, coin

View File

@ -4,7 +4,7 @@ from copy import deepcopy
from cryptarchia.cryptarchia import ( from cryptarchia.cryptarchia import (
maxvalid_bg, maxvalid_bg,
Slot, Slot,
Coin, Note,
Follower, Follower,
common_prefix_depth, common_prefix_depth,
LedgerState, LedgerState,
@ -21,16 +21,12 @@ class TestForkChoice(TestCase):
# \ # \
# 4 - 5 # 4 - 5
coin = Coin(sk=1, value=100) note = Note(sk=1, value=100)
b0 = mk_genesis_state([]).block b0 = mk_genesis_state([]).block
b1 = mk_block(b0, 1, coin) b1, b2, b3 = mk_chain(b0, Note(sk=1, value=1), slots=[1, 2, 3])
b2 = mk_block(b1, 2, coin) b4, b5 = mk_chain(b0, Note(sk=2, value=1), slots=[1, 2])
b3 = mk_block(b2, 3, coin) b6, b7 = mk_chain(b2, Note(sk=3, value=1), slots=[3, 4])
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 = { states = {
b.id(): LedgerState(block=b) for b in [b0, b1, b2, b3, b4, b5, b6, b7] b.id(): LedgerState(block=b) for b in [b0, b1, b2, b3, b4, b5, b6, b7]
@ -173,20 +169,20 @@ class TestForkChoice(TestCase):
# The longest chain is not dense after the fork # The longest chain is not dense after the fork
genesis = mk_genesis_state([]).block genesis = mk_genesis_state([]).block
short_coin, long_coin = Coin(sk=0, value=100), Coin(sk=1, value=100) short_note, long_note = Note(sk=0, value=100), Note(sk=1, value=100)
common, long_coin = mk_chain(parent=genesis, coin=long_coin, slots=range(50)) common = mk_chain(parent=genesis, note=long_note, slots=range(50))
long_chain_sparse_ext, long_coin = mk_chain( long_chain_sparse_ext = mk_chain(
parent=common[-1], coin=long_coin, slots=range(50, 100, 2) parent=common[-1], note=long_note, slots=range(50, 100, 2)
) )
short_chain_dense_ext, _ = mk_chain( short_chain_dense_ext = mk_chain(
parent=common[-1], coin=short_coin, slots=range(50, 100) parent=common[-1], note=short_note, slots=range(50, 100)
) )
# add more blocks to the long chain to ensure the long chain is indeed longer # add more blocks to the long chain to ensure the long chain is indeed longer
long_chain_further_ext, _ = mk_chain( long_chain_further_ext = mk_chain(
parent=long_chain_sparse_ext[-1], coin=long_coin, slots=range(100, 126) parent=long_chain_sparse_ext[-1], note=long_note, slots=range(100, 126)
) )
long_chain = deepcopy(common) + long_chain_sparse_ext + long_chain_further_ext long_chain = deepcopy(common) + long_chain_sparse_ext + long_chain_further_ext
@ -213,18 +209,18 @@ class TestForkChoice(TestCase):
def test_fork_choice_long_dense_chain(self): def test_fork_choice_long_dense_chain(self):
# The longest chain is also the densest after the fork # The longest chain is also the densest after the fork
short_coin, long_coin = Coin(sk=0, value=100), Coin(sk=1, value=100) short_note, long_note = Note(sk=0, value=100), Note(sk=1, value=100)
common, long_coin = mk_chain( common = mk_chain(
parent=mk_genesis_state([]).block, parent=mk_genesis_state([]).block,
coin=long_coin, note=long_note,
slots=range(1, 50), slots=range(1, 50),
) )
long_chain_dense_ext, _ = mk_chain( long_chain_dense_ext = mk_chain(
parent=common[-1], coin=long_coin, slots=range(50, 100) parent=common[-1], note=long_note, slots=range(50, 100)
) )
short_chain_sparse_ext, _ = mk_chain( short_chain_sparse_ext = mk_chain(
parent=common[-1], coin=short_coin, slots=range(50, 100, 2) parent=common[-1], note=short_note, slots=range(50, 100, 2)
) )
long_chain = deepcopy(common) + long_chain_dense_ext long_chain = deepcopy(common) + long_chain_dense_ext
@ -240,13 +236,13 @@ class TestForkChoice(TestCase):
) )
def test_fork_choice_integration(self): def test_fork_choice_integration(self):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
coins = [c_a, c_b] notes = [n_a, n_b]
config = mk_config(coins) config = mk_config(notes)
genesis = mk_genesis_state(coins) genesis = mk_genesis_state(notes)
follower = Follower(genesis, config) follower = Follower(genesis, config)
b1, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve() b1 = mk_block(genesis.block, 1, n_a)
follower.on_block(b1) follower.on_block(b1)
@ -262,8 +258,8 @@ class TestForkChoice(TestCase):
# b3 # b3
# #
b2, c_a = mk_block(b1, 2, c_a), c_a.evolve() b2 = mk_block(b1, 2, n_a)
b3, c_b = mk_block(b1, 2, c_b), c_b.evolve() b3 = mk_block(b1, 2, n_b)
follower.on_block(b2) follower.on_block(b2)
follower.on_block(b3) follower.on_block(b3)
@ -280,7 +276,7 @@ class TestForkChoice(TestCase):
# b3 - b4 == tip # b3 - b4 == tip
# #
b4, c_b = mk_block(b3, 3, c_b), c_a.evolve() b4 = mk_block(b3, 3, n_b)
follower.on_block(b4) follower.on_block(b4)
assert follower.tip_id() == b4.id() assert follower.tip_id() == b4.id()

View File

@ -2,7 +2,7 @@ from unittest import TestCase
import numpy as np import numpy as np
from .cryptarchia import Leader, EpochState, LedgerState, Coin, phi, Slot from .cryptarchia import Leader, EpochState, LedgerState, Note, phi, Slot
from .test_common import mk_config from .test_common import mk_config
@ -14,11 +14,11 @@ class TestLeader(TestCase):
inferred_total_active_stake=1000, inferred_total_active_stake=1000,
) )
coin = Coin(sk=0, value=10) note = Note(sk=0, value=10)
f = 0.05 f = 0.05
l = Leader( l = Leader(
config=mk_config([coin]).replace(active_slot_coeff=f), config=mk_config([note]).replace(active_slot_coeff=f),
coin=coin, note=note,
) )
# We'll use the Margin of Error equation to decide how many samples we need. # We'll use the Margin of Error equation to decide how many samples we need.

View File

@ -1,10 +1,9 @@
from unittest import TestCase from unittest import TestCase
from .cryptarchia import ( from .cryptarchia import (
Coin, Note,
Follower, Follower,
InvalidLeaderProof, InvalidLeaderProof,
MissingOrphanProof,
ParentNotFound, ParentNotFound,
iter_chain, iter_chain,
) )
@ -13,12 +12,12 @@ from .test_common import mk_block, mk_config, mk_genesis_state
class TestLedgerStateUpdate(TestCase): class TestLedgerStateUpdate(TestCase):
def test_on_block_idempotent(self): def test_on_block_idempotent(self):
leader_coin = Coin(sk=0, value=100) leader_note = Note(sk=0, value=100)
genesis = mk_genesis_state([leader_coin]) genesis = mk_genesis_state([leader_note])
follower = Follower(genesis, mk_config([leader_coin])) follower = Follower(genesis, mk_config([leader_note]))
block = mk_block(slot=0, parent=genesis.block, coin=leader_coin) block = mk_block(slot=0, parent=genesis.block, note=leader_note)
follower.on_block(block) follower.on_block(block)
# Follower should have accepted the block # Follower should have accepted the block
@ -33,47 +32,42 @@ class TestLedgerStateUpdate(TestCase):
assert len(follower.ledger_state) == 2 assert len(follower.ledger_state) == 2
assert len(follower.forks) == 0 assert len(follower.forks) == 0
def test_ledger_state_prevents_coin_reuse(self): def test_ledger_state_allows_note_reuse(self):
leader_coin = Coin(sk=0, value=100) leader_note = Note(sk=0, value=100)
genesis = mk_genesis_state([leader_coin]) genesis = mk_genesis_state([leader_note])
follower = Follower(genesis, mk_config([leader_coin])) follower = Follower(genesis, mk_config([leader_note]))
block = mk_block(slot=0, parent=genesis.block, coin=leader_coin) block = mk_block(slot=0, parent=genesis.block, note=leader_note)
follower.on_block(block) follower.on_block(block)
# Follower should have accepted the block # Follower should have accepted the block
assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 2 assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 2
assert follower.tip() == block assert follower.tip() == block
# Follower should have updated their ledger state to mark the leader coin as spent reuse_note_block = mk_block(slot=1, parent=block, note=leader_note)
assert follower.tip_state().verify_unspent(leader_coin.nullifier()) == False follower.on_block(reuse_note_block)
reuse_coin_block = mk_block(slot=1, parent=block, coin=leader_coin) # Follower should have accepted the block
with self.assertRaises(InvalidLeaderProof): assert len(list(iter_chain(follower.tip_id(), follower.ledger_state))) == 3
follower.on_block(reuse_coin_block) assert follower.tip() == reuse_note_block
# Follower should *not* have accepted the block
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): def test_ledger_state_is_properly_updated_on_reorg(self):
coin = [Coin(sk=0, value=100), Coin(sk=1, value=100), Coin(sk=2, value=100)] note = [Note(sk=0, value=100), Note(sk=1, value=100), Note(sk=2, value=100)]
genesis = mk_genesis_state(coin) genesis = mk_genesis_state(note)
follower = Follower(genesis, mk_config(coin)) follower = Follower(genesis, mk_config(note))
# 1) coin[0] & coin[1] both concurrently win slot 0 # 1) note[0] & note[1] both concurrently win slot 0
block_1 = mk_block(parent=genesis.block, slot=0, coin=coin[0]) block_1 = mk_block(parent=genesis.block, slot=0, note=note[0])
block_2 = mk_block(parent=genesis.block, slot=0, coin=coin[1]) block_2 = mk_block(parent=genesis.block, slot=0, note=note[1])
# 2) follower sees block 1 first # 2) follower sees block 1 first
follower.on_block(block_1) follower.on_block(block_1)
assert follower.tip() == block_1 assert follower.tip() == block_1
assert not follower.tip_state().verify_unspent(coin[0].nullifier())
# 3) then sees block 2, but sticks with block_1 as the tip # 3) then sees block 2, but sticks with block_1 as the tip
@ -81,46 +75,43 @@ class TestLedgerStateUpdate(TestCase):
assert follower.tip() == block_1 assert follower.tip() == block_1
assert len(follower.forks) == 1, f"{len(follower.forks)}" assert len(follower.forks) == 1, f"{len(follower.forks)}"
# 4) then coin[2] wins slot 1 and chooses to extend from block_2 # 4) then note[2] wins slot 1 and chooses to extend from block_2
block_3 = mk_block(parent=block_2, slot=1, coin=coin[2]) block_3 = mk_block(parent=block_2, slot=1, note=note[2])
follower.on_block(block_3) follower.on_block(block_3)
# the follower should have switched over to the block_2 fork # the follower should have switched over to the block_2 fork
assert follower.tip() == block_3 assert follower.tip() == block_3
# and the original coin[0] should now be removed from the spent pool
assert follower.tip_state().verify_unspent(coin[0].nullifier())
def test_fork_creation(self): def test_fork_creation(self):
coins = [Coin(sk=i, value=100) for i in range(7)] notes = [Note(sk=i, value=100) for i in range(7)]
genesis = mk_genesis_state(coins) genesis = mk_genesis_state(notes)
follower = Follower(genesis, mk_config(coins)) follower = Follower(genesis, mk_config(notes))
# coin_0 & coin_1 both concurrently win slot 0 based on the genesis block # note_0 & note_1 both concurrently win slot 0 based on the genesis block
# Both blocks are accepted, and a fork is created "from the genesis block" # Both blocks are accepted, and a fork is created "from the genesis block"
block_1 = mk_block(parent=genesis.block, slot=0, coin=coins[0]) block_1 = mk_block(parent=genesis.block, slot=0, note=notes[0])
block_2 = mk_block(parent=genesis.block, slot=0, coin=coins[1]) block_2 = mk_block(parent=genesis.block, slot=0, note=notes[1])
follower.on_block(block_1) follower.on_block(block_1)
follower.on_block(block_2) follower.on_block(block_2)
assert follower.tip() == block_1 assert follower.tip() == block_1
assert len(follower.forks) == 1, f"{len(follower.forks)}" assert len(follower.forks) == 1, f"{len(follower.forks)}"
assert follower.forks[0] == block_2.id() assert follower.forks[0] == block_2.id()
# coin_2 wins slot 1 and chooses to extend from block_1 # note_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 # note_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. # Both blocks are accepted. Both the local chain and the fork grow. No fork is newly created.
block_3 = mk_block(parent=block_1, slot=1, coin=coins[2]) block_3 = mk_block(parent=block_1, slot=1, note=notes[2])
block_4 = mk_block(parent=block_2, slot=1, coin=coins[3]) block_4 = mk_block(parent=block_2, slot=1, note=notes[3])
follower.on_block(block_3) follower.on_block(block_3)
follower.on_block(block_4) follower.on_block(block_4)
assert follower.tip() == block_3 assert follower.tip() == block_3
assert len(follower.forks) == 1, f"{len(follower.forks)}" assert len(follower.forks) == 1, f"{len(follower.forks)}"
assert follower.forks[0] == block_4.id() assert follower.forks[0] == block_4.id()
# coin_4 wins slot 1 and but chooses to extend from block_2 as well # note_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". # The block is accepted. A new fork is created "from the block_2".
block_5 = mk_block(parent=block_2, slot=1, coin=coins[4]) block_5 = mk_block(parent=block_2, slot=1, note=notes[4])
follower.on_block(block_5) follower.on_block(block_5)
assert follower.tip() == block_3 assert follower.tip() == block_3
assert len(follower.forks) == 2, f"{len(follower.forks)}" assert len(follower.forks) == 2, f"{len(follower.forks)}"
@ -129,8 +120,8 @@ class TestLedgerStateUpdate(TestCase):
# A block based on an unknown parent is not accepted. # A block based on an unknown parent is not accepted.
# Nothing changes from the local chain and forks. # Nothing changes from the local chain and forks.
unknown_block = mk_block(parent=block_5, slot=2, coin=coins[5]) unknown_block = mk_block(parent=block_5, slot=2, note=notes[5])
block_6 = mk_block(parent=unknown_block, slot=2, coin=coins[6]) block_6 = mk_block(parent=unknown_block, slot=2, note=notes[6])
with self.assertRaises(ParentNotFound): with self.assertRaises(ParentNotFound):
follower.on_block(block_6) follower.on_block(block_6)
assert follower.tip() == block_3 assert follower.tip() == block_3
@ -139,9 +130,9 @@ class TestLedgerStateUpdate(TestCase):
assert follower.forks[1] == block_5.id() assert follower.forks[1] == block_5.id()
def test_epoch_transition(self): def test_epoch_transition(self):
leader_coins = [Coin(sk=i, value=100) for i in range(4)] leader_notes = [Note(sk=i, value=100) for i in range(4)]
genesis = mk_genesis_state(leader_coins) genesis = mk_genesis_state(leader_notes)
config = mk_config(leader_coins) config = mk_config(leader_notes)
follower = Follower(genesis, config) follower = Follower(genesis, config)
@ -150,19 +141,19 @@ class TestLedgerStateUpdate(TestCase):
# ---- EPOCH 0 ---- # ---- EPOCH 0 ----
block_1 = mk_block(slot=0, parent=genesis.block, coin=leader_coins[0]) block_1 = mk_block(slot=0, parent=genesis.block, note=leader_notes[0])
follower.on_block(block_1) follower.on_block(block_1)
assert follower.tip() == block_1 assert follower.tip() == block_1
assert follower.tip().slot.epoch(config).epoch == 0 assert follower.tip().slot.epoch(config).epoch == 0
block_2 = mk_block(slot=19, parent=block_1, coin=leader_coins[1]) block_2 = mk_block(slot=19, parent=block_1, note=leader_notes[1])
follower.on_block(block_2) follower.on_block(block_2)
assert follower.tip() == block_2 assert follower.tip() == block_2
assert follower.tip().slot.epoch(config).epoch == 0 assert follower.tip().slot.epoch(config).epoch == 0
# ---- EPOCH 1 ---- # ---- EPOCH 1 ----
block_3 = mk_block(slot=20, parent=block_2, coin=leader_coins[2]) block_3 = mk_block(slot=20, parent=block_2, note=leader_notes[2])
follower.on_block(block_3) follower.on_block(block_3)
assert follower.tip() == block_3 assert follower.tip() == block_3
assert follower.tip().slot.epoch(config).epoch == 1 assert follower.tip().slot.epoch(config).epoch == 1
@ -171,48 +162,51 @@ class TestLedgerStateUpdate(TestCase):
# when trying to propose a block for epoch 2, the stake distribution snapshot should be taken # when trying to propose a block for epoch 2, the stake distribution snapshot should be taken
# at the end of epoch 0, i.e. slot 9 # at the end of epoch 0, i.e. slot 9
# To ensure this is the case, we add a new coin just to the state associated with that slot, # To ensure this is the case, we add a new note just to the state associated with that slot,
# so that the new block can be accepted only if that is the snapshot used # 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 # first, verify that if we don't change the state, the block is not accepted
block_4 = mk_block(slot=40, parent=block_3, coin=Coin(sk=4, value=100)) block_4 = mk_block(slot=40, parent=block_3, note=Note(sk=4, value=100))
with self.assertRaises(InvalidLeaderProof): with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_4) follower.on_block(block_4)
assert follower.tip() == block_3 assert follower.tip() == block_3
# then we add the coin to "spendable commitments" associated with slot 9 # then we add the note to "commitments" associated with slot 9
follower.ledger_state[block_2.id()].commitments_spend.add( follower.ledger_state[block_2.id()].commitments.add(
Coin(sk=4, value=100).commitment() Note(sk=4, value=100).commitment()
) )
follower.on_block(block_4) follower.on_block(block_4)
assert follower.tip() == block_4 assert follower.tip() == block_4
assert follower.tip().slot.epoch(config).epoch == 2 assert follower.tip().slot.epoch(config).epoch == 2
def test_evolved_coin_is_eligible_for_leadership(self): def test_note_added_after_stake_freeze_is_ineligible_for_leadership(self):
coin = Coin(sk=0, value=100) note = Note(sk=0, value=100)
genesis = mk_genesis_state([coin]) genesis = mk_genesis_state([note])
follower = Follower(genesis, mk_config([coin])) follower = Follower(genesis, mk_config([note]))
# coin wins the first slot # note wins the first slot
block_1 = mk_block(slot=0, parent=genesis.block, coin=coin) block_1 = mk_block(slot=0, parent=genesis.block, note=note)
follower.on_block(block_1) follower.on_block(block_1)
assert follower.tip() == block_1 assert follower.tip() == block_1
# coin can't be reused to win following slots: # note can be reused to win following slots:
block_2_reuse = mk_block(slot=1, parent=block_1, coin=coin) block_2 = mk_block(slot=1, parent=block_1, note=note)
follower.on_block(block_2)
assert follower.tip() == block_2
# but the a new note is ineligible
note_new = Note(sk=1, value=10)
follower.tip_state().commitments.add(note_new.commitment())
block_3_new = mk_block(slot=2, parent=block_2, note=note_new)
with self.assertRaises(InvalidLeaderProof): with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_2_reuse) follower.on_block(block_3_new)
assert follower.tip() == block_1
# but the evolved coin is eligible assert follower.tip() == block_2
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
def test_new_coins_becoming_eligible_after_stake_distribution_stabilizes(self): def test_new_notes_becoming_eligible_after_stake_distribution_stabilizes(self):
coin = Coin(sk=0, value=100) note = Note(sk=0, value=100)
config = mk_config([coin]) config = mk_config([note])
genesis = mk_genesis_state([coin]) genesis = mk_genesis_state([note])
follower = Follower(genesis, config) follower = Follower(genesis, config)
# We assume an epoch length of 20 slots in this test. # We assume an epoch length of 20 slots in this test.
@ -220,89 +214,34 @@ class TestLedgerStateUpdate(TestCase):
# ---- EPOCH 0 ---- # ---- EPOCH 0 ----
block_0_0 = mk_block(slot=0, parent=genesis.block, coin=coin) block_0_0 = mk_block(slot=0, parent=genesis.block, note=note)
follower.on_block(block_0_0) follower.on_block(block_0_0)
assert follower.tip() == block_0_0 assert follower.tip() == block_0_0
# mint a new coin to be used for leader elections in upcoming epochs # mint a new note to be used for leader elections in upcoming epochs
coin_new = Coin(sk=1, value=10) note_new = Note(sk=1, value=10)
follower.ledger_state[block_0_0.id()].commitments_spend.add( follower.ledger_state[block_0_0.id()].commitments.add(note_new.commitment())
coin_new.commitment()
)
# the new coin is not yet eligible for elections # the new note is not yet eligible for elections
block_0_1_attempt = mk_block(slot=1, parent=block_0_0, coin=coin_new) block_0_1_attempt = mk_block(slot=1, parent=block_0_0, note=note_new)
with self.assertRaises(InvalidLeaderProof): with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_0_1_attempt) follower.on_block(block_0_1_attempt)
assert follower.tip() == block_0_0 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, coin=coin.evolve())
follower.on_block(block_0_1)
assert follower.tip() == block_0_1
# ---- EPOCH 1 ---- # ---- EPOCH 1 ----
# The newly minted coin is still not eligible in the following epoch since the # The newly minted note is still not eligible in the following epoch since the
# stake distribution snapshot is taken at the beginning of the previous epoch # stake distribution snapshot is taken at the beginning of the previous epoch
block_1_0 = mk_block(slot=20, parent=block_0_1, coin=coin_new) block_1_0_attempt = mk_block(slot=20, parent=block_0_0, note=note_new)
with self.assertRaises(InvalidLeaderProof): with self.assertRaises(InvalidLeaderProof):
follower.on_block(block_1_0) follower.on_block(block_1_0_attempt)
assert follower.tip() == block_0_1 assert follower.tip() == block_0_0
# ---- EPOCH 2 ---- # ---- EPOCH 2 ----
# The coin is finally eligible 2 epochs after it was first minted # The note is finally eligible 2 epochs after it was first minted
block_2_0 = mk_block(slot=40, parent=block_0_1, coin=coin_new) block_2_0 = mk_block(slot=40, parent=block_0_0, note=note_new)
follower.on_block(block_2_0) follower.on_block(block_2_0)
assert follower.tip() == 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, coin=coin_new.evolve())
follower.on_block(block_2_1)
assert follower.tip() == block_2_1
def test_orphaned_proofs(self):
coin, coin_orphan = Coin(sk=0, value=100), Coin(sk=1, value=100)
genesis = mk_genesis_state([coin, coin_orphan])
follower = Follower(genesis, mk_config([coin, coin_orphan]))
block_0_0 = mk_block(slot=0, parent=genesis.block, coin=coin)
follower.on_block(block_0_0)
assert follower.tip() == block_0_0
coin_new = coin.evolve()
coin_new_new = coin_new.evolve()
block_0_1 = mk_block(slot=1, parent=block_0_0, coin=coin_new_new)
with self.assertRaises(InvalidLeaderProof):
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
# 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 coin 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, coin=coin_orphan)
block_0_1 = mk_block(
slot=1,
parent=block_0_0,
coin=coin_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 Coin, Follower
from .test_common import mk_config, mk_genesis_state, mk_block
class TestOrphanedProofs(TestCase):
def test_simple_orphan_import(self):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10)
coins = [c_a, c_b]
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
# -- fork --
#
# b2 == tip
# /
# b1
# \
# b3
#
b1, c_a = mk_block(genesis.block, 1, c_a), c_a.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 for f in follower.forks] == [b3.id()]
assert follower.unimported_orphans() == [b3]
# -- extend with import --
#
# b2 - b4
# / /
# b1 /
# \ /
# b3
#
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 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)
coins = [c_a, c_b]
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
# -- fork --
#
# b2 - b3 == tip
# /
# b1
# \
# b4 - b5
#
b1, c_a = mk_block(genesis.block, 1, 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, 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 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, 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 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)]
c_a, c_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, c_a = mk_block(genesis.block, 1, 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, 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 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, 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 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 = [Coin(sk=i, value=10) for i in range(3)]
c_a, c_b, c_c = coins
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
b1, c_a = mk_block(genesis.block, 1, 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, 2, c_b), c_b.evolve()
b5, c_b = mk_block(b4, 3, c_b), c_b.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 for f in follower.forks] == [b5.id(), b6.id()]
assert follower.unimported_orphans() == [b4, b5, b6]
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
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 = [Coin(sk=i, value=10) for i in range(2)]
c_a, c_b = coins
config = mk_config(coins)
genesis = mk_genesis_state(coins)
follower = Follower(genesis, config)
b1, c_a = mk_block(genesis.block, 1, 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, 4, c_b), c_b.evolve()
b5, c_a = mk_block(b3, 4, c_a), 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, 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() == [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

@ -3,17 +3,17 @@ import itertools
import numpy as np import numpy as np
from .cryptarchia import Config, Coin, Slot from .cryptarchia import Config, Note, Slot
from .test_common import mk_config, mk_genesis_state, mk_block, TestNode, Follower from .test_common import mk_config, mk_genesis_state, mk_block, TestNode, Follower
class TestStakeRelativization(TestCase): class TestStakeRelativization(TestCase):
def test_ledger_leader_counting(self): def test_ledger_leader_counting(self):
coins = [Coin(sk=i, value=10) for i in range(2)] notes = [Note(sk=i, value=10) for i in range(2)]
c_a, c_b = coins n_a, n_b = notes
config = mk_config(coins) config = mk_config(notes)
genesis = mk_genesis_state(coins) genesis = mk_genesis_state(notes)
follower = Follower(genesis, config) follower = Follower(genesis, config)
@ -21,31 +21,31 @@ class TestStakeRelativization(TestCase):
assert follower.tip_state().leader_count == 0 assert follower.tip_state().leader_count == 0
# after a block, 1 leader has been observed # after a block, 1 leader has been observed
b1 = mk_block(genesis.block, slot=1, coin=c_a) b1 = mk_block(genesis.block, slot=1, note=n_a)
follower.on_block(b1) follower.on_block(b1)
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, coin=c_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, coin=c_a.evolve(), orphaned_proofs=[orphan]) b2 = mk_block(b1, slot=2, note=n_a)
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):
coin = Coin(sk=0, value=10) note = Note(sk=0, value=10)
config = mk_config([coin]).replace( config = mk_config([note]).replace(
initial_total_active_stake=20, initial_total_active_stake=20,
total_active_stake_learning_rate=0.5, total_active_stake_learning_rate=0.5,
active_slot_coeff=0.5, active_slot_coeff=0.5,
) )
genesis = mk_genesis_state([coin]) genesis = mk_genesis_state([note])
node = TestNode(config, genesis, coin) node = TestNode(config, genesis, note)
# -- epoch 0 -- # -- epoch 0 --
@ -77,12 +77,12 @@ class TestStakeRelativization(TestCase):
np.random.seed(seed) np.random.seed(seed)
stake = np.array((np.random.pareto(10, N) + 1) * 1000, dtype=np.int64) stake = np.array((np.random.pareto(10, N) + 1) * 1000, dtype=np.int64)
coins = [Coin(sk=i, value=int(s)) for i, s in enumerate(stake)] notes = [Note(sk=i, value=int(s)) for i, s in enumerate(stake)]
config = Config.cryptarchia_v0_0_1(stake.sum() * 2).replace(k=40) config = Config.cryptarchia_v0_0_1(stake.sum() * 2).replace(k=40)
genesis = mk_genesis_state(coins) genesis = mk_genesis_state(notes)
nodes = [TestNode(config, genesis, c) for c in coins] nodes = [TestNode(config, genesis, n) for n in notes]
T = config.epoch_length * EPOCHS T = config.epoch_length * EPOCHS
slot_leaders = np.zeros(T, dtype=np.int32) slot_leaders = np.zeros(T, dtype=np.int32)

View File

@ -1,22 +1,19 @@
from unittest import TestCase from unittest import TestCase
from cryptarchia.cryptarchia import BlockHeader, Coin, Follower from cryptarchia.cryptarchia import BlockHeader, Note, Follower
from cryptarchia.sync import InvalidBlockTree, sync from cryptarchia.sync import InvalidBlockTree, sync
from cryptarchia.test_common import mk_block, mk_config, mk_genesis_state from cryptarchia.test_common import mk_block, mk_chain, mk_config, mk_genesis_state
class TestSync(TestCase): class TestSync(TestCase):
def test_sync_single_chain_from_genesis(self): def test_sync_single_chain_from_genesis(self):
# Prepare a peer with a single chain: # Prepare a peer with a single chain:
# b0 - b1 - b2 - b3 # b0 - b1 - b2 - b3
coin = Coin(sk=0, value=10) note = Note(sk=0, value=10)
config = mk_config([coin]) config = mk_config([note])
genesis = mk_genesis_state([coin]) genesis = mk_genesis_state([note])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, coin = mk_block(genesis.block, 1, coin), coin.evolve() b0, b1, b2, b3 = mk_chain(genesis.block, note, slots=[1, 2, 3, 4])
b1, coin = mk_block(b0, 2, coin), coin.evolve()
b2, coin = mk_block(b1, 3, coin), coin.evolve()
b3, coin = mk_block(b2, 4, coin), coin.evolve()
for b in [b0, b1, b2, b3]: for b in [b0, b1, b2, b3]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b3) self.assertEqual(peer.tip(), b3)
@ -32,14 +29,11 @@ class TestSync(TestCase):
def test_sync_single_chain_from_middle(self): def test_sync_single_chain_from_middle(self):
# Prepare a peer with a single chain: # Prepare a peer with a single chain:
# b0 - b1 - b2 - b3 # b0 - b1 - b2 - b3
coin = Coin(sk=0, value=10) note = Note(sk=0, value=10)
config = mk_config([coin]) config = mk_config([note])
genesis = mk_genesis_state([coin]) genesis = mk_genesis_state([note])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, coin = mk_block(genesis.block, 1, coin), coin.evolve() b0, b1, b2, b3 = mk_chain(genesis.block, note, slots=[1, 2, 3, 4])
b1, coin = mk_block(b0, 2, coin), coin.evolve()
b2, coin = mk_block(b1, 3, coin), coin.evolve()
b3, coin = mk_block(b2, 4, coin), coin.evolve()
for b in [b0, b1, b2, b3]: for b in [b0, b1, b2, b3]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b3) self.assertEqual(peer.tip(), b3)
@ -61,18 +55,17 @@ class TestSync(TestCase):
# b0 - b1 - b2 - b5 == tip # b0 - b1 - b2 - b5 == tip
# \ # \
# b3 - b4 # b3 - b4
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b2, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4])
b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() b3, b4 = mk_chain(b0, n_b, slots=[2, 3])
b3, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b4, c_b = mk_block(b3, 3, c_b), c_b.evolve()
b5, c_a = mk_block(b2, 4, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]: for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b5) self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b4.id()]) self.assertEqual(peer.forks, [b4.id()])
@ -88,16 +81,14 @@ class TestSync(TestCase):
# b0 - b1 - b2 - b5 == tip # b0 - b1 - b2 - b5 == tip
# \ # \
# b3 - b4 # b3 - b4
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b2, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4])
b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() b3, b4 = mk_chain(b0, n_b, slots=[2, 3])
b3, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b4, c_b = mk_block(b3, 3, c_b), c_b.evolve()
b5, c_a = mk_block(b2, 4, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]: for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b5) self.assertEqual(peer.tip(), b5)
@ -121,16 +112,14 @@ class TestSync(TestCase):
# b0 - b1 - b2 - b5 == tip # b0 - b1 - b2 - b5 == tip
# \ # \
# b3 - b4 # b3 - b4
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b2, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4])
b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() b3, b4 = mk_chain(b0, n_b, slots=[2, 3])
b3, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b4, c_b = mk_block(b3, 3, c_b), c_b.evolve()
b5, c_a = mk_block(b2, 4, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]: for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b5) self.assertEqual(peer.tip(), b5)
@ -156,15 +145,13 @@ class TestSync(TestCase):
# Peer-1: b0 - b1 - b2 # Peer-1: b0 - b1 - b2
# \ # \
# Peer-2: b3 - b4 # Peer-2: b3 - b4
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b2, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4])
b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() b3, b4 = mk_chain(b0, n_b, slots=[2, 3])
b3, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b4, c_b = mk_block(b3, 3, c_b), c_b.evolve()
b5, c_a = mk_block(b2, 4, c_a), c_a.evolve()
peer0 = Follower(genesis, config) peer0 = Follower(genesis, config)
for b in [b0, b1, b2, b5]: for b in [b0, b1, b2, b5]:
peer0.on_block(b) peer0.on_block(b)
@ -200,23 +187,22 @@ class TestSync(TestCase):
# b0 - b1 - b2 - b3 - (invalid_b4) - (invalid_b5) # b0 - b1 - b2 - b3 - (invalid_b4) - (invalid_b5)
# #
# First, build a valid chain (b0 ~ b3): # First, build a valid chain (b0 ~ b3):
coin = Coin(sk=0, value=10) note = Note(sk=0, value=10)
config = mk_config([coin]) config = mk_config([note])
genesis = mk_genesis_state([coin]) genesis = mk_genesis_state([note])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, coin = mk_block(genesis.block, 1, coin), coin.evolve()
b1, coin = mk_block(b0, 2, coin), coin.evolve() b0, b1, b2, b3 = mk_chain(genesis.block, note, slots=[1, 2, 3, 4])
b2, coin = mk_block(b1, 3, coin), coin.evolve()
b3, coin = mk_block(b2, 4, coin), coin.evolve()
for b in [b0, b1, b2, b3]: for b in [b0, b1, b2, b3]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b3) self.assertEqual(peer.tip(), b3)
self.assertEqual(peer.forks, []) self.assertEqual(peer.forks, [])
# And deliberately, add invalid blocks (b4 ~ b5): # And deliberately, add invalid blocks (b4 ~ b5):
fake_coin = Coin(sk=1, value=10) fake_note = Note(sk=1, value=10)
b4, fake_coin = mk_block(b3, 5, fake_coin), fake_coin.evolve() b4 = mk_block(b3, 5, fake_note)
b5, fake_coin = mk_block(b4, 6, fake_coin), fake_coin.evolve() b5 = mk_block(b4, 6, fake_note)
apply_invalid_block_to_ledger_state(peer, b4) apply_invalid_block_to_ledger_state(peer, b4)
apply_invalid_block_to_ledger_state(peer, b5) apply_invalid_block_to_ledger_state(peer, b5)
# the tip shouldn't be changed. # the tip shouldn't be changed.
@ -239,25 +225,23 @@ class TestSync(TestCase):
# b2 - (invalid_b6) - (invalid_b7) # b2 - (invalid_b6) - (invalid_b7)
# #
# First, build a valid chain (b0 ~ b5): # First, build a valid chain (b0 ~ b5):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b3, b4, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4, 5])
b2, c_b = mk_block(b0, 2, c_b), c_b.evolve() b2 = mk_block(b0, 2, n_b)
b3, c_a = mk_block(b1, 3, c_a), c_a.evolve()
b4, c_a = mk_block(b3, 4, c_a), c_a.evolve()
b5, c_a = mk_block(b4, 5, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]: for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b5) self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()]) self.assertEqual(peer.forks, [b2.id()])
# And deliberately, add invalid blocks (b6 ~ b7): # And deliberately, add invalid blocks (b6 ~ b7):
fake_coin = Coin(sk=2, value=10) fake_note = Note(sk=2, value=10)
b6, fake_coin = mk_block(b2, 3, fake_coin), fake_coin.evolve() b6, b7 = mk_chain(b2, fake_note, slots=[3, 4])
b7, fake_coin = mk_block(b6, 4, fake_coin), fake_coin.evolve()
apply_invalid_block_to_ledger_state(peer, b6) apply_invalid_block_to_ledger_state(peer, b6)
apply_invalid_block_to_ledger_state(peer, b7) apply_invalid_block_to_ledger_state(peer, b7)
# the tip shouldn't be changed. # the tip shouldn't be changed.
@ -287,14 +271,13 @@ class TestSyncFromCheckpoint(TestCase):
# b0 - b1 - b2 - b3 # b0 - b1 - b2 - b3
# || # ||
# checkpoint # checkpoint
coin = Coin(sk=0, value=10) note = Note(sk=0, value=10)
config = mk_config([coin]) config = mk_config([note])
genesis = mk_genesis_state([coin]) genesis = mk_genesis_state([note])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, coin = mk_block(genesis.block, 1, coin), coin.evolve()
b1, coin = mk_block(b0, 2, coin), coin.evolve() b0, b1, b2, b3 = mk_chain(genesis.block, note, slots=[1, 2, 3, 4])
b2, coin = mk_block(b1, 3, coin), coin.evolve()
b3, coin = mk_block(b2, 4, coin), coin.evolve()
for b in [b0, b1, b2, b3]: for b in [b0, b1, b2, b3]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b3) self.assertEqual(peer.tip(), b3)
@ -324,16 +307,14 @@ class TestSyncFromCheckpoint(TestCase):
# b0 - b1 - b2 - b5 == tip # b0 - b1 - b2 - b5 == tip
# \ # \
# b3 - b4 # b3 - b4
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b2, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4])
b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() b3, b4 = mk_chain(b0, n_b, slots=[2, 3])
b3, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b4, c_b = mk_block(b3, 3, c_b), c_b.evolve()
b5, c_a = mk_block(b2, 4, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]: for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b5) self.assertEqual(peer.tip(), b5)
@ -363,15 +344,13 @@ class TestSyncFromCheckpoint(TestCase):
# Peer1: b3 - b4 # Peer1: b3 - b4
# || # ||
# checkpoint # checkpoint
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b2, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4])
b2, c_a = mk_block(b1, 3, c_a), c_a.evolve() b3, b4 = mk_chain(b0, n_b, slots=[2, 3])
b3, c_b = mk_block(b0, 2, c_b), c_b.evolve()
b4, c_b = mk_block(b3, 3, c_b), c_b.evolve()
b5, c_a = mk_block(b2, 4, c_a), c_a.evolve()
peer0 = Follower(genesis, config) peer0 = Follower(genesis, config)
for b in [b0, b1, b2, b5]: for b in [b0, b1, b2, b5]:
peer0.on_block(b) peer0.on_block(b)
@ -407,25 +386,22 @@ class TestSyncFromCheckpoint(TestCase):
# b2 - (invalid_b6) - (invalid_b7) # b2 - (invalid_b6) - (invalid_b7)
# #
# First, build a valid chain (b0 ~ b5): # First, build a valid chain (b0 ~ b5):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b3, b4, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4, 5])
b2, c_b = mk_block(b0, 2, c_b), c_b.evolve() b2 = mk_block(b0, 2, n_b)
b3, c_a = mk_block(b1, 3, c_a), c_a.evolve()
b4, c_a = mk_block(b3, 4, c_a), c_a.evolve()
b5, c_a = mk_block(b4, 5, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]: for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b5) self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()]) self.assertEqual(peer.forks, [b2.id()])
# And deliberately, add invalid blocks (b6 ~ b7): # And deliberately, add invalid blocks (b6 ~ b7):
fake_coin = Coin(sk=2, value=10) fake_note = Note(sk=2, value=10)
b6, fake_coin = mk_block(b2, 3, fake_coin), fake_coin.evolve() b6, b7 = mk_chain(b2, fake_note, slots=[3, 4])
b7, fake_coin = mk_block(b6, 4, fake_coin), fake_coin.evolve()
apply_invalid_block_to_ledger_state(peer, b6) apply_invalid_block_to_ledger_state(peer, b6)
apply_invalid_block_to_ledger_state(peer, b7) apply_invalid_block_to_ledger_state(peer, b7)
# the tip shouldn't be changed. # the tip shouldn't be changed.
@ -457,25 +433,22 @@ class TestSyncFromCheckpoint(TestCase):
# b2 - (invalid_b6) - (invalid_b7) # b2 - (invalid_b6) - (invalid_b7)
# #
# First, build a valid chain (b0 ~ b5): # First, build a valid chain (b0 ~ b5):
c_a, c_b = Coin(sk=0, value=10), Coin(sk=1, value=10) n_a, n_b = Note(sk=0, value=10), Note(sk=1, value=10)
config = mk_config([c_a, c_b]) config = mk_config([n_a, n_b])
genesis = mk_genesis_state([c_a, c_b]) genesis = mk_genesis_state([n_a, n_b])
peer = Follower(genesis, config) peer = Follower(genesis, config)
b0, c_a = mk_block(genesis.block, 1, c_a), c_a.evolve()
b1, c_a = mk_block(b0, 2, c_a), c_a.evolve() b0, b1, b3, b4, b5 = mk_chain(genesis.block, n_a, slots=[1, 2, 3, 4, 5])
b2, c_b = mk_block(b0, 2, c_b), c_b.evolve() b2 = mk_block(b0, 2, n_b)
b3, c_a = mk_block(b1, 3, c_a), c_a.evolve()
b4, c_a = mk_block(b3, 4, c_a), c_a.evolve()
b5, c_a = mk_block(b4, 5, c_a), c_a.evolve()
for b in [b0, b1, b2, b3, b4, b5]: for b in [b0, b1, b2, b3, b4, b5]:
peer.on_block(b) peer.on_block(b)
self.assertEqual(peer.tip(), b5) self.assertEqual(peer.tip(), b5)
self.assertEqual(peer.forks, [b2.id()]) self.assertEqual(peer.forks, [b2.id()])
# And deliberately, add invalid blocks (b6 ~ b7): # And deliberately, add invalid blocks (b6 ~ b7):
fake_coin = Coin(sk=2, value=10) fake_note = Note(sk=2, value=10)
b6, fake_coin = mk_block(b2, 3, fake_coin), fake_coin.evolve() b6, b7 = mk_chain(b2, fake_note, slots=[3, 4])
b7, fake_coin = mk_block(b6, 4, fake_coin), fake_coin.evolve()
apply_invalid_block_to_ledger_state(peer, b6) apply_invalid_block_to_ledger_state(peer, b6)
apply_invalid_block_to_ledger_state(peer, b7) apply_invalid_block_to_ledger_state(peer, b7)
# the tip shouldn't be changed. # the tip shouldn't be changed.