Random Beacon revision (#29)

* Random Beacon revision

This is a proposed revision of the random beacon specification.
First of all it fixes a few little mistakes in the signing
process:
* Use BasicSchemeMPL instead of PoPSchemeMPL since we don't use
  Proof of Possession.
* Hashing the values prior to the call to BasicSchemeMPL.sing()
  is not necessary. This step has been removed.

In addition, all data inside the random beacon state that
anyone willing to verify must know anyway has been removed.
In the current version this includes the 'context' and the public
key of the signer. The verifier has to independently check that
those values have been correctly obtained anyway, so there's no
need to include them in the state that is passed around.

Lastly, the beacon context view has been changed from using a
string encoding to a little endian variable-length encoding and
is now tied to qc.view instead of current_view of the processing
node.

* actually use the version const
This commit is contained in:
Giacomo Pasini 2023-05-25 10:26:35 +02:00 committed by GitHub
parent b0edea6a98
commit 34617dc911
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 118 deletions

View File

@ -5,67 +5,68 @@ from typing import TypeAlias
# carnot imports # carnot imports
# lib imports # lib imports
from blspy import PrivateKey, Util, PopSchemeMPL, G2Element, G1Element from blspy import PrivateKey, Util, BasicSchemeMPL, G2Element, G1Element
# stdlib imports # stdlib imports
from hashlib import sha256 from hashlib import sha256
View: TypeAlias = int View: TypeAlias = int
Beacon: TypeAlias = bytes Sig: TypeAlias = bytes
Proof: TypeAlias = bytes # For now this is gonna be a public key, in future research we may pivot to zk proofs. Entropy: TypeAlias = bytes
PublicKey: TypeAlias = G1Element
VERSION = 0
def generate_random_sk() -> PrivateKey: def generate_random_sk() -> PrivateKey:
seed = bytes([randint(0, 255) for _ in range(32)]) seed = bytes([randint(0, 255) for _ in range(32)])
return PopSchemeMPL.key_gen(seed) return BasicSchemeMPL.key_gen(seed)
def view_to_bytes(view: View) -> bytes:
return view.to_bytes((view.bit_length() + 7) // 8, byteorder='little', signed=True)
@dataclass @dataclass
class RandomBeacon: class RandomBeacon:
version: int version: int
context: View sig: Sig
entropy: Beacon
# TODO: Just the happy path beacons owns a proof, we can set the proof to empty bytes for now. def entropy(self) -> Entropy:
# Probably we should separate this into two kinds of beacons and group them under a single type later on. return self.sig
proof: Proof
class NormalMode: class NormalMode:
@staticmethod @staticmethod
def verify(beacon: RandomBeacon) -> bool: def verify(beacon: RandomBeacon, pk: PublicKey, view: View) -> bool:
""" """
:param proof: BLS signature :param beacon: the provided beacon
:param beacon: Beacon is signature for current view :param view: view to verify beacon upon
:param view: View to verify beacon upon :param pk: public key of the issuer of the beacon
:return: :return:
""" """
# TODO: Actually verify that the message is propoerly signed sig = G2Element.from_bytes(beacon.sig)
sig = G2Element.from_bytes(beacon.entropy) return BasicSchemeMPL.verify(pk, view_to_bytes(view), sig)
proof = G1Element.from_bytes(beacon.proof)
return PopSchemeMPL.verify(proof, Util.hash256(str(beacon.context).encode()), sig)
@staticmethod @staticmethod
def generate_beacon(private_key: PrivateKey, view: View) -> Beacon: def generate_beacon(private_key: PrivateKey, view: View) -> RandomBeacon:
return bytes(PopSchemeMPL.sign(private_key, Util.hash256(str(view).encode()))) return RandomBeacon(VERSION, bytes(BasicSchemeMPL.sign(private_key, view_to_bytes(view))))
class RecoveryMode: class RecoveryMode:
@staticmethod @staticmethod
def verify(last_beacon: RandomBeacon, beacon: RandomBeacon) -> bool: def verify(last_beacon: RandomBeacon, beacon: RandomBeacon, view: View) -> bool:
""" """
:param last_beacon: Unhappy -> last working beacon (signature), Happy -> Hash of previous beacon and next view number :param last_beacon: beacon for view - 1
:param beacon: :param beacon: beacon for view
:param view: :param view: the view to verify beacon upon
:return: :return:
""" """
b = sha256(last_beacon.entropy + str(beacon.context).encode()).digest() b = sha256(last_beacon.entropy() + view_to_bytes(view)).digest()
return b == beacon.entropy return b == beacon.entropy()
@staticmethod @staticmethod
def generate_beacon(last_beacon: Beacon, view: View) -> Beacon: def generate_beacon(last_beacon_entropy: Entropy, view: View) -> RandomBeacon:
return sha256(last_beacon + str(view).encode()).digest() return RandomBeacon(VERSION, sha256(last_beacon_entropy + view_to_bytes(view)).digest())
class RandomBeaconHandler: class RandomBeaconHandler:
@ -77,14 +78,14 @@ class RandomBeaconHandler:
""" """
self.last_beacon: RandomBeacon = beacon self.last_beacon: RandomBeacon = beacon
def verify_happy(self, new_beacon: RandomBeacon) -> bool: def verify_happy(self, new_beacon: RandomBeacon, pk: PublicKey, view: View) -> bool:
if NormalMode.verify(new_beacon): if NormalMode.verify(new_beacon, pk, view):
self.last_beacon = new_beacon self.last_beacon = new_beacon
return True return True
return False return False
def verify_unhappy(self, new_beacon: RandomBeacon) -> bool: def verify_unhappy(self, new_beacon: RandomBeacon, view: View) -> bool:
if RecoveryMode.verify(self.last_beacon, new_beacon): if RecoveryMode.verify(self.last_beacon, new_beacon, view):
self.last_beacon = new_beacon self.last_beacon = new_beacon
return True return True
return False return False

View File

@ -7,6 +7,8 @@ from overlay import EntropyOverlay
@dataclass @dataclass
class BeaconizedBlock(Block): class BeaconizedBlock(Block):
beacon: RandomBeacon beacon: RandomBeacon
# public key of the proposer
pk: PublicKey
class BeaconizedCarnot(Carnot): class BeaconizedCarnot(Carnot):
@ -14,14 +16,9 @@ class BeaconizedCarnot(Carnot):
self.sk = sk self.sk = sk
self.pk = bytes(self.sk.get_g1()) self.pk = bytes(self.sk.get_g1())
self.random_beacon = RandomBeaconHandler( self.random_beacon = RandomBeaconHandler(
RandomBeacon( RecoveryMode.generate_beacon(entropy, -1)
version=0,
context=-1,
entropy=RecoveryMode.generate_beacon(entropy, -1),
proof=self.pk
)
) )
overlay.set_entropy(self.random_beacon.last_beacon.entropy) overlay.set_entropy(self.random_beacon.last_beacon.entropy())
super().__init__(self.pk, overlay=overlay) super().__init__(self.pk, overlay=overlay)
def approve_block(self, block: BeaconizedBlock, votes: Set[Vote]) -> Event: def approve_block(self, block: BeaconizedBlock, votes: Set[Vote]) -> Event:
@ -47,39 +44,28 @@ class BeaconizedCarnot(Carnot):
# root members send votes to next leader, we update our beacon first # root members send votes to next leader, we update our beacon first
if self.overlay.is_member_of_root_committee(self.id): if self.overlay.is_member_of_root_committee(self.id):
self.random_beacon.verify_happy(block.beacon) assert(self.random_beacon.verify_happy(block.beacon, block.pk, block.qc.view))
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy) self.overlay.set_entropy(self.random_beacon.last_beacon.entropy())
return Send(to=self.overlay.leader(), payload=vote) return Send(to=self.overlay.leader(), payload=vote)
# otherwise we send to the parent committee and update the beacon second # otherwise we send to the parent committee and update the beacon second
return_event = Send(to=self.overlay.parent_committee(self.id), payload=vote) return_event = Send(to=self.overlay.parent_committee(self.id), payload=vote)
self.random_beacon.verify_happy(block.beacon) assert(self.random_beacon.verify_happy(block.beacon, block.pk, block.qc.view))
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy) self.overlay.set_entropy(self.random_beacon.last_beacon.entropy())
return return_event return return_event
def receive_timeout_qc(self, timeout_qc: TimeoutQc): def receive_timeout_qc(self, timeout_qc: TimeoutQc):
super().receive_timeout_qc(timeout_qc) super().receive_timeout_qc(timeout_qc)
if timeout_qc.view < self.current_view: if timeout_qc.view < self.current_view:
return return
entropy = RecoveryMode.generate_beacon(self.random_beacon.last_beacon.entropy, timeout_qc.view) new_beacon = RecoveryMode.generate_beacon(self.random_beacon.last_beacon.entropy(), timeout_qc.view)
new_beacon = RandomBeacon( self.random_beacon.verify_unhappy(new_beacon, timeout_qc.view)
version=0, self.overlay.set_entropy(self.random_beacon.last_beacon.entropy())
context=self.current_view,
entropy=entropy,
proof=b""
)
self.random_beacon.verify_unhappy(new_beacon)
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy)
def propose_block(self, view: View, quorum: Quorum) -> Event: def propose_block(self, view: View, quorum: Quorum) -> Event:
beacon = RandomBeacon(
version=0,
context=self.current_view,
entropy=NormalMode.generate_beacon(self.sk, self.current_view),
proof=self.pk
)
event: Event = super().propose_block(view, quorum) event: Event = super().propose_block(view, quorum)
block = event.payload block = event.payload
block = BeaconizedBlock(view=block.view, qc=block.qc, _id=block._id, beacon=beacon) beacon = NormalMode.generate_beacon(self.sk, block.qc.view)
block = BeaconizedBlock(view=block.view, qc=block.qc, _id=block._id, beacon=beacon, pk = G1Element.from_bytes(self.pk))
event.payload = block event.payload = block
return event return event

View File

@ -8,66 +8,32 @@ from random import randint
class TestRandomBeaconVerification(TestCase): class TestRandomBeaconVerification(TestCase):
@staticmethod @staticmethod
def happy_entropy_and_proof(view: View) -> Tuple[Beacon, Proof]: def happy_beacon_and_pk(view: View) -> Tuple[RandomBeacon, PublicKey]:
sk = generate_random_sk() sk = generate_random_sk()
beacon = NormalMode.generate_beacon(sk, view) beacon = NormalMode.generate_beacon(sk, view)
return bytes(beacon), bytes(sk.get_g1()) return beacon, sk.get_g1()
@staticmethod @staticmethod
def unhappy_entropy(last_beacon: Beacon, view: View) -> Beacon: def unhappy_beacon(last_beacon: Entropy, view: View) -> RandomBeacon:
return RecoveryMode.generate_beacon(last_beacon, view) return RecoveryMode.generate_beacon(last_beacon, view)
def setUp(self): def setUp(self):
entropy, proof = self.happy_entropy_and_proof(0) beacon, pk = self.happy_beacon_and_pk(0)
self.beacon = RandomBeaconHandler( self.beacon = RandomBeaconHandler(beacon)
beacon=RandomBeacon(
version=0,
context=0,
entropy=entropy,
proof=proof
)
)
def test_happy(self): def test_happy(self):
for i in range(3): for i in range(3):
entropy, proof = self.happy_entropy_and_proof(i) new_beacon, pk = self.happy_beacon_and_pk(i)
new_beacon = RandomBeacon( self.beacon.verify_happy(new_beacon, pk, i)
version=0,
context=i,
entropy=entropy,
proof=proof
)
self.beacon.verify_happy(new_beacon)
self.assertEqual(self.beacon.last_beacon.context, 2)
def test_unhappy(self): def test_unhappy(self):
for i in range(1, 3): for i in range(1, 3):
entropy = self.unhappy_entropy(self.beacon.last_beacon.entropy, i) new_beacon = self.unhappy_beacon(self.beacon.last_beacon.entropy(), i)
new_beacon = RandomBeacon( self.beacon.verify_unhappy(new_beacon, i)
version=0,
context=i,
entropy=entropy,
proof=b""
)
self.beacon.verify_unhappy(new_beacon)
self.assertEqual(self.beacon.last_beacon.context, 2)
def test_mixed(self): def test_mixed(self):
for i in range(1, 6, 2): for i in range(1, 6, 2):
entropy, proof = self.happy_entropy_and_proof(i) new_beacon, pk = self.happy_beacon_and_pk(i)
new_beacon = RandomBeacon( self.beacon.verify_happy(new_beacon, pk, i)
version=0, new_beacon = self.unhappy_beacon(self.beacon.last_beacon.entropy(), i+1)
context=i, self.beacon.verify_unhappy(new_beacon, i+1)
entropy=entropy,
proof=proof
)
self.beacon.verify_happy(new_beacon)
entropy = self.unhappy_entropy(self.beacon.last_beacon.entropy, i+1)
new_beacon = RandomBeacon(
version=0,
context=i+1,
entropy=entropy,
proof=b""
)
self.beacon.verify_unhappy(new_beacon)
self.assertEqual(self.beacon.last_beacon.context, 6)

View File

@ -6,7 +6,7 @@ from blspy import PrivateKey
from carnot import Id, Carnot, Block, Overlay, Vote, StandardQc, NewView from carnot import Id, Carnot, Block, Overlay, Vote, StandardQc, NewView
from beacon import generate_random_sk, RandomBeacon, NormalMode from beacon import generate_random_sk, RandomBeacon, NormalMode
from beconized_carnot import BeaconizedCarnot, BeaconizedBlock from beaconized_carnot import BeaconizedCarnot, BeaconizedBlock
from overlay import FlatOverlay, EntropyOverlay from overlay import FlatOverlay, EntropyOverlay
from test_unhappy_path import parents_from_childs from test_unhappy_path import parents_from_childs
@ -18,7 +18,7 @@ def gen_node(sk: PrivateKey, overlay: Overlay, entropy: bytes = b""):
def succeed(nodes: Dict[Id, BeaconizedCarnot], proposed_block: BeaconizedBlock) -> (List[Vote], EntropyOverlay): def succeed(nodes: Dict[Id, BeaconizedCarnot], proposed_block: BeaconizedBlock) -> (List[Vote], EntropyOverlay):
overlay = FlatOverlay(list(nodes.keys())) overlay = FlatOverlay(list(nodes.keys()))
overlay.set_entropy(proposed_block.beacon.entropy) overlay.set_entropy(proposed_block.beacon.entropy())
# broadcast the block # broadcast the block
for node in nodes.values(): for node in nodes.values():
@ -94,23 +94,19 @@ def fail(nodes: Dict[Id, BeaconizedCarnot], proposed_block: BeaconizedBlock) ->
def add_genesis_block(carnot: BeaconizedCarnot, sk: PrivateKey) -> Block: def add_genesis_block(carnot: BeaconizedCarnot, sk: PrivateKey) -> Block:
entropy = NormalMode.generate_beacon(sk, -1) beacon = NormalMode.generate_beacon(sk, -1)
genesis_block = BeaconizedBlock( genesis_block = BeaconizedBlock(
view=0, view=0,
qc=StandardQc(block=b"", view=0), qc=StandardQc(block=b"", view=0),
_id=b"", _id=b"",
beacon=RandomBeacon( beacon=beacon,
version=0, pk=sk.get_g1()
context=-1,
entropy=entropy,
proof=bytes(sk.get_g1())
)
) )
carnot.safe_blocks[genesis_block.id()] = genesis_block carnot.safe_blocks[genesis_block.id()] = genesis_block
carnot.receive_block(genesis_block) carnot.receive_block(genesis_block)
carnot.local_high_qc = genesis_block.qc carnot.local_high_qc = genesis_block.qc
carnot.current_view = 1 carnot.current_view = 1
carnot.overlay.set_entropy(entropy) carnot.overlay.set_entropy(beacon.entropy())
return genesis_block return genesis_block
@ -121,7 +117,7 @@ def initial_setup(test_case: TestCase, size: int) -> (Dict[Id, Carnot], Carnot,
nodes = dict(gen_node(key, FlatOverlay(nodes_ids), bytes(genesis_sk.get_g1())) for key in keys) nodes = dict(gen_node(key, FlatOverlay(nodes_ids), bytes(genesis_sk.get_g1())) for key in keys)
genesis_block = None genesis_block = None
overlay = FlatOverlay(nodes_ids) overlay = FlatOverlay(nodes_ids)
overlay.set_entropy(NormalMode.generate_beacon(genesis_sk, -1)) overlay.set_entropy(NormalMode.generate_beacon(genesis_sk, -1).entropy())
leader: Carnot = nodes[overlay.leader()] leader: Carnot = nodes[overlay.leader()]
for node in nodes.values(): for node in nodes.values():
genesis_block = add_genesis_block(node, genesis_sk) genesis_block = add_genesis_block(node, genesis_sk)
@ -140,7 +136,7 @@ def initial_setup(test_case: TestCase, size: int) -> (Dict[Id, Carnot], Carnot,
proposed_block = leader.propose_block(1, genesis_votes).payload proposed_block = leader.propose_block(1, genesis_votes).payload
test_case.assertIsNotNone(proposed_block) test_case.assertIsNotNone(proposed_block)
overlay = FlatOverlay(nodes_ids) overlay = FlatOverlay(nodes_ids)
overlay.set_entropy(NormalMode.generate_beacon(genesis_sk, -1)) overlay.set_entropy(genesis_block.beacon.entropy())
return nodes, leader, proposed_block, overlay return nodes, leader, proposed_block, overlay