From 7d8e4d72d998900e4924046792223120ae5458dd Mon Sep 17 00:00:00 2001 From: David Rusu Date: Thu, 1 Feb 2024 13:56:49 +0400 Subject: [PATCH] setup for ledger state update --- cryptarchia/cryptarchia.py | 128 +++++++++++++++++--------------- cryptarchia/test_fork_choice.py | 15 +++- cryptarchia/test_leader.py | 7 +- 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/cryptarchia/cryptarchia.py b/cryptarchia/cryptarchia.py index 8bce2a3..cb0f6e5 100644 --- a/cryptarchia/cryptarchia.py +++ b/cryptarchia/cryptarchia.py @@ -2,7 +2,7 @@ from typing import TypeAlias, List, Optional from hashlib import sha256, blake2b # Please note this is still a work in progress -from dataclasses import dataclass +from dataclasses import dataclass, field Id: TypeAlias = bytes @@ -42,12 +42,29 @@ class Config: time: TimeConfig +@dataclass +class MockLeaderProof: + commitment: Id + nullifier: Id + + def verify(self): + # TODO: verification not implemented + return True + + def _id_update(self, hasher): + commitment_bytes = int.to_bytes(self.commitment, length=32, byteorder="little") + nullifier_bytes = int.to_bytes(self.nullifier, length=32, byteorder="little") + hasher.update(commitment_bytes) + hasher.update(nullifier_bytes) + + @dataclass class BlockHeader: slot: Slot parent: Id content_size: int content_id: Id + leader_proof: MockLeaderProof # **Attention**: # The ID of a block header is defined as the 32byte blake2b hash of its fields @@ -70,6 +87,10 @@ class BlockHeader: # parent assert len(self.parent) == 32 h.update(self.parent) + + # TODO: Leader proof component of block id is mocked here until CL is understood + self.leader_proof._id_update(h) + return h.digest() @@ -93,15 +114,43 @@ class Chain: return i +@dataclass +class LedgerState: + """ + A snapshot of the ledger state up to some block + """ + + block: Id = None + nonce: bytes = None + total_stake: int = None + commitments: set[Id] = field(default_factory=set) # set of commitments + nullifiers: set[Id] = field(default_factory=set) # set of nullified + + def is_coin_nullified(self, nullifier: Id) -> bool: + return nullifier in self.nullifiers + + class Follower: - def __init__(self, genesis: BlockHeader, config: Config): + def __init__(self, genesis_state: LedgerState, config: Config): self.config = config self.forks = [] - self.local_chain = Chain([genesis]) + self.local_chain = Chain([]) + self.epoch = EpochState( + stake_distribution_snapshot=genesis_state, + nonce_snapshot=genesis_state, + ) + self.ledger_state = genesis_state - # We don't do any validation in the current version def validate_header(block: BlockHeader) -> bool: - return True + # TODO: this is not the full block validation spec, only slot leader is verified + return self.verify_slot_leader(block.slot, block.leader_proof) + + def verify_slot_leader(self, slot: Slot, proof: MockLeaderProof) -> bool: + return ( + proof.verify(slot) # verify slot leader proof + and self.epoch.is_coin_old_enough_to_lead(proof.coin) # verify coin was not recently created + and not self.ledger_state.is_coin_nullified(proof.nullifier) # verify the coin has not already been spent + ) # Try appending this block to an existing chain and return whether # the operation was successful @@ -177,38 +226,11 @@ class Coin: return h.digest() -@dataclass -class LedgerState: - """ - A snapshot of the ledger state up to some block - """ - - block: Id = None - nonce: bytes = None - total_stake: int = None - commitments: set[Id] = set() # set of commitments - nullifiers: set[Id] = set() # set of nullified - - -@dataclass -class LeaderProof: - epoch: int - slot: Slot - commitment: Id - nullifier: Id - - def verify(self): - # TODO: verification not implemented - return True - - @dataclass class EpochState: # for details of snapshot schedule please see: # https://github.com/IntersectMBO/ouroboros-consensus/blob/fe245ac1d8dbfb563ede2fdb6585055e12ce9738/docs/website/contents/for-developers/Glossary.md#epoch-structure - epoch_number: int - # The stake distribution snapshot is taken at the beginning of the previous epoch stake_distribution_snapshot: LedgerState @@ -218,9 +240,6 @@ class EpochState: def is_coin_old_enough_to_lead(self, coin: Coin) -> bool: return coin in self.stake_distribution.commitments - def is_nullified(self, nullifier: Id) -> bool: - return nullifier in self.stake_distribution.nullifiers - def total_stake(self) -> int: """Returns the total stake that will be used to reletivize leadership proofs during this epoch""" return self.stake_distribution_snapshot.total_stake @@ -263,40 +282,31 @@ class MOCK_LEADER_VRF: raise NotImplemented() -def is_slot_leader( - config: LeaderConfig, coin: Coin, epoch: EpochState, slot: Slot -) -> bool: - relative_stake = coin.value / epoch.total_stake() - - r = MOCK_LEADER_VRF.vrf(coin.pk, epoch.nonce(), slot) - - return r < MOCK_LEADER_VRF.ORDER * phi(config.active_slot_coeff, relative_stake) - - @dataclass class Leader: config: LeaderConfig coin: Coin - def try_slot_leader_proof( + def try_prove_slot_leader( self, epoch: EpochState, slot: Slot - ) -> LeaderProof | None: - if is_slot_leader(self.config, self.coin, epoch, slot): - return LeaderProof(epoch.epoch_number, slot, self.coin) - - def verify_slot_leader_proof( - self, epoch: EpochState, slot: Slot, proof: LeaderProof - ) -> bool: - return ( - proof.verify() - and epoch.is_coin_old_enough_to_lead(proof.coin) - and not epoch.is_coin_nullified(proof.nullifier) - and is_slot_leader(self.config, proof.coin, epoch, slot) - ) + ) -> MockLeaderProof | None: + if self._is_slot_leader(epoch, slot): + return MockLeaderProof( + commitment=self.coin.commitment(), nullifier=self.coin.nullifier() + ) def propose_block(self, slot: Slot, parent: BlockHeader) -> BlockHeader: return BlockHeader(parent=parent.id(), slot=slot) + def _is_slot_leader(self, epoch: EpochState, slot: Slot): + relative_stake = self.coin.value / epoch.total_stake() + + r = MOCK_LEADER_VRF.vrf(self.coin.pk, epoch.nonce(), slot) + + return r < MOCK_LEADER_VRF.ORDER * phi( + self.config.active_slot_coeff, relative_stake + ) + def common_prefix_len(a: Chain, b: Chain) -> int: for i, (x, y) in enumerate(zip(a.blocks, b.blocks)): diff --git a/cryptarchia/test_fork_choice.py b/cryptarchia/test_fork_choice.py index 3f4f8b4..b281dee 100644 --- a/cryptarchia/test_fork_choice.py +++ b/cryptarchia/test_fork_choice.py @@ -4,14 +4,25 @@ import numpy as np import hashlib from copy import deepcopy -from cryptarchia.cryptarchia import maxvalid_bg, Chain, BlockHeader, Slot, Id +from cryptarchia.cryptarchia import ( + maxvalid_bg, + Chain, + BlockHeader, + Slot, + Id, + MockLeaderProof, +) def make_block(parent_id: Id, slot: Slot, content: bytes) -> BlockHeader: assert len(parent_id) == 32 content_id = hashlib.sha256(content).digest() return BlockHeader( - parent=parent_id, content_size=1, slot=slot, content_id=content_id + parent=parent_id, + content_size=1, + slot=slot, + content_id=content_id, + leader_proof=MockLeaderProof(commitment=0, nullifier=0), ) diff --git a/cryptarchia/test_leader.py b/cryptarchia/test_leader.py index 6ea01f1..ffa5e43 100644 --- a/cryptarchia/test_leader.py +++ b/cryptarchia/test_leader.py @@ -7,7 +7,7 @@ from .cryptarchia import Leader, LeaderConfig, EpochState, LedgerState, Coin, ph class TestLeader(TestCase): def test_slot_leader_statistics(self): - epoch_state = EpochState( + epoch = EpochState( stake_distribution_snapshot=LedgerState( total_stake=1000, ), @@ -27,7 +27,10 @@ class TestLeader(TestCase): N = int((Z * std / margin_of_error) ** 2) # After N slots, the measured leader rate should be within the interval `p +- margin_of_error` with high probabiltiy - leader_rate = sum(l.is_slot_leader(epoch_state, slot) for slot in range(N)) / N + leader_rate = ( + sum(l.try_prove_slot_leader(epoch, slot) is not None for slot in range(N)) + / N + ) assert ( abs(leader_rate - p) < margin_of_error ), f"{leader_rate} != {p}, err={abs(leader_rate - p)} > {margin_of_error}"