mirror of
https://github.com/logos-co/nomos-specs.git
synced 2025-01-10 23:56:31 +00:00
Follower maintains ledger state as it follows the chain
This commit is contained in:
parent
7d8e4d72d9
commit
45bddc0e21
@ -23,6 +23,12 @@ class TimeConfig:
|
|||||||
chain_start_time: int
|
chain_start_time: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
k: int
|
||||||
|
time: TimeConfig
|
||||||
|
|
||||||
|
|
||||||
# An absolute unique indentifier of a slot, counting incrementally from 0
|
# An absolute unique indentifier of a slot, counting incrementally from 0
|
||||||
@dataclass
|
@dataclass
|
||||||
class Slot:
|
class Slot:
|
||||||
@ -37,9 +43,30 @@ class Slot:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Coin:
|
||||||
k: int
|
pk: int
|
||||||
time: TimeConfig
|
value: int
|
||||||
|
|
||||||
|
def commitment(self) -> Id:
|
||||||
|
# TODO: mocked until CL is understood
|
||||||
|
pk_bytes = int.to_bytes(self.pk, length=32, byteorder="little")
|
||||||
|
value_bytes = int.to_bytes(self.value, length=32, byteorder="little")
|
||||||
|
|
||||||
|
h = sha256()
|
||||||
|
h.update(pk_bytes)
|
||||||
|
h.update(value_bytes)
|
||||||
|
return h.digest()
|
||||||
|
|
||||||
|
def nullifier(self) -> Id:
|
||||||
|
# TODO: mocked until CL is understood
|
||||||
|
pk_bytes = int.to_bytes(self.pk, length=32, byteorder="little")
|
||||||
|
value_bytes = int.to_bytes(self.value, length=32, byteorder="little")
|
||||||
|
|
||||||
|
h = sha256()
|
||||||
|
h.update(pk_bytes)
|
||||||
|
h.update(value_bytes)
|
||||||
|
h.update(b"\x00") # extra 0 byte to differentiate from commitment
|
||||||
|
return h.digest()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -47,15 +74,17 @@ class MockLeaderProof:
|
|||||||
commitment: Id
|
commitment: Id
|
||||||
nullifier: Id
|
nullifier: Id
|
||||||
|
|
||||||
def verify(self):
|
@staticmethod
|
||||||
|
def from_coin(coin: Coin):
|
||||||
|
return MockLeaderProof(commitment=coin.commitment(), nullifier=coin.nullifier())
|
||||||
|
|
||||||
|
def verify(self, slot):
|
||||||
# TODO: verification not implemented
|
# TODO: verification not implemented
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _id_update(self, hasher):
|
def _id_update(self, hasher):
|
||||||
commitment_bytes = int.to_bytes(self.commitment, length=32, byteorder="little")
|
hasher.update(self.commitment)
|
||||||
nullifier_bytes = int.to_bytes(self.nullifier, length=32, byteorder="little")
|
hasher.update(self.nullifier)
|
||||||
hasher.update(commitment_bytes)
|
|
||||||
hasher.update(nullifier_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -121,13 +150,21 @@ class LedgerState:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
block: Id = None
|
block: Id = None
|
||||||
nonce: bytes = None
|
nonce: Id = None
|
||||||
total_stake: int = None
|
total_stake: int = None
|
||||||
commitments: set[Id] = field(default_factory=set) # set of commitments
|
commitments: set[Id] = field(default_factory=set) # set of commitments
|
||||||
nullifiers: set[Id] = field(default_factory=set) # set of nullified
|
nullifiers: set[Id] = field(default_factory=set) # set of nullified
|
||||||
|
|
||||||
def is_coin_nullified(self, nullifier: Id) -> bool:
|
def verify_committed(self, commitment: Id) -> bool:
|
||||||
return nullifier in self.nullifiers
|
return commitment in self.commitments
|
||||||
|
|
||||||
|
def verify_unspent(self, nullifier: Id) -> bool:
|
||||||
|
return nullifier not in self.nullifiers
|
||||||
|
|
||||||
|
def apply(self, block: BlockHeader):
|
||||||
|
assert block.parent == self.block
|
||||||
|
self.block = block.id()
|
||||||
|
self.nullifiers.add(block.leader_proof.nullifier)
|
||||||
|
|
||||||
|
|
||||||
class Follower:
|
class Follower:
|
||||||
@ -139,23 +176,24 @@ class Follower:
|
|||||||
stake_distribution_snapshot=genesis_state,
|
stake_distribution_snapshot=genesis_state,
|
||||||
nonce_snapshot=genesis_state,
|
nonce_snapshot=genesis_state,
|
||||||
)
|
)
|
||||||
|
self.ledger_state_snapshot = genesis_state
|
||||||
self.ledger_state = genesis_state
|
self.ledger_state = genesis_state
|
||||||
|
|
||||||
def validate_header(block: BlockHeader) -> bool:
|
def validate_header(self, block: BlockHeader) -> bool:
|
||||||
# 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
|
||||||
return self.verify_slot_leader(block.slot, block.leader_proof)
|
return self.verify_slot_leader(block.slot, block.leader_proof)
|
||||||
|
|
||||||
def verify_slot_leader(self, slot: Slot, proof: MockLeaderProof) -> bool:
|
def verify_slot_leader(self, slot: Slot, proof: MockLeaderProof) -> bool:
|
||||||
return (
|
return (
|
||||||
proof.verify(slot) # verify slot leader proof
|
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 self.epoch.verify_commitment_is_old_enough_to_lead(proof.commitment)
|
||||||
and not self.ledger_state.is_coin_nullified(proof.nullifier) # verify the coin has not already been spent
|
and self.ledger_state.verify_unspent(proof.nullifier)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try appending this block to an existing chain and return whether
|
# Try appending this block to an existing chain and return whether
|
||||||
# the operation was successful
|
# the operation was successful
|
||||||
def try_extend_chains(self, block: BlockHeader) -> bool:
|
def try_extend_chains(self, block: BlockHeader) -> bool:
|
||||||
if self.local_chain.tip().id() == block.parent():
|
if self.tip_id() == block.parent:
|
||||||
self.local_chain.blocks.append(block)
|
self.local_chain.blocks.append(block)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -180,50 +218,47 @@ class Follower:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# check if the new block extends an existing chain
|
# check if the new block extends an existing chain
|
||||||
if self.try_extend_chains(block):
|
succeeded_in_extending_a_chain = self.try_extend_chains(block)
|
||||||
return
|
if not succeeded_in_extending_a_chain:
|
||||||
|
# we failed to extend one of the existing chains,
|
||||||
|
# therefore we might need to create a new fork
|
||||||
|
new_chain = self.try_create_fork(block)
|
||||||
|
if new_chain is not None:
|
||||||
|
self.forks.append(new_chain)
|
||||||
|
else:
|
||||||
|
# otherwise, we're missing the parent block
|
||||||
|
# in that case, just ignore the block
|
||||||
|
return
|
||||||
|
|
||||||
# if we get here, we might need to create a fork
|
# We may need to switch forks, lets run the fork choice rule to check.
|
||||||
new_chain = self.try_create_fork(block)
|
new_chain = Follower.fork_choice(self.local_chain, self.forks)
|
||||||
if new_chain is not None:
|
|
||||||
self.forks.append(new_chain)
|
if new_chain == self.local_chain:
|
||||||
# otherwise, we're missing the parent block
|
# we have not re-org'd therefore we can simply update our ledger state
|
||||||
# in that case, just ignore the block
|
# if this block extend our local chain
|
||||||
|
if self.local_chain.tip() == block:
|
||||||
|
self.ledger_state.apply(block)
|
||||||
|
else:
|
||||||
|
# we have re-org'd, therefore we must roll back out ledger state and
|
||||||
|
# re-apply blocks from the new chain
|
||||||
|
ledger_state = self.ledger_state_snapshot.copy()
|
||||||
|
for block in new_chain.blocks:
|
||||||
|
ledger_state.apply(block)
|
||||||
|
|
||||||
|
self.ledger_state = ledger_state
|
||||||
|
self.local_chain = new_chain
|
||||||
|
|
||||||
# Evaluate the fork choice rule and return the block header of the block that should be the head of the chain
|
# Evaluate the fork choice rule and return the block header of the block that should be the head of the chain
|
||||||
|
@staticmethod
|
||||||
def fork_choice(local_chain: Chain, forks: List[Chain]) -> Chain:
|
def fork_choice(local_chain: Chain, forks: List[Chain]) -> Chain:
|
||||||
# TODO: define k and s
|
# TODO: define k and s
|
||||||
return maxvalid_bg(local_chain, forks, 0, 0)
|
return maxvalid_bg(local_chain, forks, 0, 0)
|
||||||
|
|
||||||
def tip(self) -> BlockHeader:
|
def tip_id(self) -> Id:
|
||||||
return self.fork_choice()
|
if self.local_chain.length() > 0:
|
||||||
|
return self.local_chain.tip().id
|
||||||
|
else:
|
||||||
@dataclass
|
return self.ledger_state.block
|
||||||
class Coin:
|
|
||||||
pk: int
|
|
||||||
value: int
|
|
||||||
|
|
||||||
def commitment(self) -> Id:
|
|
||||||
# TODO: mocked until CL is understood
|
|
||||||
pk_bytes = int.to_bytes(self.pk, length=32, byteorder="little")
|
|
||||||
value_bytes = int.to_bytes(self.value, length=32, byteorder="little")
|
|
||||||
|
|
||||||
h = sha256()
|
|
||||||
h.update(pk_bytes)
|
|
||||||
h.update(value_bytes)
|
|
||||||
return h.digest()
|
|
||||||
|
|
||||||
def nullifier(self) -> Id:
|
|
||||||
# TODO: mocked until CL is understood
|
|
||||||
pk_bytes = int.to_bytes(self.pk, length=32, byteorder="little")
|
|
||||||
value_bytes = int.to_bytes(self.value, length=32, byteorder="little")
|
|
||||||
|
|
||||||
h = sha256()
|
|
||||||
h.update(pk_bytes)
|
|
||||||
h.update(value_bytes)
|
|
||||||
h.update(b"\x00") # extra 0 byte to differentiate from commitment
|
|
||||||
return h.digest()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -237,8 +272,8 @@ class EpochState:
|
|||||||
# The nonce snapshot is taken 7k/f slots into the previous epoch
|
# The nonce snapshot is taken 7k/f slots into the previous epoch
|
||||||
nonce_snapshot: LedgerState
|
nonce_snapshot: LedgerState
|
||||||
|
|
||||||
def is_coin_old_enough_to_lead(self, coin: Coin) -> bool:
|
def verify_commitment_is_old_enough_to_lead(self, commitment: Id) -> bool:
|
||||||
return coin in self.stake_distribution.commitments
|
return self.stake_distribution_snapshot.verify_committed(commitment)
|
||||||
|
|
||||||
def total_stake(self) -> int:
|
def total_stake(self) -> int:
|
||||||
"""Returns the total stake that will be used to reletivize leadership proofs during this epoch"""
|
"""Returns the total stake that will be used to reletivize leadership proofs during this epoch"""
|
||||||
@ -291,9 +326,7 @@ class Leader:
|
|||||||
self, epoch: EpochState, slot: Slot
|
self, epoch: EpochState, slot: Slot
|
||||||
) -> MockLeaderProof | None:
|
) -> MockLeaderProof | None:
|
||||||
if self._is_slot_leader(epoch, slot):
|
if self._is_slot_leader(epoch, slot):
|
||||||
return MockLeaderProof(
|
return MockLeaderProof.from_coin(self.coin)
|
||||||
commitment=self.coin.commitment(), nullifier=self.coin.nullifier()
|
|
||||||
)
|
|
||||||
|
|
||||||
def propose_block(self, slot: Slot, parent: BlockHeader) -> BlockHeader:
|
def propose_block(self, slot: Slot, parent: BlockHeader) -> BlockHeader:
|
||||||
return BlockHeader(parent=parent.id(), slot=slot)
|
return BlockHeader(parent=parent.id(), slot=slot)
|
||||||
|
@ -11,6 +11,7 @@ from cryptarchia.cryptarchia import (
|
|||||||
Slot,
|
Slot,
|
||||||
Id,
|
Id,
|
||||||
MockLeaderProof,
|
MockLeaderProof,
|
||||||
|
Coin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ def make_block(parent_id: Id, slot: Slot, content: bytes) -> BlockHeader:
|
|||||||
content_size=1,
|
content_size=1,
|
||||||
slot=slot,
|
slot=slot,
|
||||||
content_id=content_id,
|
content_id=content_id,
|
||||||
leader_proof=MockLeaderProof(commitment=0, nullifier=0),
|
leader_proof=MockLeaderProof.from_coin(Coin(pk=0, value=10)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
67
cryptarchia/test_ledger_state_update.py
Normal file
67
cryptarchia/test_ledger_state_update.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .cryptarchia import (
|
||||||
|
Follower,
|
||||||
|
TimeConfig,
|
||||||
|
BlockHeader,
|
||||||
|
Config,
|
||||||
|
Coin,
|
||||||
|
LedgerState,
|
||||||
|
MockLeaderProof,
|
||||||
|
Slot,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def config() -> Config:
|
||||||
|
return Config(
|
||||||
|
k=10, time=TimeConfig(slots_per_epoch=1000, slot_duration=1, chain_start_time=0)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLedgerStateUpdate(TestCase):
|
||||||
|
def test_ledger_state_prevents_coin_reuse(self):
|
||||||
|
leader_coin = Coin(pk=0, value=100)
|
||||||
|
genesis_state = LedgerState(
|
||||||
|
block=bytes(32),
|
||||||
|
nonce=bytes(32),
|
||||||
|
total_stake=leader_coin.value,
|
||||||
|
commitments={leader_coin.commitment()},
|
||||||
|
nullifiers=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
follower = Follower(genesis_state, config())
|
||||||
|
|
||||||
|
block = BlockHeader(
|
||||||
|
slot=Slot(0),
|
||||||
|
parent=genesis_state.block,
|
||||||
|
content_size=1,
|
||||||
|
content_id=bytes(32),
|
||||||
|
leader_proof=MockLeaderProof.from_coin(leader_coin),
|
||||||
|
)
|
||||||
|
|
||||||
|
follower.on_block(block)
|
||||||
|
|
||||||
|
# Follower should have accepted the block
|
||||||
|
assert follower.local_chain.length() == 1
|
||||||
|
assert follower.local_chain.tip() == block
|
||||||
|
|
||||||
|
# Follower should have updated their ledger state to mark the leader coin as spent
|
||||||
|
assert follower.ledger_state.verify_unspent(leader_coin.nullifier()) == False
|
||||||
|
|
||||||
|
reuse_coin_block = BlockHeader(
|
||||||
|
slot=Slot(0),
|
||||||
|
parent=block.id(),
|
||||||
|
content_size=1,
|
||||||
|
content_id=bytes(32),
|
||||||
|
leader_proof=MockLeaderProof(
|
||||||
|
commitment=leader_coin.commitment(),
|
||||||
|
nullifier=leader_coin.nullifier(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
follower.on_block(block)
|
||||||
|
|
||||||
|
# Follower should *not* have accepted the block
|
||||||
|
assert follower.local_chain.length() == 1
|
||||||
|
assert follower.local_chain.tip() == block
|
Loading…
x
Reference in New Issue
Block a user