mirror of
https://github.com/logos-co/nomos-specs.git
synced 2025-02-22 12:08:10 +00:00
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:
parent
5dd7b2730a
commit
495e0c119a
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user