mirror of
https://github.com/logos-co/nomos-specs.git
synced 2025-02-02 10:34:50 +00:00
Merge pull request #65 from logos-co/evolve-leader-coin
Spec. Leader Coin Evolution
This commit is contained in:
commit
0c447881ca
@ -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
|
||||
@ -156,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()
|
||||
|
||||
@ -192,20 +220,31 @@ class LedgerState:
|
||||
# Note that this does not prevent nonce grinding at the last slot before the nonce snapshot
|
||||
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 eligible 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_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
|
||||
@ -221,6 +260,8 @@ class LedgerState:
|
||||
self.nonce = h.digest()
|
||||
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
|
||||
@ -234,8 +275,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_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_stake(self) -> int:
|
||||
"""Returns the total stake that will be used to reletivize leadership proofs during this epoch"""
|
||||
@ -271,7 +319,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_eligible_to_lead(proof.commitment)
|
||||
or epoch_state.verify_eligible_to_lead_due_to_age(proof.commitment)
|
||||
)
|
||||
and ledger_state.verify_unspent(proof.nullifier)
|
||||
)
|
||||
|
||||
|
@ -9,7 +9,8 @@ CONTENT-SIZE = U32
|
||||
BLOCK-DATE = BLOCK-SLOT
|
||||
BLOCK-SLOT = U64
|
||||
PARENT-ID = HEADER-ID
|
||||
MOCK-LEADER-PROOF = COMMITMENT NULLIFIER
|
||||
MOCK-LEADER-PROOF = COMMITMENT NULLIFIER EVOLVE-COMMITMENT
|
||||
EVOLVE-COMMITMENT = COMMITMENT
|
||||
|
||||
; ------------ CONTENT --------------------
|
||||
CONTENT = *OCTET
|
||||
|
@ -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)),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
)
|
||||
|
||||
@ -50,7 +51,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 +77,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 +115,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,13 +146,108 @@ 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()
|
||||
# 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_eligible_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 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user