test ledger state is properly updated on re-org

This commit is contained in:
David Rusu 2024-02-01 21:33:37 +04:00
parent 62ea40ba5e
commit 9345af0614
3 changed files with 103 additions and 45 deletions

View File

@ -26,8 +26,13 @@ class TimeConfig:
@dataclass @dataclass
class Config: class Config:
k: int k: int
active_slot_coeff: float # 'f', the rate of occupied slots
time: TimeConfig time: TimeConfig
@property
def s(self):
return int(3 * self.k / self.active_slot_coeff)
# An absolute unique indentifier of a slot, counting incrementally from 0 # An absolute unique indentifier of a slot, counting incrementally from 0
@dataclass @dataclass
@ -158,6 +163,15 @@ class LedgerState:
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 copy(self):
return LedgerState(
block=self.block,
nonce=self.nonce,
total_stake=self.total_stake,
commitments=self.commitments.copy(),
nullifiers=self.nullifiers.copy(),
)
def verify_committed(self, commitment: Id) -> bool: def verify_committed(self, commitment: Id) -> bool:
return commitment in self.commitments return commitment in self.commitments
@ -180,7 +194,7 @@ class Follower:
nonce_snapshot=genesis_state, nonce_snapshot=genesis_state,
) )
self.genesis_state = genesis_state self.genesis_state = genesis_state
self.ledger_state = genesis_state self.ledger_state = genesis_state.copy()
def validate_header(self, 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
@ -201,16 +215,20 @@ class Follower:
return True return True
for chain in self.forks: for chain in self.forks:
if chain.tip().id() == block.parent(): if chain.tip().id() == block.parent:
chain.blocks.append(block) chain.blocks.append(block)
return True return True
return False return False
def try_create_fork(self, block: BlockHeader) -> Optional[Chain]: def try_create_fork(self, block: BlockHeader) -> Optional[Chain]:
if self.genesis_state.block == block.parent:
# this block is forking off the genesis state
return Chain(blocks=[block])
chains = self.forks + [self.local_chain] chains = self.forks + [self.local_chain]
for chain in chains: for chain in chains:
if self.chain.contains_block(block): if chain.contains_block(block):
block_position = chain.block_position(block) block_position = chain.block_position(block)
return Chain(blocks=chain.blocks[:block_position] + [block]) return Chain(blocks=chain.blocks[:block_position] + [block])
@ -234,7 +252,7 @@ class Follower:
return return
# We may need to switch forks, lets run the fork choice rule to check. # We may need to switch forks, lets run the fork choice rule to check.
new_chain = Follower.fork_choice(self.local_chain, self.forks) new_chain = self.fork_choice()
if new_chain == self.local_chain: if new_chain == self.local_chain:
# we have not re-org'd therefore we can simply update our ledger state # we have not re-org'd therefore we can simply update our ledger state
@ -252,14 +270,14 @@ class Follower:
self.local_chain = new_chain 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(self) -> Chain:
def fork_choice(local_chain: Chain, forks: List[Chain]) -> Chain: return maxvalid_bg(
# TODO: define k and s self.local_chain, self.forks, k=self.config.k, s=self.config.s
return maxvalid_bg(local_chain, forks, 0, 0) )
def tip_id(self) -> Id: def tip_id(self) -> Id:
if self.local_chain.length() > 0: if self.local_chain.length() > 0:
return self.local_chain.tip().id return self.local_chain.tip().id()
else: else:
return self.ledger_state.block return self.ledger_state.block
@ -286,11 +304,6 @@ class EpochState:
return self.nonce_snapshot.nonce return self.nonce_snapshot.nonce
@dataclass
class LeaderConfig:
active_slot_coeff: float = 0.05 # 'f', the rate of occupied slots
def phi(f: float, alpha: float) -> float: def phi(f: float, alpha: float) -> float:
""" """
params: params:
@ -322,7 +335,7 @@ class MOCK_LEADER_VRF:
@dataclass @dataclass
class Leader: class Leader:
config: LeaderConfig config: Config
coin: Coin coin: Coin
def try_prove_slot_leader( def try_prove_slot_leader(

View File

@ -2,7 +2,7 @@ from unittest import TestCase
import numpy as np import numpy as np
from .cryptarchia import Leader, LeaderConfig, EpochState, LedgerState, Coin, phi from .cryptarchia import Leader, Config, EpochState, LedgerState, Coin, phi, TimeConfig
class TestLeader(TestCase): class TestLeader(TestCase):
@ -15,8 +15,12 @@ class TestLeader(TestCase):
) )
f = 0.05 f = 0.05
leader_config = LeaderConfig(active_slot_coeff=f) config = Config(
l = Leader(config=leader_config, coin=Coin(pk=0, value=10)) k=10,
active_slot_coeff=f,
time=TimeConfig(slots_per_epoch=1000, slot_duration=1, chain_start_time=0),
)
l = Leader(config=config, coin=Coin(pk=0, value=10))
# 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.
# https://en.wikipedia.org/wiki/Margin_of_error # https://en.wikipedia.org/wiki/Margin_of_error

View File

@ -11,36 +11,48 @@ from .cryptarchia import (
LedgerState, LedgerState,
MockLeaderProof, MockLeaderProof,
Slot, Slot,
Id,
) )
def mk_genesis_state(initial_stake_distribution: list[Coin]) -> LedgerState:
return 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},
nullifiers=set(),
)
def mk_block(parent: Id, slot: int, coin: Coin, content=bytes(32)) -> BlockHeader:
from hashlib import sha256
return BlockHeader(
slot=Slot(slot),
parent=parent,
content_size=len(content),
content_id=sha256(content).digest(),
leader_proof=MockLeaderProof.from_coin(coin),
)
def config() -> Config: def config() -> Config:
return Config( return Config(
k=10, time=TimeConfig(slots_per_epoch=1000, slot_duration=1, chain_start_time=0) k=10,
active_slot_coeff=0.05,
time=TimeConfig(slots_per_epoch=1000, slot_duration=1, chain_start_time=0),
) )
class TestLedgerStateUpdate(TestCase): class TestLedgerStateUpdate(TestCase):
def test_ledger_state_prevents_coin_reuse(self): def test_ledger_state_prevents_coin_reuse(self):
leader_coin = Coin(pk=0, value=100) leader_coin = Coin(pk=0, value=100)
genesis_state = LedgerState( genesis = mk_genesis_state([leader_coin])
block=bytes(32),
nonce=bytes(32),
total_stake=leader_coin.value,
commitments={leader_coin.commitment()},
nullifiers=set(),
)
follower = Follower(genesis_state, config()) follower = Follower(genesis, config())
block = BlockHeader(
slot=Slot(0),
parent=genesis_state.block,
content_size=1,
content_id=bytes(32),
leader_proof=MockLeaderProof.from_coin(leader_coin),
)
block = mk_block(slot=0, parent=genesis.block, coin=leader_coin)
follower.on_block(block) follower.on_block(block)
# Follower should have accepted the block # Follower should have accepted the block
@ -50,18 +62,47 @@ class TestLedgerStateUpdate(TestCase):
# Follower should have updated their ledger state to mark the leader coin as spent # Follower should have updated their ledger state to mark the leader coin as spent
assert follower.ledger_state.verify_unspent(leader_coin.nullifier()) == False assert follower.ledger_state.verify_unspent(leader_coin.nullifier()) == False
reuse_coin_block = BlockHeader( reuse_coin_block = mk_block(slot=1, parent=block.id, coin=leader_coin)
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.on_block(block)
# Follower should *not* have accepted the block # Follower should *not* have accepted the block
assert follower.local_chain.length() == 1 assert follower.local_chain.length() == 1
assert follower.local_chain.tip() == block 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=1, value=100)
genesis = mk_genesis_state([coin_1, coin_2, coin_3])
follower = Follower(genesis, config())
# 1) coin_1 & coin_2 both concurrently win slot 0
block_1 = mk_block(parent=genesis.block, slot=0, coin=coin_1)
block_2 = mk_block(parent=genesis.block, slot=0, coin=coin_2)
# 2) follower sees block 1 first
follower.on_block(block_1)
assert follower.tip_id() == block_1.id()
assert not follower.ledger_state.verify_unspent(coin_1.nullifier())
# 3) then sees block 2, but sticks with block_1 as the tip
follower.on_block(block_2)
assert follower.tip_id() == block_1.id()
assert len(follower.forks) == 1, f"{len(follower.forks)}"
# 4) then coin_3 wins slot 1 and chooses to extend from block_2
block_3 = mk_block(parent=block_2.id(), slot=0, coin=coin_3)
follower.on_block(block_3)
# the follower should have switched over to the block_2 fork
assert follower.tip_id() == block_3.id()
# and the original coin_1 should now be removed from the spent pool
assert follower.ledger_state.verify_unspent(coin_1.nullifier())