From eebf439a30996fd8af4c5eb1c8dc0f6738c964e9 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Tue, 6 Feb 2024 19:31:34 +0400 Subject: [PATCH 1/5] feat(leader_coin): add nonce and coin.evolve() api --- cryptarchia/cryptarchia.py | 32 ++++++++++++++++++++++--- cryptarchia/test_fork_choice.py | 2 +- cryptarchia/test_leader.py | 2 +- cryptarchia/test_ledger_state_update.py | 14 +++++------ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/cryptarchia/cryptarchia.py b/cryptarchia/cryptarchia.py index 2df0958..8a0a87d 100644 --- a/cryptarchia/cryptarchia.py +++ b/cryptarchia/cryptarchia.py @@ -79,8 +79,24 @@ class Slot: @dataclass class Coin: - pk: int + sk: int value: int + nonce: bytes = bytes(32) + + @property + def pk(self): + return self.sk + + def evolve(self) -> "Coin": + sk_bytes = int.to_bytes(self.sk, length=32, byteorder="little") + + h = blake2b(digest_size=32) + h.update(b"coin-evolve") + h.update(sk_bytes) + 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 @@ -88,6 +104,8 @@ class Coin: value_bytes = int.to_bytes(self.value, length=32, byteorder="little") h = sha256() + h.update(b"coin-commitment") + h.update(self.nonce) h.update(pk_bytes) h.update(value_bytes) return h.digest() @@ -98,9 +116,10 @@ class Coin: value_bytes = int.to_bytes(self.value, length=32, byteorder="little") h = sha256() + h.update(b"coin-nullifier") + h.update(self.nonce) h.update(pk_bytes) h.update(value_bytes) - h.update(b"\x00") # extra 0 byte to differentiate from commitment return h.digest() @@ -108,10 +127,17 @@ class Coin: class MockLeaderProof: commitment: Id nullifier: Id + evolved_commitment: Id @staticmethod def from_coin(coin: Coin): - return MockLeaderProof(commitment=coin.commitment(), nullifier=coin.nullifier()) + evolved_coin = coin.evolve() + + return MockLeaderProof( + commitment=coin.commitment(), + nullifier=coin.nullifier(), + evolved_commitment=evolved_coin.commitment(), + ) def verify(self, slot): # TODO: verification not implemented diff --git a/cryptarchia/test_fork_choice.py b/cryptarchia/test_fork_choice.py index 87dc379..93e5571 100644 --- a/cryptarchia/test_fork_choice.py +++ b/cryptarchia/test_fork_choice.py @@ -23,7 +23,7 @@ def make_block(parent_id: Id, slot: Slot, content: bytes) -> BlockHeader: content_size=1, slot=slot, content_id=content_id, - leader_proof=MockLeaderProof.from_coin(Coin(pk=0, value=10)), + leader_proof=MockLeaderProof.from_coin(Coin(sk=0, value=10)), ) diff --git a/cryptarchia/test_leader.py b/cryptarchia/test_leader.py index ffb5554..43de3ac 100644 --- a/cryptarchia/test_leader.py +++ b/cryptarchia/test_leader.py @@ -23,7 +23,7 @@ class TestLeader(TestCase): epoch_period_nonce_stabilization=3, time=TimeConfig(slot_duration=1, chain_start_time=0), ) - l = Leader(config=config, coin=Coin(pk=0, value=10)) + l = Leader(config=config, coin=Coin(sk=0, value=10)) # We'll use the Margin of Error equation to decide how many samples we need. # https://en.wikipedia.org/wiki/Margin_of_error diff --git a/cryptarchia/test_ledger_state_update.py b/cryptarchia/test_ledger_state_update.py index 6c8ec7e..50e0539 100644 --- a/cryptarchia/test_ledger_state_update.py +++ b/cryptarchia/test_ledger_state_update.py @@ -50,7 +50,7 @@ def config() -> Config: class TestLedgerStateUpdate(TestCase): def test_ledger_state_prevents_coin_reuse(self): - leader_coin = Coin(pk=0, value=100) + leader_coin = Coin(sk=0, value=100) genesis = mk_genesis_state([leader_coin]) follower = Follower(genesis, config()) @@ -76,9 +76,9 @@ class TestLedgerStateUpdate(TestCase): assert follower.local_chain.tip() == block def test_ledger_state_is_properly_updated_on_reorg(self): - coin_1 = Coin(pk=0, value=100) - coin_2 = Coin(pk=1, value=100) - coin_3 = Coin(pk=2, value=100) + coin_1 = Coin(sk=0, value=100) + coin_2 = Coin(sk=1, value=100) + coin_3 = Coin(sk=2, value=100) genesis = mk_genesis_state([coin_1, coin_2, coin_3]) @@ -114,7 +114,7 @@ class TestLedgerStateUpdate(TestCase): assert follower.ledger_state[block_3.id()].verify_unspent(coin_1.nullifier()) def test_epoch_transition(self): - leader_coins = [Coin(pk=i, value=100) for i in range(4)] + leader_coins = [Coin(sk=i, value=100) for i in range(4)] genesis = mk_genesis_state(leader_coins) # An epoch will be 10 slots long, with stake distribution snapshot taken at the start of the epoch @@ -145,12 +145,12 @@ class TestLedgerStateUpdate(TestCase): # To ensure this is the case, we add a new coin just to the state associated with that slot, # so that the new block can be accepted only if that is the snapshot used # first, verify that if we don't change the state, the block is not accepted - block_4 = mk_block(slot=20, parent=block_3.id(), coin=Coin(pk=4, value=100)) + block_4 = mk_block(slot=20, parent=block_3.id(), coin=Coin(sk=4, value=100)) follower.on_block(block_4) assert follower.tip() == block_3 # then we add the coin to the state associated with slot 9 follower.ledger_state[block_2.id()].commitments.add( - Coin(pk=4, value=100).commitment() + Coin(sk=4, value=100).commitment() ) follower.on_block(block_4) assert follower.tip() == block_4 From 5c3de9ab84a886c66a4fe8b269164799ba6cf7dd Mon Sep 17 00:00:00 2001 From: David Rusu Date: Tue, 6 Feb 2024 20:07:26 +0400 Subject: [PATCH 2/5] implement support for leader-proofs generated from evolved coins --- cryptarchia/cryptarchia.py | 39 ++++++++++++++++++----- cryptarchia/test_ledger_state_update.py | 41 +++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/cryptarchia/cryptarchia.py b/cryptarchia/cryptarchia.py index 8a0a87d..42cddd2 100644 --- a/cryptarchia/cryptarchia.py +++ b/cryptarchia/cryptarchia.py @@ -215,20 +215,31 @@ class LedgerState: block: Id = None nonce: Id = 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 + + # set of commitments + commitments_spend: set[Id] = field(default_factory=set) + + # set of commitments elligible to lead + commitments_lead: set[Id] = field(default_factory=set) + + # set of nullified coins + nullifiers: set[Id] = field(default_factory=set) def copy(self): return LedgerState( block=self.block, nonce=self.nonce, total_stake=self.total_stake, - commitments=deepcopy(self.commitments), + commitments_spend=deepcopy(self.commitments_spend), + commitments_lead=deepcopy(self.commitments_lead), nullifiers=deepcopy(self.nullifiers), ) - def verify_committed(self, commitment: Id) -> bool: - return commitment in self.commitments + def verify_elligible_to_spend(self, commitment: Id) -> bool: + return commitment in self.commitments_spend + + def verify_elligible_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 @@ -237,6 +248,8 @@ class LedgerState: assert block.parent == self.block self.block = block.id() self.nullifiers.add(block.leader_proof.nullifier) + self.commitments_spend.add(block.leader_proof.evolved_commitment) + self.commitments_lead.add(block.leader_proof.evolved_commitment) @dataclass @@ -250,8 +263,15 @@ class EpochState: # The nonce snapshot is taken 7k/f slots into the previous epoch nonce_snapshot: LedgerState - def verify_commitment_is_old_enough_to_lead(self, commitment: Id) -> bool: - return self.stake_distribution_snapshot.verify_committed(commitment) + def verify_elligible_to_lead_due_to_age(self, commitment: Id) -> bool: + # A coin is elligible 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_elligible_to_spend(commitment) def total_stake(self) -> int: """Returns the total stake that will be used to reletivize leadership proofs during this epoch""" @@ -287,7 +307,10 @@ class Follower: ) -> bool: return ( proof.verify(slot) # verify slot leader proof - and epoch_state.verify_commitment_is_old_enough_to_lead(proof.commitment) + and ( + ledger_state.verify_elligible_to_lead(proof.commitment) + or epoch_state.verify_elligible_to_lead_due_to_age(proof.commitment) + ) and ledger_state.verify_unspent(proof.nullifier) ) diff --git a/cryptarchia/test_ledger_state_update.py b/cryptarchia/test_ledger_state_update.py index 50e0539..752c73c 100644 --- a/cryptarchia/test_ledger_state_update.py +++ b/cryptarchia/test_ledger_state_update.py @@ -20,7 +20,8 @@ def mk_genesis_state(initial_stake_distribution: list[Coin]) -> LedgerState: block=bytes(32), nonce=bytes(32), total_stake=sum(c.value for c in initial_stake_distribution), - commitments={c.commitment() for c in initial_stake_distribution}, + commitments_spend={c.commitment() for c in initial_stake_distribution}, + commitments_lead={c.commitment() for c in initial_stake_distribution}, nullifiers=set(), ) @@ -148,10 +149,44 @@ class TestLedgerStateUpdate(TestCase): block_4 = mk_block(slot=20, parent=block_3.id(), coin=Coin(sk=4, value=100)) follower.on_block(block_4) assert follower.tip() == block_3 - # then we add the coin to the state associated with slot 9 - follower.ledger_state[block_2.id()].commitments.add( + # then we add the coin to "spendable commitments" associated with slot 9 + follower.ledger_state[block_2.id()].commitments_spend.add( Coin(sk=4, value=100).commitment() ) follower.on_block(block_4) assert follower.tip() == block_4 assert follower.tip().slot.epoch(follower.config).epoch == 2 + + def test_evolved_coin_is_elligble_for_leadership(self): + coin = Coin(sk=0, value=100) + + genesis = mk_genesis_state([coin]) + + config = Config( + k=1, + active_slot_coeff=1, + epoch_stake_distribution_stabilization=4, + epoch_period_nonce_buffer=3, + epoch_period_nonce_stabilization=3, + time=TimeConfig(slot_duration=1, chain_start_time=0), + ) + + follower = Follower(genesis, config) + + # coin wins the first slot + block_1 = mk_block(slot=0, parent=genesis.block, coin=coin) + follower.on_block(block_1) + + assert follower.tip_id() == block_1.id() + + # coin can't be reused to win following slots: + block_2_reuse = mk_block(slot=1, parent=block_1.id(), coin=coin) + follower.on_block(block_2_reuse) + + assert follower.tip_id() == block_1.id() + + # but the evolved coin is elligible + block_2_evolve = mk_block(slot=1, parent=block_1.id(), coin=coin.evolve()) + follower.on_block(block_2_evolve) + + assert follower.tip_id() == block_2_evolve.id() From 3f681fc51f48fee867dee87bd3b3ca3306e27b56 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Tue, 6 Feb 2024 20:19:30 +0400 Subject: [PATCH 3/5] update block id spec; typo --- cryptarchia/cryptarchia.py | 18 ++++++++++-------- cryptarchia/messages.abnf | 2 +- cryptarchia/test_ledger_state_update.py | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cryptarchia/cryptarchia.py b/cryptarchia/cryptarchia.py index 42cddd2..9e0b241 100644 --- a/cryptarchia/cryptarchia.py +++ b/cryptarchia/cryptarchia.py @@ -182,6 +182,8 @@ class BlockHeader: 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) return h.digest() @@ -219,7 +221,7 @@ class LedgerState: # set of commitments commitments_spend: set[Id] = field(default_factory=set) - # set of commitments elligible to lead + # set of commitments eligible to lead commitments_lead: set[Id] = field(default_factory=set) # set of nullified coins @@ -235,10 +237,10 @@ class LedgerState: nullifiers=deepcopy(self.nullifiers), ) - def verify_elligible_to_spend(self, commitment: Id) -> bool: + def verify_eligible_to_spend(self, commitment: Id) -> bool: return commitment in self.commitments_spend - def verify_elligible_to_lead(self, commitment: Id) -> bool: + def verify_eligible_to_lead(self, commitment: Id) -> bool: return commitment in self.commitments_lead def verify_unspent(self, nullifier: Id) -> bool: @@ -263,15 +265,15 @@ class EpochState: # The nonce snapshot is taken 7k/f slots into the previous epoch nonce_snapshot: LedgerState - def verify_elligible_to_lead_due_to_age(self, commitment: Id) -> bool: - # A coin is elligible to lead if it was committed to before the the stake + 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_elligible_to_spend(commitment) + return self.stake_distribution_snapshot.verify_eligible_to_spend(commitment) def total_stake(self) -> int: """Returns the total stake that will be used to reletivize leadership proofs during this epoch""" @@ -308,8 +310,8 @@ class Follower: return ( proof.verify(slot) # verify slot leader proof and ( - ledger_state.verify_elligible_to_lead(proof.commitment) - or epoch_state.verify_elligible_to_lead_due_to_age(proof.commitment) + ledger_state.verify_eligible_to_lead(proof.commitment) + or epoch_state.verify_eligible_to_lead_due_to_age(proof.commitment) ) and ledger_state.verify_unspent(proof.nullifier) ) diff --git a/cryptarchia/messages.abnf b/cryptarchia/messages.abnf index fefd6a6..b749146 100644 --- a/cryptarchia/messages.abnf +++ b/cryptarchia/messages.abnf @@ -9,7 +9,7 @@ CONTENT-SIZE = U32 BLOCK-DATE = BLOCK-SLOT BLOCK-SLOT = U64 PARENT-ID = HEADER-ID -MOCK-LEADER-PROOF = COMMITMENT NULLIFIER +MOCK-LEADER-PROOF = COMMITMENT NULLIFIER COMMITMENT ; ------------ CONTENT -------------------- CONTENT = *OCTET diff --git a/cryptarchia/test_ledger_state_update.py b/cryptarchia/test_ledger_state_update.py index 752c73c..349db47 100644 --- a/cryptarchia/test_ledger_state_update.py +++ b/cryptarchia/test_ledger_state_update.py @@ -157,7 +157,7 @@ class TestLedgerStateUpdate(TestCase): assert follower.tip() == block_4 assert follower.tip().slot.epoch(follower.config).epoch == 2 - def test_evolved_coin_is_elligble_for_leadership(self): + def test_evolved_coin_is_eligible_for_leadership(self): coin = Coin(sk=0, value=100) genesis = mk_genesis_state([coin]) @@ -185,7 +185,7 @@ class TestLedgerStateUpdate(TestCase): assert follower.tip_id() == block_1.id() - # but the evolved coin is elligible + # but the evolved coin is eligible block_2_evolve = mk_block(slot=1, parent=block_1.id(), coin=coin.evolve()) follower.on_block(block_2_evolve) From bddaa40d632b8a9220fd95f5280c930a817829e0 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Tue, 6 Feb 2024 22:19:08 +0400 Subject: [PATCH 4/5] test coin minting and stake stabilizing --- cryptarchia/test_ledger_state_update.py | 67 +++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/cryptarchia/test_ledger_state_update.py b/cryptarchia/test_ledger_state_update.py index 349db47..c56e1b0 100644 --- a/cryptarchia/test_ledger_state_update.py +++ b/cryptarchia/test_ledger_state_update.py @@ -176,17 +176,78 @@ class TestLedgerStateUpdate(TestCase): # coin wins the first slot block_1 = mk_block(slot=0, parent=genesis.block, coin=coin) follower.on_block(block_1) - assert follower.tip_id() == block_1.id() # coin can't be reused to win following slots: block_2_reuse = mk_block(slot=1, parent=block_1.id(), coin=coin) follower.on_block(block_2_reuse) - assert follower.tip_id() == block_1.id() # but the evolved coin is eligible block_2_evolve = mk_block(slot=1, parent=block_1.id(), coin=coin.evolve()) follower.on_block(block_2_evolve) - assert follower.tip_id() == block_2_evolve.id() + + def test_new_coins_becoming_eligible_after_stake_distribution_stabilizes(self): + coin = Coin(sk=0, value=100) + genesis = mk_genesis_state([coin]) + + # An epoch will be 10 slots long, with stake distribution snapshot taken at the start of the epoch + # and nonce snapshot before slot 7 + config = Config( + k=1, + active_slot_coeff=1, + epoch_stake_distribution_stabilization=4, + epoch_period_nonce_buffer=3, + epoch_period_nonce_stabilization=3, + time=TimeConfig(slot_duration=1, chain_start_time=0), + ) + + follower = Follower(genesis, config) + + # ---- EPOCH 0 ---- + + block_0_0 = mk_block(slot=0, parent=genesis.block, coin=coin) + follower.on_block(block_0_0) + assert follower.tip() == block_0_0 + + # mint a new coin to be used for leader elections in upcoming epochs + coin_new = Coin(sk=1, value=10) + follower.ledger_state[block_0_0.id()].commitments_spend.add( + coin_new.commitment() + ) + + # the new coin is not yet eligible for elections + + block_0_1_attempt = mk_block(slot=1, parent=block_0_0.id(), coin=coin_new) + follower.on_block(block_0_1_attempt) + assert follower.tip() == block_0_0 + + # whereas the evolved coin from genesis can be spent immediately + + block_0_1 = mk_block(slot=1, parent=block_0_0.id(), coin=coin.evolve()) + follower.on_block(block_0_1) + assert follower.tip() == block_0_1 + + # ---- EPOCH 1 ---- + + # The newly minted coin is still not eligible in the following epoch since the + # stake distribution snapshot is taken at the beginning of the previous epoch + + block_1_0 = mk_block(slot=10, parent=block_0_1.id(), coin=coin_new) + follower.on_block(block_1_0) + assert follower.tip() == block_0_1 + + # ---- EPOCH 2 ---- + + # The coin is finally eligible 2 epochs after it was first minted + + block_2_0 = mk_block(slot=20, parent=block_0_1.id(), coin=coin_new) + follower.on_block(block_2_0) + assert follower.tip() == block_2_0 + + # And now the minted coin can freely use the evolved coin for subsequent blocks + + block_2_1 = mk_block(slot=20, parent=block_2_0.id(), coin=coin_new.evolve()) + follower.on_block(block_2_1) + assert follower.tip() == block_2_1 From 2a9ec4bc864757adc7527ecae73449361f8f70f7 Mon Sep 17 00:00:00 2001 From: David Rusu Date: Tue, 6 Feb 2024 22:21:16 +0400 Subject: [PATCH 5/5] distinguish thew two commitments in the leader prf abnf --- cryptarchia/messages.abnf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cryptarchia/messages.abnf b/cryptarchia/messages.abnf index b749146..94aa013 100644 --- a/cryptarchia/messages.abnf +++ b/cryptarchia/messages.abnf @@ -9,7 +9,8 @@ CONTENT-SIZE = U32 BLOCK-DATE = BLOCK-SLOT BLOCK-SLOT = U64 PARENT-ID = HEADER-ID -MOCK-LEADER-PROOF = COMMITMENT NULLIFIER COMMITMENT +MOCK-LEADER-PROOF = COMMITMENT NULLIFIER EVOLVE-COMMITMENT +EVOLVE-COMMITMENT = COMMITMENT ; ------------ CONTENT -------------------- CONTENT = *OCTET