From 7b8eb7d16d2c8eece5bfd056e2518e060836adea Mon Sep 17 00:00:00 2001 From: mohab Date: Sun, 6 Feb 2022 14:44:51 +0200 Subject: [PATCH] [Ouroboros] implemented basic blockchain script/research/PoS-blockchain/ouroboros --- lc.file | Bin 0 -> 54 bytes .../PoS-blockchain/ouroboros/beacon.py | 45 +++++++++ .../PoS-blockchain/ouroboros/block.py | 91 ++++++++++++++++++ .../PoS-blockchain/ouroboros/blockchain.py | 39 ++++++++ .../PoS-blockchain/ouroboros/clock.py | 46 +++++++++ .../PoS-blockchain/ouroboros/environment.py | 68 +++++++++++++ .../PoS-blockchain/ouroboros/epoch.py | 53 ++++++++++ .../research/PoS-blockchain/ouroboros/kes.py | 14 +++ .../PoS-blockchain/ouroboros/logger.py | 13 +++ .../PoS-blockchain/ouroboros/stakeholder.py | 70 ++++++++++++++ .../PoS-blockchain/ouroboros/utils.py | 56 +++++++++++ .../research/PoS-blockchain/ouroboros/vrf.py | 43 ++++++--- 12 files changed, 522 insertions(+), 16 deletions(-) create mode 100644 lc.file create mode 100644 script/research/PoS-blockchain/ouroboros/beacon.py create mode 100644 script/research/PoS-blockchain/ouroboros/block.py create mode 100644 script/research/PoS-blockchain/ouroboros/blockchain.py create mode 100644 script/research/PoS-blockchain/ouroboros/clock.py create mode 100644 script/research/PoS-blockchain/ouroboros/environment.py create mode 100644 script/research/PoS-blockchain/ouroboros/epoch.py create mode 100644 script/research/PoS-blockchain/ouroboros/kes.py create mode 100644 script/research/PoS-blockchain/ouroboros/logger.py create mode 100644 script/research/PoS-blockchain/ouroboros/stakeholder.py diff --git a/lc.file b/lc.file new file mode 100644 index 0000000000000000000000000000000000000000..91276b807853878246cd9c5027c49ba4d3e9018c GIT binary patch literal 54 zcmZo*nX1hI0kKnhIO5}T6EpMT1: + self.state=state_hash(previous_block) + self.tx = data + self.sl = slot_uid + self.is_genesis=genesis + + def __repr__(self): + if self.is_genesis: + return "GensisBlock at {slot:"+self.sl+",data:"+self.tx+",state:"+self.state+"}\n"+\ + decode_gensis_data(self.data) + return "Block at {slot:"+self.sl+",data:"+self.tx+",state:"+self.state+"}" + + def __eq__(self, block): + return self.state==block.state and \ + self.tx == block.tx and \ + self.sl == block.sl + + def to_json(self): + d = {'state':self.state, \ + 'data': self.tx, \ + 'sl': self.sl} + return json.encoder(d) + + @property + def state(self): + return self.st + + @property + def data(self): + return self.tx + + @property + def slot(self): + return self.sl + + @property + def empty(self): + return (self.data=='' or self.slot<0) and self.state=='' + +class GensisBlock(Block): + ''' + @param data: data is dictionary of list of (pk_i, s_i) public key, + and stake respectively of the corresponding stakeholder U_i, + seed of the leader election function. + ''' + def __init__(self, previous_block, data, slot_uid): + # stakeholders is list of tuple (pk_i, s_i) for the ith stakeholder + self.stakeholders = data['stakeholders'] + self.seed = data['seed'] + data = encode_genesis_data(self.stakeholders, self.seed) + super.__init__(previous_block, data, slot_uid, True) + ''' + @return: the number of participating stakeholders in the blockchain + ''' + @property + def length(self): + return len(self.stakeholders) + + def __getitem__(self, i): + if i<0 or i>=self.length: + raise "index is out of range!" + return self.stakeholders[i] + +''' +block lead by an adversary, or +lead by offline leader +is an empty Block +''' +class EmptyBlock(Block): + def __init__(self): + super.__init__(None, '', -1, False) diff --git a/script/research/PoS-blockchain/ouroboros/blockchain.py b/script/research/PoS-blockchain/ouroboros/blockchain.py new file mode 100644 index 000000000..64852b3cf --- /dev/null +++ b/script/research/PoS-blockchain/ouroboros/blockchain.py @@ -0,0 +1,39 @@ +from logger import Logger +''' +Non-forkable Blockchain for simplicity +#TODO consider forkable property +''' +class Blockchain(object): + def __init__(self, R): + self.blocks = [] + self.log = Logger(self) + self.R = R # how many slots in single epoch + + @property + def epoch_length(self): + return self.R + + def __repr__(self): + buff='' + for i in range(len(self.blocks)): + buff+=self.blocks[i] + return buff + + def __getitem__(self, i): + return self.blocks[i] + + def __len__(self): + return len(self.blocks) + + def __add_block(self, block): + self.blocks.append(block) + + def add_epoch(self, epoch): + for idx, block in enumerate(epoch): + if not block.empty: + self.__add_block(block) + else: + self.log.info(f"met an empty block at index of index: {block.index},\nrelative slot:{idx}\nabsolute slot: {self.length*idx+block.slot}") + + + \ No newline at end of file diff --git a/script/research/PoS-blockchain/ouroboros/clock.py b/script/research/PoS-blockchain/ouroboros/clock.py new file mode 100644 index 000000000..f8781ea05 --- /dev/null +++ b/script/research/PoS-blockchain/ouroboros/clock.py @@ -0,0 +1,46 @@ +''' +synchronized NTP clock +''' + +import ntplib +from time import ctime +import math + +class SynchedNTPClock(object): + + def __init__(self, slot_length=180, ntp_server='europe.pool.ntp.org'): + #TODO how long should be the slot length + self.slot_length=slot_length + self.ntp_server = ntp_server + self.ntp_client = ntplib.NTPClient() + #TODO validate the server + # when was darkfi birthday? as seconds since the epoch + self.darkfi_epoch=0 + + def __repr__(self): + return 'darkfi time: '+ ctime(self.darkfi_time) + ', current synched time: ' + ctime(self.synched_time) + + def __get_time_stat(self): + response=None + success=True + while not success: + try: + response = self.ntp_client.request(self.ntp_server, version=3) + success=True + except ntplib.NTPException as e: + print("connection failed: {}".format(e.what())) + return response + + @property + def synched_time(self): + state = self.__get_time_stat() + synched_time = state.tx_time + return synched_time + + @property + def darkfi_time(self): + return self.synched_time - self.darkfi_epoch + + @property + def slot(self): + return math.floor(self.darkfi_time/self.slot_length) diff --git a/script/research/PoS-blockchain/ouroboros/environment.py b/script/research/PoS-blockchain/ouroboros/environment.py new file mode 100644 index 000000000..d2ffbdef5 --- /dev/null +++ b/script/research/PoS-blockchain/ouroboros/environment.py @@ -0,0 +1,68 @@ +import numpy as np +import math +import random +''' +\class Z is the environment +''' +class Z(object): + def __init__(self, stakeholdes, epoch_length=100): + self.epoch_length=epoch_length + self.stakeholders = np.array(stakeholdes) + self.adversary_mask=np.array([True]*len(stakeholdes)) + ''' + return genesis data of the current epoch + ''' + def get_genesis_data(self): + #TODO implement + pass + + @property + def current_leader_vrf_pk(self): + #TODO implement + pass + + @property + def current_leader_vrf_g(self): + #TODO implement + pass + + #TODO complete + def obfuscate_idx(self, i): + return i + + #TODO complete + def deobfuscate_idx(self, i): + return i + + def corrupt(self, i): + if i<0 or i>len(self.adversary_mask): + return False + self.adversary_mask[self.deobfuscate_idx(i)]=False + return True + + ''' + return the length of all parties + ''' + def __len__(self): + return len(self.stakeholders) + + @property + def length(self): + return len(self.stakeholders) + @property + def honest(self): + return len(self.stakeholders[self.adversary_mask]) + + def select_epoch_leaders(self, sigma): + def leader_selection_hash(sigma): + Y = np.array(sigma) + y_hypotenuse2 = math.ceil(np.sum(Y[1]**2+Y[2]**2)) + return y_hypotenuse2 + seed = leader_selection_hash(sigma) + random.seed(seed) + leader_idx=seed%self.length + leader = self.stakeholders[leader_idx] + while not self.adversary_mask[leader_idx]: + leader_idx=random.randint(0,self.length) + #TODO select the following leader for this epoch, note, + # under a single condition that no one is able to predict who is next diff --git a/script/research/PoS-blockchain/ouroboros/epoch.py b/script/research/PoS-blockchain/ouroboros/epoch.py new file mode 100644 index 000000000..517991298 --- /dev/null +++ b/script/research/PoS-blockchain/ouroboros/epoch.py @@ -0,0 +1,53 @@ +from utils import state_hash + +class Epoch(object): + ''' + epoch spans R slots, + maximum number of block in Epoch is R + epoch must start with gensis block B0 + ''' + def __init__(self, gensis_block, R, epoch_idx): + self.gensis_block=gensis_block + self.blocks = [] + self.R = R #maximum epoch legnth, and it's a fixed property of the system + self.e = epoch_idx + self.index=0 + @property + def slot(self): + return self.gensis_block.sl + + @property + def length(self): + return len(self.blocks) + + @property + def index(self): + return self.e + + @property + def genesis(self): + if self.length==0: + return None + return self.blocks[0] + + def add_block(self, block): + if len(self.blocks)>0 and not block.state==state_hash(self.block[-1]): + #TODO we dealing with corrupt stakeholder, + # action to be taken + # the caller of the add_block should execute (corrupt,U) + pass + if self.length==self.R: + raise f"can't exceed Epoch's length: {self.length}" + self.blocks.append(block) + + def __iter__(self): + return self + + def __next__(self): + for i in range(self.length): + try: + res=self.blocks[self.index] + except IndexError: + raise StopIteration + self.index+=1 + return res \ No newline at end of file diff --git a/script/research/PoS-blockchain/ouroboros/kes.py b/script/research/PoS-blockchain/ouroboros/kes.py new file mode 100644 index 000000000..1bfb8a8b9 --- /dev/null +++ b/script/research/PoS-blockchain/ouroboros/kes.py @@ -0,0 +1,14 @@ +''' +forward secure signature scheme by key evolving +''' +class KES(object): + def __init__(sefl): + pass + def gen(self): + pk=None + sk=None + return pk, sk + def verify(self, sigma, message, pk): + return True + def update(self): + pass diff --git a/script/research/PoS-blockchain/ouroboros/logger.py b/script/research/PoS-blockchain/ouroboros/logger.py new file mode 100644 index 000000000..1885b99ba --- /dev/null +++ b/script/research/PoS-blockchain/ouroboros/logger.py @@ -0,0 +1,13 @@ +class Logger(object): + def __init__(self, obj): + self.obj = obj + + def info(self, payload): + print(f"\t[{self.obj}]:\n{payload}") + + def warn(self, payload): + print(f"\t[{self.obj}]:\n{payload}") + + def error(self, pyaload): + print(f"\t[{self.obj}]:\n{payload}") + exit() \ No newline at end of file diff --git a/script/research/PoS-blockchain/ouroboros/stakeholder.py b/script/research/PoS-blockchain/ouroboros/stakeholder.py new file mode 100644 index 000000000..b3c8e74ba --- /dev/null +++ b/script/research/PoS-blockchain/ouroboros/stakeholder.py @@ -0,0 +1,70 @@ +from block import Block, GensisBlock, EmptyBlock +from blockchain import Blockchain +from epoch import Epoch +from beacon import TrustedBeacon +from vrf import generate_vrf_keys +from utils import * +import numpy as np +import math + +''' +\class Stakeholder +''' +class Stakeholder(object): + def __init__(self, env, epoch_length=100, passwd='password'): + self.passwd=passwd + self.epoch_length=epoch_length + self.blockchain = Blockchain(self.epoch_length) + self.beacon = TrustedBeacon(self, self.epoch_length) + self.beacon.start() + pk, sk, g = generate_vrf_keys(self.passwd) + self.vrf_pk = pk + self.vrf_sk = sk + self.vrf_base = g + self.current_block = None + self.uncommited_tx='' + self.tx='' + self.current_slot_uid = self.beacon.slot + self.current_epoch = None + self.env = env + + @property + def epoch_index(self): + return round(self.current_slot_uid/self.epoch_length) + + ''' + it's a callback function, and called by the diffuser + ''' + def new_slot(self, slot, sigma, proof, new_epoch=False): + ''' + #TODO implement praos + for this implementation we assume synchrony, + and at this point, and no delay is considered (for simplicity) + ''' + + if not self.beacon.verify(sigma, proof, self.env.current_leader_vrf_pk, self.env.current_leader_vrf_g): + #TODO the leader is corrupted, action to be taken against the corrupt stakeholder + #in this case this slot is empty + self.current_block=EmptyBlock() + self.current_epoch.add_block(self.current_block) + return + self.current_slot_uid = slot + if new_epoch: + # add epoch to the ledger + if self.current_slot_uid > 1: + self.blockchain.add_epoch(self.current_epoch) + #kickoff gensis block + self.tx = self.env.get_genesis_data() + self.current_block=GensisBlock(self.current_block, self.tx, self.current_slot_uid) + self.current_epoch=Epoch(self.current_block, self.epoch_length, self.epoch_index) + #TODO elect leaders + self.select_leader(slot, sigma, proof) + + else: + self.current_block=Block(self.current_block, self.tx, self.current_slot_uid) + self.current_epoch.add_block(self.current_block) + + + def select_leader(self, slot, sigma, proof): + #TODO implement + pass \ No newline at end of file diff --git a/script/research/PoS-blockchain/ouroboros/utils.py b/script/research/PoS-blockchain/ouroboros/utils.py index 09bf76e85..aafd2bae9 100644 --- a/script/research/PoS-blockchain/ouroboros/utils.py +++ b/script/research/PoS-blockchain/ouroboros/utils.py @@ -1,3 +1,7 @@ +import random +import joblib +import pickle + def extended_euclidean_algorithm(a, b): """ Returns a three-tuple (gcd, x, y) such that @@ -38,3 +42,55 @@ def inverse_of(n, p): 'modulo {}'.format(n, p)) else: return x % p + +''' +@param nums: list of weight +@param true_rnd_fn: truely random function +@return zero-based index of the truely selected element +''' +def weighted_random(nums, true_rnd_fn=random.random): + """ + nums is list of weight, it return the truely random + weighted value. + """ + L = len(nums) + pair = [(i, nums[i]) for i in range(L)] + pair.sort(key=lambda p: p[1]) + tot = sum([pair[i][1] for i in range(L)]) + frequency = [pair[i][1]/tot for i in range(L)] + acc_prop = [sum(frequency[:i+1]) for i in range(L)] + rnd = true_rnd_fn() + for elected in range(L): + if rnd<=acc_prop[elected]: + break + return pair[elected][0] + + +''' +@param data: data is dictionary of list of (pk_i, s_i) public key, + and stake respectively of the corresponding stakeholder U_i, + seed of the leader election function. +''' +def encode_genesis_data(data): + return pickle.dumps(data) + +def decode_gensis_data(encoded_data): + return pickle.loads(encoded_data) + +''' +TODO this is a adhoc solution +this has is used to compute the state of block from the previous block +''' +def state_hash(obj): + return hash(obj) + + +''' +TODO this is a adhoc solution +this is used to generate VRF's sk from some seed +note there is a need for nounce to be concatenated with the seed, +just in case two stakeholders started with the same seed +(for the time being the seed is provided by the stakeholder, it's stakeholder passowrd) +''' +def vrf_hash(seed): + return hash(seed) diff --git a/script/research/PoS-blockchain/ouroboros/vrf.py b/script/research/PoS-blockchain/ouroboros/vrf.py index 5485b980b..e1d47cade 100644 --- a/script/research/PoS-blockchain/ouroboros/vrf.py +++ b/script/research/PoS-blockchain/ouroboros/vrf.py @@ -2,37 +2,48 @@ from streamlet.logger import Logger import random as rnd from tate_bilinear_pairing import eta, ecc from ouroboros.utils import inverse_of +from utils import vrf_hash eta.init(369) +''' +gernate vrf keys for stakeholder +@param sk_seed: this is suppoed to be the password of the stakeholder +''' +def generate_vrf_keys(sk_seed): + ''' + generate pk/sk + return: list of pk (public key), sk(secret key), base(field base) + ''' + sk = vrf_hash(sk_seed) + base = ecc.gen() + pk = ecc.scalar_mult(sk, base) + return (pk, sk, base) + class VRF(object): ''' verifiable random function implementation ''' - def __init__(self): - self.pk = None - self.sk = None + def __init__(self, pk, sk, base): + self.pk = pk + self.sk = sk + self.g=base self.log = Logger(self) - #TODO (res) adhoc temporary - self.g = ecc.gen() - self.__gen() self.order = ecc.order() - def __gen(self): - ''' - generate pk/sk - ''' - # TODO implement that is simple sk choosing mechanism for poc; - self.sk = rnd.randint(0,1000) - self.pk = ecc.scalar_mult(self.sk, self.g) - ''' short signature without random oracle @param x: message to be signed + @return y (the signature), pi (the proof) ''' def sign(self, x): pi = ecc.scalar_mult(inverse_of(x+self.sk, self.order), self.g) y = eta.pairing(*self.g[1:], *pi[1:]) - return (y, pi, self.g) + return (y, pi) + + def update(self, pk, sk, g): + self.pk = pk + self.sk = sk + self.g = g ''' verify signature @@ -56,4 +67,4 @@ class VRF(object): print(f"proposed {x}, {y}, {pi}, {pk_raw}, {g}") print(f"lhs: {lhs},\nrhs: {rhs}") return False - return True + return True \ No newline at end of file