Add orphan proofs validation (#69)

* Add orphan proofs validation

* Update cryptarchia/cryptarchia.py

Co-authored-by: davidrusu <davidrusu.me@gmail.com>

---------

Co-authored-by: davidrusu <davidrusu.me@gmail.com>
This commit is contained in:
Giacomo Pasini 2024-02-09 15:12:12 +01:00 committed by GitHub
parent 5dd7b2730a
commit 495e0c119a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 39 deletions

View File

@ -2,6 +2,7 @@ from typing import TypeAlias, List, Optional
from hashlib import sha256, blake2b
from math import floor
from copy import deepcopy
from itertools import chain
import functools
# Please note this is still a work in progress
@ -133,20 +134,24 @@ class MockLeaderProof:
commitment: Id
nullifier: Id
evolved_commitment: Id
slot: Slot
parent: Id
@staticmethod
def from_coin(coin: Coin):
def new(coin: Coin, slot: Slot, parent: Id):
evolved_coin = coin.evolve()
return MockLeaderProof(
commitment=coin.commitment(),
nullifier=coin.nullifier(),
evolved_commitment=evolved_coin.commitment(),
slot=slot,
parent=parent,
)
def verify(self, slot):
def verify(self, slot: Slot, parent: Id):
# TODO: verification not implemented
return True
return slot == self.slot and parent == self.parent
@dataclass
@ -156,15 +161,9 @@ class BlockHeader:
content_size: int
content_id: Id
leader_proof: MockLeaderProof
orphaned_proofs: List["BlockHeader"] = field(default_factory=list)
# **Attention**:
# The ID of a block header is defined as the 32byte blake2b hash of its fields
# as serialized in the format specified by the 'HEADER' rule in 'messages.abnf'.
#
# The following code is to be considered as a reference implementation, mostly to be used for testing.
def id(self) -> Id:
h = blake2b(digest_size=32)
def update_header_hash(self, h):
# version byte
h.update(b"\x01")
@ -190,12 +189,31 @@ class BlockHeader:
assert len(self.leader_proof.evolved_commitment) == 32
h.update(self.leader_proof.evolved_commitment)
# orphaned proofs
h.update(int.to_bytes(len(self.orphaned_proofs), length=4, byteorder="big"))
for proof in self.orphaned_proofs:
proof.update_header_hash(h)
# **Attention**:
# The ID of a block header is defined as the 32byte blake2b hash of its fields
# as serialized in the format specified by the 'HEADER' rule in 'messages.abnf'.
#
# The following code is to be considered as a reference implementation, mostly to be used for testing.
def id(self) -> Id:
h = blake2b(digest_size=32)
self.update_header_hash(h)
return h.digest()
@dataclass
class Chain:
blocks: List[BlockHeader]
genesis: Id
def tip_id(self) -> Id:
if len(self.blocks) == 0:
return self.genesis
return self.tip().id()
def tip(self) -> BlockHeader:
return self.blocks[-1]
@ -265,9 +283,11 @@ 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)
for proof in chain(block.orphaned_proofs, [block]):
proof = proof.leader_proof
self.nullifiers.add(proof.nullifier)
self.commitments_spend.add(proof.evolved_commitment)
self.commitments_lead.add(proof.evolved_commitment)
@dataclass
@ -303,33 +323,59 @@ class Follower:
def __init__(self, genesis_state: LedgerState, config: Config):
self.config = config
self.forks = []
self.local_chain = Chain([])
self.local_chain = Chain([], genesis=genesis_state.block)
self.genesis_state = genesis_state
self.ledger_state = {genesis_state.block: genesis_state.copy()}
def validate_header(self, block: BlockHeader, chain: Chain) -> bool:
# TODO: verify blocks are not in the 'future'
parent_state = self.ledger_state[block.parent]
current_state = self.ledger_state[chain.tip_id()].copy()
orphaned_commitments = set()
# first, we verify adopted leadership transactions
for proof in block.orphaned_proofs:
proof = proof.leader_proof
# each proof is validated against the last state of the ledger of the chain this block
# is being added to before that proof slot
parent_state = self.state_at_slot_beginning(chain, proof.slot).copy()
# we add effects of previous orphaned proofs to the ledger state
parent_state.commitments_lead |= orphaned_commitments
epoch_state = self.compute_epoch_state(proof.slot.epoch(self.config), chain)
if self.verify_slot_leader(
proof.slot, proof, epoch_state, parent_state, current_state
):
# if an adopted leadership proof is valid we need to apply its effects to the ledger state
orphaned_commitments.add(proof.evolved_commitment)
current_state.nullifiers.add(proof.nullifier)
else:
# otherwise, the whole block is invalid
return False
parent_state = self.ledger_state[block.parent].copy()
parent_state.commitments_lead |= orphaned_commitments
epoch_state = self.compute_epoch_state(block.slot.epoch(self.config), chain)
# TODO: this is not the full block validation spec, only slot leader is verified
return self.verify_slot_leader(
block.slot, block.leader_proof, epoch_state, parent_state
block.slot, block.leader_proof, epoch_state, parent_state, current_state
)
def verify_slot_leader(
self,
slot: Slot,
proof: MockLeaderProof,
# coins are old enough if their commitment is in the stake distribution snapshot
epoch_state: EpochState,
ledger_state: LedgerState,
# commitments derived from leadership coin evolution are checked in the parent state
parent_state: LedgerState,
# nullifiers are checked in the current state
current_state: LedgerState,
) -> bool:
return (
proof.verify(slot) # verify slot leader proof
proof.verify(slot, parent_state.block) # verify slot leader proof
and (
ledger_state.verify_eligible_to_lead(proof.commitment)
parent_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)
and current_state.verify_unspent(proof.nullifier)
)
# Try appending this block to an existing chain and return whether
@ -347,13 +393,16 @@ class Follower:
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=[])
return Chain(blocks=[], genesis=self.genesis_state.block)
chains = self.forks + [self.local_chain]
for chain in chains:
if chain.contains_block(block):
block_position = chain.block_position(block)
return Chain(blocks=chain.blocks[:block_position])
return Chain(
blocks=chain.blocks[:block_position],
genesis=self.genesis_state.block,
)
return None
@ -468,13 +517,17 @@ class Leader:
coin: Coin
def try_prove_slot_leader(
self, epoch: EpochState, slot: Slot
self, epoch: EpochState, slot: Slot, parent: Id
) -> MockLeaderProof | None:
if self._is_slot_leader(epoch, slot):
return MockLeaderProof.from_coin(self.coin)
return MockLeaderProof.new(self.coin, slot, parent)
def propose_block(self, slot: Slot, parent: BlockHeader) -> BlockHeader:
return BlockHeader(parent=parent.id(), slot=slot)
def propose_block(
self, slot: Slot, parent: BlockHeader, orphaned_proofs=[]
) -> BlockHeader:
return BlockHeader(
parent=parent.id(), slot=slot, orphaned_proofs=orphaned_proofs
)
def _is_slot_leader(self, epoch: EpochState, slot: Slot):
relative_stake = self.coin.value / epoch.total_stake()

View File

@ -3,7 +3,7 @@
BLOCK = HEADER CONTENT
; ------------ HEADER ---------------------
VERSION = %x01
HEADER = VERSION HEADER-FIELDS MOCK-LEADER-PROOF
HEADER = VERSION HEADER-FIELDS MOCK-LEADER-PROOF ORPHAN-PROOFS
HEADER-FIELDS = CONTENT-SIZE CONTENT-ID BLOCK-DATE PARENT-ID
CONTENT-SIZE = U32
BLOCK-DATE = BLOCK-SLOT
@ -11,6 +11,10 @@ BLOCK-SLOT = U64
PARENT-ID = HEADER-ID
MOCK-LEADER-PROOF = COMMITMENT NULLIFIER EVOLVE-COMMITMENT
EVOLVE-COMMITMENT = COMMITMENT
ORPHAN-PROOFS = ORPHAN-PROOF-CNT *ORPHAN-PROOF
ORPHAN-PROOF-CNT = U32
; note this is not recursive, only the header leadership proof will be processed (orphan proofs are ignored)
ORPHAN-PROOF = HEADER
; ------------ CONTENT --------------------
CONTENT = *OCTET

View File

@ -23,7 +23,9 @@ 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(sk=0, value=10)),
leader_proof=MockLeaderProof.new(
Coin(sk=0, value=10), slot=slot, parent=parent_id
),
)
@ -49,14 +51,28 @@ class TestLeader(TestCase):
# by setting a low k we trigger the density choice rule
k = 1
s = 50
assert maxvalid_bg(Chain(short_chain), [Chain(long_chain)], k, s) == Chain(
short_chain
short_chain = Chain(short_chain, genesis=bytes(32))
long_chain = Chain(long_chain, genesis=bytes(32))
assert (
maxvalid_bg(
short_chain,
[long_chain],
k,
s,
)
== short_chain
)
# However, if we set k to the fork length, it will be accepted
k = len(long_chain)
assert maxvalid_bg(Chain(short_chain), [Chain(long_chain)], k, s) == Chain(
long_chain
k = long_chain.length()
assert (
maxvalid_bg(
short_chain,
[long_chain],
k,
s,
)
== long_chain
)
def test_fork_choice_long_dense_chain(self):
@ -73,6 +89,14 @@ class TestLeader(TestCase):
short_chain.append(make_block(bytes(32), Slot(slot), short_content))
k = 1
s = 50
assert maxvalid_bg(Chain(short_chain), [Chain(long_chain)], k, s) == Chain(
long_chain
short_chain = Chain(short_chain, genesis=bytes(32))
long_chain = Chain(long_chain, genesis=bytes(32))
assert (
maxvalid_bg(
short_chain,
[long_chain],
k,
s,
)
== long_chain
)

View File

@ -45,7 +45,7 @@ class TestLeader(TestCase):
# After N slots, the measured leader rate should be within the interval `p +- margin_of_error` with high probabiltiy
leader_rate = (
sum(
l.try_prove_slot_leader(epoch, Slot(slot)) is not None
l.try_prove_slot_leader(epoch, Slot(slot), bytes(32)) is not None
for slot in range(N)
)
/ N

View File

@ -26,7 +26,9 @@ def mk_genesis_state(initial_stake_distribution: list[Coin]) -> LedgerState:
)
def mk_block(parent: Id, slot: int, coin: Coin, content=bytes(32)) -> BlockHeader:
def mk_block(
parent: Id, slot: int, coin: Coin, content=bytes(32), orphaned_proofs=[]
) -> BlockHeader:
from hashlib import sha256
return BlockHeader(
@ -34,7 +36,8 @@ def mk_block(parent: Id, slot: int, coin: Coin, content=bytes(32)) -> BlockHeade
parent=parent,
content_size=len(content),
content_id=sha256(content).digest(),
leader_proof=MockLeaderProof.from_coin(coin),
leader_proof=MockLeaderProof.new(coin, Slot(slot), parent=parent),
orphaned_proofs=orphaned_proofs,
)
@ -251,3 +254,51 @@ class TestLedgerStateUpdate(TestCase):
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
def test_orphaned_proofs(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
coin_new = coin.evolve()
coin_new_new = coin_new.evolve()
block_0_1 = mk_block(slot=1, parent=block_0_0.id(), coin=coin_new_new)
follower.on_block(block_0_1)
# the coin evolved twice should not be accepted as it is not in the lead commitments
assert follower.tip() == block_0_0
# an orphaned proof with an evolved coin for the same slot as the original coin should not be accepted as the evolved coin is not in the lead commitments at slot 0
block_0_1 = mk_block(
slot=1,
parent=block_0_0.id(),
coin=coin_new_new,
orphaned_proofs=[mk_block(parent=genesis.block, slot=0, coin=coin_new)],
)
follower.on_block(block_0_1)
assert follower.tip() == block_0_0
# the coin evolved twice should be accepted as the evolved coin is in the lead commitments at slot 1 and processed before that
block_0_2 = mk_block(
slot=2,
parent=block_0_0.id(),
coin=coin_new_new,
orphaned_proofs=[mk_block(parent=block_0_0.id(), slot=1, coin=coin_new)],
)
follower.on_block(block_0_2)
assert follower.tip() == block_0_2