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:
parent
b0edea6a98
commit
34617dc911
|
@ -5,67 +5,68 @@ from typing import TypeAlias
|
|||
|
||||
# carnot imports
|
||||
# lib imports
|
||||
from blspy import PrivateKey, Util, PopSchemeMPL, G2Element, G1Element
|
||||
from blspy import PrivateKey, Util, BasicSchemeMPL, G2Element, G1Element
|
||||
|
||||
# stdlib imports
|
||||
from hashlib import sha256
|
||||
|
||||
View: TypeAlias = int
|
||||
Beacon: TypeAlias = bytes
|
||||
Proof: TypeAlias = bytes # For now this is gonna be a public key, in future research we may pivot to zk proofs.
|
||||
|
||||
Sig: TypeAlias = bytes
|
||||
Entropy: TypeAlias = bytes
|
||||
PublicKey: TypeAlias = G1Element
|
||||
VERSION = 0
|
||||
|
||||
def generate_random_sk() -> PrivateKey:
|
||||
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
|
||||
class RandomBeacon:
|
||||
version: int
|
||||
context: View
|
||||
entropy: Beacon
|
||||
# TODO: Just the happy path beacons owns a proof, we can set the proof to empty bytes for now.
|
||||
# Probably we should separate this into two kinds of beacons and group them under a single type later on.
|
||||
proof: Proof
|
||||
sig: Sig
|
||||
|
||||
def entropy(self) -> Entropy:
|
||||
return self.sig
|
||||
|
||||
|
||||
class NormalMode:
|
||||
|
||||
@staticmethod
|
||||
def verify(beacon: RandomBeacon) -> bool:
|
||||
def verify(beacon: RandomBeacon, pk: PublicKey, view: View) -> bool:
|
||||
"""
|
||||
:param proof: BLS signature
|
||||
:param beacon: Beacon is signature for current view
|
||||
:param view: View to verify beacon upon
|
||||
:param beacon: the provided beacon
|
||||
:param view: view to verify beacon upon
|
||||
:param pk: public key of the issuer of the beacon
|
||||
:return:
|
||||
"""
|
||||
# TODO: Actually verify that the message is propoerly signed
|
||||
sig = G2Element.from_bytes(beacon.entropy)
|
||||
proof = G1Element.from_bytes(beacon.proof)
|
||||
return PopSchemeMPL.verify(proof, Util.hash256(str(beacon.context).encode()), sig)
|
||||
sig = G2Element.from_bytes(beacon.sig)
|
||||
return BasicSchemeMPL.verify(pk, view_to_bytes(view), sig)
|
||||
|
||||
@staticmethod
|
||||
def generate_beacon(private_key: PrivateKey, view: View) -> Beacon:
|
||||
return bytes(PopSchemeMPL.sign(private_key, Util.hash256(str(view).encode())))
|
||||
def generate_beacon(private_key: PrivateKey, view: View) -> RandomBeacon:
|
||||
return RandomBeacon(VERSION, bytes(BasicSchemeMPL.sign(private_key, view_to_bytes(view))))
|
||||
|
||||
|
||||
class RecoveryMode:
|
||||
|
||||
@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 beacon:
|
||||
:param view:
|
||||
:param last_beacon: beacon for view - 1
|
||||
:param beacon: beacon for view
|
||||
:param view: the view to verify beacon upon
|
||||
:return:
|
||||
"""
|
||||
b = sha256(last_beacon.entropy + str(beacon.context).encode()).digest()
|
||||
return b == beacon.entropy
|
||||
b = sha256(last_beacon.entropy() + view_to_bytes(view)).digest()
|
||||
return b == beacon.entropy()
|
||||
|
||||
@staticmethod
|
||||
def generate_beacon(last_beacon: Beacon, view: View) -> Beacon:
|
||||
return sha256(last_beacon + str(view).encode()).digest()
|
||||
def generate_beacon(last_beacon_entropy: Entropy, view: View) -> RandomBeacon:
|
||||
return RandomBeacon(VERSION, sha256(last_beacon_entropy + view_to_bytes(view)).digest())
|
||||
|
||||
|
||||
class RandomBeaconHandler:
|
||||
|
@ -77,14 +78,14 @@ class RandomBeaconHandler:
|
|||
"""
|
||||
self.last_beacon: RandomBeacon = beacon
|
||||
|
||||
def verify_happy(self, new_beacon: RandomBeacon) -> bool:
|
||||
if NormalMode.verify(new_beacon):
|
||||
def verify_happy(self, new_beacon: RandomBeacon, pk: PublicKey, view: View) -> bool:
|
||||
if NormalMode.verify(new_beacon, pk, view):
|
||||
self.last_beacon = new_beacon
|
||||
return True
|
||||
return False
|
||||
|
||||
def verify_unhappy(self, new_beacon: RandomBeacon) -> bool:
|
||||
if RecoveryMode.verify(self.last_beacon, new_beacon):
|
||||
def verify_unhappy(self, new_beacon: RandomBeacon, view: View) -> bool:
|
||||
if RecoveryMode.verify(self.last_beacon, new_beacon, view):
|
||||
self.last_beacon = new_beacon
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -7,6 +7,8 @@ from overlay import EntropyOverlay
|
|||
@dataclass
|
||||
class BeaconizedBlock(Block):
|
||||
beacon: RandomBeacon
|
||||
# public key of the proposer
|
||||
pk: PublicKey
|
||||
|
||||
|
||||
class BeaconizedCarnot(Carnot):
|
||||
|
@ -14,14 +16,9 @@ class BeaconizedCarnot(Carnot):
|
|||
self.sk = sk
|
||||
self.pk = bytes(self.sk.get_g1())
|
||||
self.random_beacon = RandomBeaconHandler(
|
||||
RandomBeacon(
|
||||
version=0,
|
||||
context=-1,
|
||||
entropy=RecoveryMode.generate_beacon(entropy, -1),
|
||||
proof=self.pk
|
||||
RecoveryMode.generate_beacon(entropy, -1)
|
||||
)
|
||||
)
|
||||
overlay.set_entropy(self.random_beacon.last_beacon.entropy)
|
||||
overlay.set_entropy(self.random_beacon.last_beacon.entropy())
|
||||
super().__init__(self.pk, overlay=overlay)
|
||||
|
||||
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
|
||||
if self.overlay.is_member_of_root_committee(self.id):
|
||||
self.random_beacon.verify_happy(block.beacon)
|
||||
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy)
|
||||
assert(self.random_beacon.verify_happy(block.beacon, block.pk, block.qc.view))
|
||||
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy())
|
||||
return Send(to=self.overlay.leader(), payload=vote)
|
||||
|
||||
# otherwise we send to the parent committee and update the beacon second
|
||||
return_event = Send(to=self.overlay.parent_committee(self.id), payload=vote)
|
||||
self.random_beacon.verify_happy(block.beacon)
|
||||
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy)
|
||||
assert(self.random_beacon.verify_happy(block.beacon, block.pk, block.qc.view))
|
||||
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy())
|
||||
return return_event
|
||||
|
||||
def receive_timeout_qc(self, timeout_qc: TimeoutQc):
|
||||
super().receive_timeout_qc(timeout_qc)
|
||||
if timeout_qc.view < self.current_view:
|
||||
return
|
||||
entropy = RecoveryMode.generate_beacon(self.random_beacon.last_beacon.entropy, timeout_qc.view)
|
||||
new_beacon = RandomBeacon(
|
||||
version=0,
|
||||
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)
|
||||
new_beacon = RecoveryMode.generate_beacon(self.random_beacon.last_beacon.entropy(), timeout_qc.view)
|
||||
self.random_beacon.verify_unhappy(new_beacon, timeout_qc.view)
|
||||
self.overlay.set_entropy(self.random_beacon.last_beacon.entropy())
|
||||
|
||||
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)
|
||||
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
|
||||
return event
|
|
@ -8,66 +8,32 @@ from random import randint
|
|||
class TestRandomBeaconVerification(TestCase):
|
||||
|
||||
@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()
|
||||
beacon = NormalMode.generate_beacon(sk, view)
|
||||
return bytes(beacon), bytes(sk.get_g1())
|
||||
return beacon, sk.get_g1()
|
||||
|
||||
@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)
|
||||
|
||||
def setUp(self):
|
||||
entropy, proof = self.happy_entropy_and_proof(0)
|
||||
self.beacon = RandomBeaconHandler(
|
||||
beacon=RandomBeacon(
|
||||
version=0,
|
||||
context=0,
|
||||
entropy=entropy,
|
||||
proof=proof
|
||||
)
|
||||
)
|
||||
beacon, pk = self.happy_beacon_and_pk(0)
|
||||
self.beacon = RandomBeaconHandler(beacon)
|
||||
|
||||
def test_happy(self):
|
||||
for i in range(3):
|
||||
entropy, proof = self.happy_entropy_and_proof(i)
|
||||
new_beacon = RandomBeacon(
|
||||
version=0,
|
||||
context=i,
|
||||
entropy=entropy,
|
||||
proof=proof
|
||||
)
|
||||
self.beacon.verify_happy(new_beacon)
|
||||
self.assertEqual(self.beacon.last_beacon.context, 2)
|
||||
new_beacon, pk = self.happy_beacon_and_pk(i)
|
||||
self.beacon.verify_happy(new_beacon, pk, i)
|
||||
|
||||
def test_unhappy(self):
|
||||
for i in range(1, 3):
|
||||
entropy = self.unhappy_entropy(self.beacon.last_beacon.entropy, i)
|
||||
new_beacon = RandomBeacon(
|
||||
version=0,
|
||||
context=i,
|
||||
entropy=entropy,
|
||||
proof=b""
|
||||
)
|
||||
self.beacon.verify_unhappy(new_beacon)
|
||||
self.assertEqual(self.beacon.last_beacon.context, 2)
|
||||
new_beacon = self.unhappy_beacon(self.beacon.last_beacon.entropy(), i)
|
||||
self.beacon.verify_unhappy(new_beacon, i)
|
||||
|
||||
def test_mixed(self):
|
||||
for i in range(1, 6, 2):
|
||||
entropy, proof = self.happy_entropy_and_proof(i)
|
||||
new_beacon = RandomBeacon(
|
||||
version=0,
|
||||
context=i,
|
||||
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)
|
||||
new_beacon, pk = self.happy_beacon_and_pk(i)
|
||||
self.beacon.verify_happy(new_beacon, pk, i)
|
||||
new_beacon = self.unhappy_beacon(self.beacon.last_beacon.entropy(), i+1)
|
||||
self.beacon.verify_unhappy(new_beacon, i+1)
|
||||
|
|
|
@ -6,7 +6,7 @@ from blspy import PrivateKey
|
|||
|
||||
from carnot import Id, Carnot, Block, Overlay, Vote, StandardQc, NewView
|
||||
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 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):
|
||||
overlay = FlatOverlay(list(nodes.keys()))
|
||||
overlay.set_entropy(proposed_block.beacon.entropy)
|
||||
overlay.set_entropy(proposed_block.beacon.entropy())
|
||||
|
||||
# broadcast the block
|
||||
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:
|
||||
entropy = NormalMode.generate_beacon(sk, -1)
|
||||
beacon = NormalMode.generate_beacon(sk, -1)
|
||||
genesis_block = BeaconizedBlock(
|
||||
view=0,
|
||||
qc=StandardQc(block=b"", view=0),
|
||||
_id=b"",
|
||||
beacon=RandomBeacon(
|
||||
version=0,
|
||||
context=-1,
|
||||
entropy=entropy,
|
||||
proof=bytes(sk.get_g1())
|
||||
)
|
||||
beacon=beacon,
|
||||
pk=sk.get_g1()
|
||||
)
|
||||
carnot.safe_blocks[genesis_block.id()] = genesis_block
|
||||
carnot.receive_block(genesis_block)
|
||||
carnot.local_high_qc = genesis_block.qc
|
||||
carnot.current_view = 1
|
||||
carnot.overlay.set_entropy(entropy)
|
||||
carnot.overlay.set_entropy(beacon.entropy())
|
||||
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)
|
||||
genesis_block = None
|
||||
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()]
|
||||
for node in nodes.values():
|
||||
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
|
||||
test_case.assertIsNotNone(proposed_block)
|
||||
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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue