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

This commit is contained in:
mohab
2022-02-06 14:44:51 +02:00
parent 59ac25a54e
commit 7b8eb7d16d
12 changed files with 522 additions and 16 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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}")

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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