Explorar o código

[Ouroboros] implemented basic blockchain script/research/PoS-blockchain/ouroboros

mohab %!s(int64=4) %!d(string=hai) anos
pai
achega
7b8eb7d16d

BIN=BIN
lc.file


+ 45 - 0
script/research/PoS-blockchain/ouroboros/beacon.py

@@ -0,0 +1,45 @@
+from clock import SynchedNTPClock
+from vrf import VRF
+import threading
+import time 
+
+'''
+\class TrustedBeacon
+
+the trusted beacon is decentralized, such that at the onset of the Epoch,
+the leader of the first slot generated the signed seed, and release the signature, 
+proof, and base to the genesis block.
+
+#TODO implement trustedbeacon as a node
+'''
+class TrustedBeacon(SynchedNTPClock, threading.Thread):
+    def __init__(self, node, epoch_length):
+        SynchedNTPClock.__init__(self.epoch_length)
+        threading.Thread.__init__(self)
+        self.daemon=True
+        self.epoch_length=epoch_length # how many slots in a a block
+        self.node = node #stakeholder
+        self.vrf = VRF(self.node.vrf_pk, self.node.vrf_sk, self.node.vrk_base)
+        self.current_slot = self.slot
+
+    def run(self):
+        self.__background()
+
+    def __background(self):
+        current_epoch = self.slot
+        while True:
+            if self.slot != current_epoch:
+                current_epoch = self.slot
+                self.__callback()
+
+    def __callback(self):
+        self.current_slot = self.slot
+        sigma, proof = self.vrf.sign(self.current_slot)
+        if self.slot%self.epoch_length==0:
+            self.node.new_slot(self.current_slot, sigma, proof)
+        else:
+            self.node.new_slot(self.current_slot, sigma, proof, True)
+
+    def verify(self, y, pi, pk_raw, g):
+        return VRF.verify(self.current_slot, y, pi, pk_raw, g)
+    

+ 91 - 0
script/research/PoS-blockchain/ouroboros/block.py

@@ -0,0 +1,91 @@
+import json
+from utils import encode_genesis_data, decode_gensis_data, state_hash
+
+'''
+single block B_i for slot i in the system live time L,
+assigned to stakeholder U_j, with propability P_j_i,
+in the chain C, should be signed by U_j keys.
+'''
+class Block(object):
+    '''
+    @param previous_block: parent block to the current block
+    @param data: is the transaction, or contracts in the leadger, or gensis block data, 
+        data is expected to be binary, no format is enforced
+    @param slot_uid: the block corresponding slot monotonically increasing index, 
+        it's one-based
+    @param gensis: boolean, True for gensis block
+    '''
+    def __init__(self, previous_block, data, slot_uid, genesis=False):
+        # state is hte hash of the previous block in the blockchain
+        self.state=''
+        if slot_uid>1:
+            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)

+ 39 - 0
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}")
+
+
+    

+ 46 - 0
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)

+ 68 - 0
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

+ 53 - 0
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

+ 14 - 0
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

+ 13 - 0
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()

+ 70 - 0
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

+ 56 - 0
script/research/PoS-blockchain/ouroboros/utils.py

@@ -1,3 +1,7 @@
+import random
+import joblib
+import pickle
+
 def extended_euclidean_algorithm(a, b):
 def extended_euclidean_algorithm(a, b):
     """
     """
     Returns a three-tuple (gcd, x, y) such that
     Returns a three-tuple (gcd, x, y) such that
@@ -38,3 +42,55 @@ def inverse_of(n, p):
             'modulo {}'.format(n, p))
             'modulo {}'.format(n, p))
     else:
     else:
         return x % p
         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)

+ 27 - 16
script/research/PoS-blockchain/ouroboros/vrf.py

@@ -2,37 +2,48 @@ from streamlet.logger import Logger
 import random as rnd
 import random as rnd
 from  tate_bilinear_pairing import eta, ecc
 from  tate_bilinear_pairing import eta, ecc
 from ouroboros.utils import inverse_of
 from ouroboros.utils import inverse_of
+from utils import vrf_hash
 eta.init(369)
 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):
 class VRF(object):
     '''
     '''
     verifiable random function implementation
     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)
         self.log = Logger(self)
-        #TODO (res) adhoc temporary
-        self.g = ecc.gen()
-        self.__gen()
         self.order = ecc.order()
         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
     short signature without random oracle
     @param x: message to be signed
     @param x: message to be signed
+    @return y (the signature), pi (the proof)
     '''
     '''
     def sign(self, x):
     def sign(self, x):
         pi = ecc.scalar_mult(inverse_of(x+self.sk, self.order), self.g)
         pi = ecc.scalar_mult(inverse_of(x+self.sk, self.order), self.g)
         y = eta.pairing(*self.g[1:], *pi[1:])
         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
     verify signature
@@ -56,4 +67,4 @@ class VRF(object):
             print(f"proposed {x}, {y}, {pi}, {pk_raw}, {g}")
             print(f"proposed {x}, {y}, {pi}, {pk_raw}, {g}")
             print(f"lhs: {lhs},\nrhs: {rhs}")
             print(f"lhs: {lhs},\nrhs: {rhs}")
             return False
             return False
-        return True
+        return True