mirror of
https://github.com/logos-blockchain/logos-blockchain-specs.git
synced 2026-01-03 21:53:07 +00:00
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:
parent
f4b68f33cd
commit
dcdb419648
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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() == []
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user