2015-07-19 19:01:20 +00:00
import copy , random , hashlib
from distributions import normal_distribution
import networksim
2015-09-12 21:46:40 +00:00
from voting_strategy import vote , default_vote
2015-08-27 13:38:48 +00:00
import math
2015-07-19 19:01:20 +00:00
2015-09-12 21:46:40 +00:00
# Number of validators
2015-07-19 19:01:20 +00:00
NUM_VALIDATORS = 20
2015-09-12 21:46:40 +00:00
# Block time
2015-08-27 13:38:48 +00:00
BLKTIME = 40
2015-09-12 21:46:40 +00:00
# 0 for no netsplits
# 1 for simulating a netsplit where 20% of validators jump off
# the network
# 2 for simulating the above netsplit, plus a 50-50 netsplit,
# plus reconvergence
2015-09-01 13:25:33 +00:00
NETSPLITS = 2
2015-09-12 21:46:40 +00:00
# Check the equality of finalized states
2015-09-01 13:25:33 +00:00
CHECK_INTEGRITY = True
2015-09-12 21:46:40 +00:00
# The genesis state root
2015-08-27 13:38:48 +00:00
GENESIS_STATE = 0
2015-07-19 19:01:20 +00:00
logging_level = 0
def log ( s , lvl ) :
if logging_level > = lvl :
print ( s )
2015-09-12 21:46:40 +00:00
# A signture specifies an initial height ("sign_from"), a finalized
# state from all blocks before that height and a list of probability
# bets from that height up to the latest height
2015-07-19 19:01:20 +00:00
class Signature ( ) :
2015-08-27 13:38:48 +00:00
def __init__ ( self , signer , probs , finalized_state , sign_from ) :
2015-09-12 21:46:40 +00:00
# The ID of the signer
2015-07-19 19:01:20 +00:00
self . signer = signer
2015-09-12 21:46:40 +00:00
# List of probability bets, expressed in log odds
2015-07-19 19:01:20 +00:00
self . probs = probs
# Hash of the signature (for db storage purposes)
2015-08-27 13:38:48 +00:00
self . hash = random . randrange ( 10 * * 14 )
# Finalized state
self . finalized_state = finalized_state
# Finalized height
self . sign_from = sign_from
2015-07-19 19:01:20 +00:00
2015-09-01 13:25:33 +00:00
def get_height ( self ) :
return self . sign_from + len ( self . probs )
2015-07-19 19:01:20 +00:00
2015-09-12 21:46:40 +00:00
# Right now, a block simply specifies a proposer and a height.
2015-07-19 19:01:20 +00:00
class Block ( ) :
def __init__ ( self , maker , height ) :
# The producer of the block
self . maker = maker
# The height of the block
self . height = height
# Hash of the signature (for db storage purposes)
2015-08-27 13:38:48 +00:00
self . hash = random . randrange ( 10 * * 20 ) + 10 * * 21 + 10 * * 23 * self . height
2015-07-19 19:01:20 +00:00
2015-09-12 21:46:40 +00:00
# A request to receive a block at a particular height
2015-09-01 13:25:33 +00:00
class BlockRequest ( ) :
def __init__ ( self , sender , height ) :
self . sender = sender
self . ask_height = height
self . hash = random . randrange ( 10 * * 14 )
2015-09-12 21:46:40 +00:00
# Toy state transition function (in production, do sequential
# apply_transaction here)
2015-08-27 13:38:48 +00:00
def state_transition ( state , block ) :
return state if block is None else ( state * * 3 + block . hash * * 5 ) % 10 * * 40
2015-07-19 19:01:20 +00:00
# A validator
class Validator ( ) :
def __init__ ( self , pos , network ) :
2015-09-12 21:46:40 +00:00
# Map from height to {node_id: latest_bet}
2015-08-27 13:38:48 +00:00
self . received_signatures = [ ]
# List of received blocks
self . received_blocks = [ ]
# Own probability estimates
self . probs = [ ]
2015-07-19 19:01:20 +00:00
# All objects that this validator has received; basically a database
self . received_objects = { }
2015-09-12 21:46:40 +00:00
# Time when the object was received
2015-08-27 13:38:48 +00:00
self . time_received = { }
2015-07-19 19:01:20 +00:00
# The validator's ID, and its position in the queue
self . pos = self . id = pos
2015-09-12 21:46:40 +00:00
# The offset of this validator's clock vs. real time
2015-08-28 10:23:03 +00:00
self . time_offset = normal_distribution ( 0 , 100 ) ( )
2015-07-19 19:01:20 +00:00
# The highest height that this validator has seen
self . max_height = 0
# The validator's hash chain
2015-08-27 13:38:48 +00:00
self . finalized_hashes = [ ]
# Finalized states
self . finalized_states = [ ]
2015-07-19 19:01:20 +00:00
# The highest height that the validator has finalized
2015-08-27 13:38:48 +00:00
self . max_finalized_height = - 1
2015-07-19 19:01:20 +00:00
# The network object
self . network = network
2015-08-27 13:38:48 +00:00
# Last time signed
self . last_time_signed = 0
2015-09-12 21:46:40 +00:00
# Next height to mine
2015-08-27 13:38:48 +00:00
self . next_height = self . pos
2015-07-19 19:01:20 +00:00
2015-09-12 21:46:40 +00:00
# Get the local time from the point of view of this validator, using the
# validator's offset from real time
2015-08-27 13:38:48 +00:00
def get_time ( self ) :
return self . network . time + self . time_offset
2015-07-19 19:01:20 +00:00
2015-09-12 21:46:40 +00:00
# Broadcast an object to the network
2015-09-01 13:25:33 +00:00
def broadcast ( self , obj ) :
self . network . broadcast ( self , obj )
2015-09-12 21:46:40 +00:00
# Create a signature
def sign ( self ) :
2015-08-27 13:38:48 +00:00
# Initialize the probability array, the core of the signature
best_guesses = [ None ] * len ( self . received_blocks )
sign_from = max ( 0 , self . max_finalized_height - 30 )
for i , b in list ( enumerate ( self . received_blocks ) ) [ sign_from : ] :
2015-09-12 21:46:40 +00:00
# Compute this validator's own initial vote based on when the block
# was received, compared to what time the block should have arrived
received_time = self . time_received [ b . hash ] if b is not None else None
my_opinion = default_vote ( BLKTIME * i , received_time , self . get_time ( ) , blktime = BLKTIME )
# Get others' bets on this height
2015-08-27 13:38:48 +00:00
votes = self . received_signatures [ i ] . values ( ) if i < len ( self . received_signatures ) else [ ]
2015-09-12 21:46:40 +00:00
votes = [ x for x in votes if x != 0 ]
# Fill in the not-yet-received votes with this validator's default bet
2015-08-27 13:38:48 +00:00
votes + = [ my_opinion ] * ( NUM_VALIDATORS - len ( votes ) )
2015-09-12 21:46:40 +00:00
vote_from_signatures = int ( vote ( votes ) )
# Add the bet to the list
bg = min ( vote_from_signatures , 10 if self . received_blocks [ i ] is not None else my_opinion )
2015-09-01 13:25:33 +00:00
best_guesses [ i ] = bg
2015-09-12 21:46:40 +00:00
# Request a block if we should have it, and should have had it for
# a long time, but don't
if vote_from_signatures > 3 and self . received_blocks [ i ] is None :
2015-09-01 13:25:33 +00:00
self . broadcast ( BlockRequest ( self . id , i ) )
2015-09-12 21:46:40 +00:00
elif i < len ( self . received_blocks ) - 50 and self . received_blocks [ i ] is None :
if random . random ( ) < 0.05 :
self . broadcast ( BlockRequest ( self . id , i ) )
# Block finalized
if best_guesses [ i ] > = 10 :
2015-08-27 13:38:48 +00:00
while len ( self . finalized_hashes ) < = i :
self . finalized_hashes . append ( None )
self . finalized_hashes [ i ] = self . received_blocks [ i ] . hash
2015-09-12 21:46:40 +00:00
# Absense of the block finalized
elif best_guesses [ i ] < = - 10 :
2015-08-27 13:38:48 +00:00
while len ( self . finalized_hashes ) < = i :
self . finalized_hashes . append ( None )
self . finalized_hashes [ i ] = False
2015-09-12 21:46:40 +00:00
# Add to the list of finalized states
2015-08-27 13:38:48 +00:00
while self . max_finalized_height < len ( self . finalized_hashes ) - 1 \
and self . finalized_hashes [ self . max_finalized_height + 1 ] is not None :
self . max_finalized_height + = 1
last_state = self . finalized_states [ - 1 ] if len ( self . finalized_states ) else GENESIS_STATE
self . finalized_states . append ( state_transition ( last_state , self . received_blocks [ self . max_finalized_height ] ) )
2015-09-01 13:25:33 +00:00
2015-08-27 13:38:48 +00:00
self . probs = self . probs [ : sign_from ] + best_guesses [ sign_from : ]
log ( ' Making signature: %r ' % self . probs [ - 10 : ] , lvl = 1 )
sign_from_state = self . finalized_states [ sign_from - 1 ] if sign_from > 0 else GENESIS_STATE
s = Signature ( self . pos , self . probs [ sign_from : ] , sign_from_state , sign_from )
all_signatures . append ( s )
return s
2015-07-19 19:01:20 +00:00
def on_receive ( self , obj ) :
# Ignore objects that we already know about
if obj . hash in self . received_objects :
return
# When receiving a block
if isinstance ( obj , Block ) :
log ( ' received block: %d %d ' % ( obj . height , obj . hash ) , lvl = 2 )
2015-08-27 13:38:48 +00:00
while len ( self . received_blocks ) < = obj . height :
self . received_blocks . append ( None )
self . received_blocks [ obj . height ] = obj
self . time_received [ obj . hash ] = self . get_time ( )
2015-09-12 21:46:40 +00:00
# Upon receiving a new block, make a new signature
s = self . sign ( )
2015-08-27 13:38:48 +00:00
self . network . broadcast ( self , s )
self . on_receive ( s )
2015-07-19 19:01:20 +00:00
self . network . broadcast ( self , obj )
# When receiving a signature
elif isinstance ( obj , Signature ) :
2015-08-27 13:38:48 +00:00
while len ( self . received_signatures ) < = len ( obj . probs ) + obj . sign_from :
self . received_signatures . append ( { } )
for i , p in enumerate ( obj . probs ) :
self . received_signatures [ i + obj . sign_from ] [ obj . signer ] = p
2015-07-19 19:01:20 +00:00
self . network . broadcast ( self , obj )
2015-09-01 13:25:33 +00:00
# Received a block request, respond if we have it
elif isinstance ( obj , BlockRequest ) :
if obj . ask_height < len ( self . received_blocks ) :
if self . received_blocks [ obj . ask_height ] is not None :
self . network . direct_send ( obj . sender , self . received_blocks [ obj . ask_height ] )
2015-07-19 19:01:20 +00:00
self . received_objects [ obj . hash ] = obj
2015-08-27 13:38:48 +00:00
self . time_received [ obj . hash ] = self . get_time ( )
2015-07-19 19:01:20 +00:00
# Run every tick
def tick ( self ) :
2015-08-27 13:38:48 +00:00
mytime = self . get_time ( )
target_time = BLKTIME * self . next_height
if mytime > = target_time :
o = Block ( self . pos , self . next_height )
self . next_height + = NUM_VALIDATORS
2015-07-19 19:01:20 +00:00
log ( ' making block: %d %d ' % ( o . height , o . hash ) , lvl = 1 )
2015-08-27 13:38:48 +00:00
if random . random ( ) < 0.9 :
self . network . broadcast ( self , o )
while len ( self . received_blocks ) < = o . height :
self . received_blocks . append ( None )
self . received_blocks [ o . height ] = o
2015-07-19 19:01:20 +00:00
self . received_objects [ o . hash ] = o
2015-08-27 13:38:48 +00:00
self . time_received [ o . hash ] = mytime
2015-07-19 19:01:20 +00:00
return o
validator_list = [ ]
future = { }
discarded = { }
finalized_blocks = { }
2015-08-27 13:38:48 +00:00
all_signatures = [ ]
2015-07-19 19:01:20 +00:00
now = [ 0 ]
2015-08-27 13:38:48 +00:00
def who_heard_of ( h , n ) :
2015-09-01 13:25:33 +00:00
o = ' '
for x in n . agents :
o + = ' 1 ' if h in x . received_objects else ' 0 '
return o
2015-08-27 13:38:48 +00:00
def get_opinions ( n ) :
o = [ ]
maxheight = 0
for x in n . agents :
maxheight = max ( maxheight , len ( x . probs ) )
for h in range ( maxheight ) :
p = ' '
q = ' '
for x in n . agents :
if len ( x . probs ) < = h :
2015-09-01 13:25:33 +00:00
p + = ' _ '
2015-09-12 21:46:40 +00:00
elif x . probs [ h ] < = - 10 :
2015-09-01 13:25:33 +00:00
p + = ' - '
2015-09-12 21:46:40 +00:00
elif x . probs [ h ] > = 10 :
2015-09-01 13:25:33 +00:00
p + = ' + '
else :
2015-09-12 21:46:40 +00:00
p + = str ( x . probs [ h ] ) + ' , '
2015-08-27 13:38:48 +00:00
q + = ' n ' if len ( x . received_blocks ) < = h or x . received_blocks [ h ] is None else ' y '
o . append ( ( h , p , q ) )
return o
2015-09-01 13:25:33 +00:00
2015-08-27 13:38:48 +00:00
def get_finalization_heights ( n ) :
2015-09-01 13:25:33 +00:00
o = [ ]
for x in n . agents :
o . append ( x . max_finalized_height )
return o
2015-08-27 13:38:48 +00:00
2015-07-19 19:01:20 +00:00
# Check how often blocks that are assigned particular probabilities of
# finalization by our algorithm are actually finalized
2015-08-27 13:38:48 +00:00
def calibrate ( finalized_hashes ) :
2015-09-12 22:01:08 +00:00
thresholds = range ( - 10 , 11 )
2015-07-19 19:01:20 +00:00
signed = [ 0 ] * ( len ( thresholds ) - 1 )
_finalized = [ 0 ] * ( len ( thresholds ) - 1 )
_discarded = [ 0 ] * ( len ( thresholds ) - 1 )
2015-08-27 13:38:48 +00:00
for s in all_signatures :
for i , prob in enumerate ( s . probs ) :
if i + s . sign_from > = len ( finalized_hashes ) :
continue
actual_result = 1 if finalized_hashes [ i + s . sign_from ] else 0
index = 0
2015-09-12 21:46:40 +00:00
while index + 2 < len ( thresholds ) and prob > thresholds [ index + 1 ] :
2015-08-27 13:38:48 +00:00
index + = 1
signed [ index ] + = 1
if actual_result == 1 :
_finalized [ index ] + = 1
elif actual_result == 0 :
_discarded [ index ] + = 1
2015-07-19 19:01:20 +00:00
for i in range ( len ( thresholds ) - 1 ) :
if _finalized [ i ] + _discarded [ i ] :
2015-09-12 22:01:08 +00:00
print ' Probability from %f to %f : %f ( %d of %d ) ' % ( thresholds [ i ] , thresholds [ i + 1 ] , _finalized [ i ] * 1.0 / ( _finalized [ i ] + _discarded [ i ] ) , _finalized [ i ] , _finalized [ i ] + _discarded [ i ] )
print ' Percentage of block heights filled: %f %% ' % ( len ( [ x for x in finalized_hashes if x ] ) * 100.0 / len ( finalized_hashes ) )
2015-07-19 19:01:20 +00:00
def run ( steps = 4000 ) :
n = networksim . NetworkSimulator ( )
for i in range ( NUM_VALIDATORS ) :
n . agents . append ( Validator ( i , n ) )
2015-09-01 13:25:33 +00:00
n . generate_peers ( 3 )
2015-08-27 13:38:48 +00:00
while len ( all_signatures ) :
all_signatures . pop ( )
2015-07-19 19:01:20 +00:00
for x in future . keys ( ) :
del future [ x ]
for x in finalized_blocks . keys ( ) :
del finalized_blocks [ x ]
for x in discarded . keys ( ) :
del discarded [ x ]
for i in range ( steps ) :
n . tick ( )
2015-08-28 10:23:03 +00:00
if i % 500 == 0 :
2015-09-01 13:25:33 +00:00
minmax = 99999999999999999
for x in n . agents :
minmax = min ( minmax , x . max_finalized_height - 10 )
print get_opinions ( n ) [ max ( minmax , 0 ) : ]
2015-08-27 13:38:48 +00:00
finalized0 = [ ( v . max_finalized_height , v . finalized_hashes ) for v in n . agents ]
2015-09-01 13:25:33 +00:00
if CHECK_INTEGRITY :
finalized = sorted ( finalized0 , key = lambda x : len ( x [ 1 ] ) )
for j in range ( len ( n . agents ) - 1 ) :
for k in range ( len ( finalized [ j ] [ 1 ] ) ) :
if finalized [ j ] [ 1 ] [ k ] is not None and finalized [ j + 1 ] [ 1 ] [ k ] is not None :
if finalized [ j ] [ 1 ] [ k ] != finalized [ j + 1 ] [ 1 ] [ k ] :
print finalized [ j ]
print finalized [ j + 1 ]
raise Exception ( " Finalization mismatch: %r %r " % ( finalized [ j ] [ 1 ] [ k ] , finalized [ j + 1 ] [ 1 ] [ k ] ) )
2015-08-27 13:38:48 +00:00
print ' Finalized status: %r ' % [ x [ 0 ] for x in finalized0 ]
2015-09-01 13:25:33 +00:00
_all = finalized0 [ 0 ] [ 1 ]
_pos = len ( [ x for x in _all if x ] )
_neg = len ( [ x for x in _all if not x ] )
print ' Finalized blocks: %r ( %r positive, %r negaitve) ' % ( len ( _all ) , _pos , _neg )
if i == 10000 and NETSPLITS > = 1 :
2015-07-19 19:01:20 +00:00
print " ########################################################### "
print " Knocking off 20 % o f the network!!!!! "
print " ########################################################### "
n . knock_offline_random ( NUM_VALIDATORS / / 5 )
2015-09-01 13:25:33 +00:00
if i == 20000 and NETSPLITS > = 2 :
2015-07-19 19:01:20 +00:00
print " ########################################################### "
print " Simluating a netsplit!!!!! "
print " ########################################################### "
n . generate_peers ( )
n . partition ( )
2015-09-01 13:25:33 +00:00
if i == 30000 and NETSPLITS > = 1 :
2015-07-19 19:01:20 +00:00
print " ########################################################### "
print " Network health back to normal! "
print " ########################################################### "
n . generate_peers ( )
2015-08-28 10:23:03 +00:00
calibrate ( n . agents [ 0 ] . finalized_hashes [ : n . agents [ 0 ] . max_finalized_height + 1 ] )