mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
[Ouroboros] implemented basic blockchain script/research/PoS-blockchain/ouroboros
This commit is contained in:
45
script/research/PoS-blockchain/ouroboros/beacon.py
Normal file
45
script/research/PoS-blockchain/ouroboros/beacon.py
Normal 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)
|
||||
|
||||
91
script/research/PoS-blockchain/ouroboros/block.py
Normal file
91
script/research/PoS-blockchain/ouroboros/block.py
Normal 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)
|
||||
39
script/research/PoS-blockchain/ouroboros/blockchain.py
Normal file
39
script/research/PoS-blockchain/ouroboros/blockchain.py
Normal 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}")
|
||||
|
||||
|
||||
|
||||
46
script/research/PoS-blockchain/ouroboros/clock.py
Normal file
46
script/research/PoS-blockchain/ouroboros/clock.py
Normal 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)
|
||||
68
script/research/PoS-blockchain/ouroboros/environment.py
Normal file
68
script/research/PoS-blockchain/ouroboros/environment.py
Normal 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
|
||||
53
script/research/PoS-blockchain/ouroboros/epoch.py
Normal file
53
script/research/PoS-blockchain/ouroboros/epoch.py
Normal 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
|
||||
14
script/research/PoS-blockchain/ouroboros/kes.py
Normal file
14
script/research/PoS-blockchain/ouroboros/kes.py
Normal 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
|
||||
13
script/research/PoS-blockchain/ouroboros/logger.py
Normal file
13
script/research/PoS-blockchain/ouroboros/logger.py
Normal 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()
|
||||
70
script/research/PoS-blockchain/ouroboros/stakeholder.py
Normal file
70
script/research/PoS-blockchain/ouroboros/stakeholder.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user