Merge pull request #4 from karlfloersch/feat/sim_validator_rotation
Add dynamic validator set logic
This commit is contained in:
commit
25085b74ee
|
@ -1,9 +1,12 @@
|
||||||
# Implements Minimal Slashing Conditions, description here:
|
# Implements Minimal Slashing Conditions and dynamic validator sets, descriptions here:
|
||||||
# https://docs.google.com/document/d/1ecFPYhe7YsKNQUAx48S8hoyK9Y4Rbe9be_lCe_vj2ek
|
# Slashing Conditions: https://docs.google.com/document/d/1ecFPYhe7YsKNQUAx48S8hoyK9Y4Rbe9be_lCe_vj2ek
|
||||||
|
# Dynamic Validator Sets: https://medium.com/@VitalikButerin/safety-under-dynamic-validator-sets-ef0c3bbdf9f6#.igylifcm9
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
NODE_COUNT = 10
|
POOL_SIZE = 10
|
||||||
|
VALIDATOR_IDS = range(0, POOL_SIZE*2)
|
||||||
|
INITIAL_VALIDATORS = range(0, POOL_SIZE)
|
||||||
BLOCK_TIME = 100
|
BLOCK_TIME = 100
|
||||||
EPOCH_LENGTH = 5
|
EPOCH_LENGTH = 5
|
||||||
AVG_LATENCY = 255
|
AVG_LATENCY = 255
|
||||||
|
@ -35,31 +38,64 @@ class Network():
|
||||||
self.time += 1
|
self.time += 1
|
||||||
|
|
||||||
class Block():
|
class Block():
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None, finalized_dynasties=None):
|
||||||
|
self.hash = random.randrange(10**30)
|
||||||
|
# If we are genesis block, set initial values
|
||||||
if not parent:
|
if not parent:
|
||||||
self.number = 0
|
self.number = 0
|
||||||
self.prevhash = 0
|
self.prevhash = 0
|
||||||
else:
|
self.prev_dynasty = self.current_dynasty = Dynasty(INITIAL_VALIDATORS)
|
||||||
self.number = parent.number + 1
|
self.next_dynasty = self.generate_next_dynasty(self.current_dynasty.number)
|
||||||
self.prevhash = parent.hash
|
return
|
||||||
self.hash = random.randrange(10**30)
|
# Set our block number and our prevhash
|
||||||
|
self.number = parent.number + 1
|
||||||
|
self.prevhash = parent.hash
|
||||||
|
# Generate a random next dynasty
|
||||||
|
self.next_dynasty = self.generate_next_dynasty(parent.current_dynasty.number)
|
||||||
|
# If the current_dynasty was finalized, we advance to the next dynasty
|
||||||
|
if parent.current_dynasty in finalized_dynasties:
|
||||||
|
self.prev_dynasty = parent.current_dynasty
|
||||||
|
self.current_dynasty = parent.next_dynasty
|
||||||
|
return
|
||||||
|
# `current_dynasty` has not yet been finalized so we don't rotate validators
|
||||||
|
self.prev_dynasty = parent.prev_dynasty
|
||||||
|
self.current_dynasty = parent.current_dynasty
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def epoch(self):
|
def epoch(self):
|
||||||
return self.number // EPOCH_LENGTH
|
return self.number // EPOCH_LENGTH
|
||||||
|
|
||||||
|
def generate_next_dynasty(self, prev_dynasty_number):
|
||||||
|
random.seed(self.hash)
|
||||||
|
next_dynasty = Dynasty(random.sample(VALIDATOR_IDS, POOL_SIZE), prev_dynasty_number+1)
|
||||||
|
random.seed()
|
||||||
|
return next_dynasty
|
||||||
|
|
||||||
class Prepare():
|
class Prepare():
|
||||||
def __init__(self, view, _hash, view_source):
|
def __init__(self, view, _hash, view_source, sender):
|
||||||
self.view = view
|
self.view = view
|
||||||
self.hash = random.randrange(10**30)
|
self.hash = random.randrange(10**30)
|
||||||
self.blockhash = _hash
|
self.blockhash = _hash
|
||||||
self.view_source = view_source
|
self.view_source = view_source
|
||||||
|
self.sender = sender
|
||||||
|
|
||||||
class Commit():
|
class Commit():
|
||||||
def __init__(self, view, _hash):
|
def __init__(self, view, _hash, sender):
|
||||||
self.view = view
|
self.view = view
|
||||||
self.hash = random.randrange(10**30)
|
self.hash = random.randrange(10**30)
|
||||||
self.blockhash = _hash
|
self.blockhash = _hash
|
||||||
|
self.sender = sender
|
||||||
|
|
||||||
|
class Dynasty():
|
||||||
|
def __init__(self, validators, number=0):
|
||||||
|
self.validators = validators
|
||||||
|
self.number = number
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(str(self.number) + str(self.validators))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (str(self.number) + str(self.validators)) == (str(other.number) + str(other.validators))
|
||||||
|
|
||||||
GENESIS = Block()
|
GENESIS = Block()
|
||||||
|
|
||||||
|
@ -83,7 +119,10 @@ class Node():
|
||||||
self.committable = {}
|
self.committable = {}
|
||||||
# Commits for any given checkpoint
|
# Commits for any given checkpoint
|
||||||
# Genesis is an immutable start of the chain
|
# Genesis is an immutable start of the chain
|
||||||
self.commits = {GENESIS.hash: 101}
|
self.commits = {GENESIS.hash: INITIAL_VALIDATORS}
|
||||||
|
# Set of finalized dynasties
|
||||||
|
self.finalized_dynasties = set()
|
||||||
|
self.finalized_dynasties.add(Dynasty(INITIAL_VALIDATORS))
|
||||||
# My current epoch
|
# My current epoch
|
||||||
self.current_epoch = 0
|
self.current_epoch = 0
|
||||||
# My highest committed epoch and hash
|
# My highest committed epoch and hash
|
||||||
|
@ -178,13 +217,17 @@ class Node():
|
||||||
if self.is_ancestor(self.highest_committed_hash, last_committed_checkpoint):
|
if self.is_ancestor(self.highest_committed_hash, last_committed_checkpoint):
|
||||||
print('Preparing %d for epoch %d with view source %d' %
|
print('Preparing %d for epoch %d with view source %d' %
|
||||||
(target_block.hash, target_block.epoch, self.received[last_committed_checkpoint].epoch))
|
(target_block.hash, target_block.epoch, self.received[last_committed_checkpoint].epoch))
|
||||||
self.network.broadcast(Prepare(target_block.epoch, target_block.hash, self.received[last_committed_checkpoint].epoch))
|
self.network.broadcast(Prepare(target_block.epoch, target_block.hash, self.received[last_committed_checkpoint].epoch, self.id))
|
||||||
assert self.received[target_block.hash]
|
assert self.received[target_block.hash]
|
||||||
|
|
||||||
# Pick a checkpoint by number of commits first, epoch number
|
# Pick a checkpoint by number of commits first, epoch number
|
||||||
# (ie. longest chain rule) second
|
# (ie. longest chain rule) second
|
||||||
def score_checkpoint(self, block):
|
def score_checkpoint(self, block):
|
||||||
return self.commits.get(block.hash, 0) + 0.000000001 * self.tails[block.hash].number
|
# Choose the dynasty (current or previous) with the minimum number of commits
|
||||||
|
current_dynasty_number_of_commits = len(list(set(block.current_dynasty.validators) & set(self.commits.get(block.hash, []))))
|
||||||
|
prev_dynasty_number_of_commits = len(list(set(block.prev_dynasty.validators) & set(self.commits.get(block.hash, []))))
|
||||||
|
number_of_commits = min(current_dynasty_number_of_commits, prev_dynasty_number_of_commits)
|
||||||
|
return number_of_commits + 0.000000001 * self.tails[block.hash].number
|
||||||
|
|
||||||
# See if a given epoch block requires us to reorganize our checkpoint list
|
# See if a given epoch block requires us to reorganize our checkpoint list
|
||||||
def check_checkpoints(self, block):
|
def check_checkpoints(self, block):
|
||||||
|
@ -234,17 +277,22 @@ class Node():
|
||||||
if prepare.blockhash not in self.received:
|
if prepare.blockhash not in self.received:
|
||||||
self.add_dependency(prepare.blockhash, prepare)
|
self.add_dependency(prepare.blockhash, prepare)
|
||||||
return False
|
return False
|
||||||
|
# If the sender is not in the prepare's dynasty, ignore the prepare
|
||||||
|
if prepare.sender not in self.received[prepare.blockhash].current_dynasty.validators and \
|
||||||
|
prepare.sender not in self.received[prepare.blockhash].prev_dynasty.validators:
|
||||||
|
return False
|
||||||
# Add to the prepare count
|
# Add to the prepare count
|
||||||
if prepare.blockhash not in self.prepare_count:
|
if prepare.blockhash not in self.prepare_count:
|
||||||
self.prepare_count[prepare.blockhash] = {}
|
self.prepare_count[prepare.blockhash] = {}
|
||||||
self.prepare_count[prepare.blockhash][prepare.view_source] = self.prepare_count[prepare.blockhash].get(prepare.view_source, 0) + 1
|
self.prepare_count[prepare.blockhash][prepare.view_source] = self.prepare_count[prepare.blockhash].get(prepare.view_source, 0) + 1
|
||||||
# If there are enough prepares...
|
# If there are enough prepares and the previous dynasty is finalized...
|
||||||
if self.prepare_count[prepare.blockhash][prepare.view_source] > (NODE_COUNT * 2) // 3 and \
|
if self.prepare_count[prepare.blockhash][prepare.view_source] > (POOL_SIZE * 2) // 3 and \
|
||||||
|
self.received[prepare.blockhash].prev_dynasty in self.finalized_dynasties and \
|
||||||
prepare.blockhash not in self.committable:
|
prepare.blockhash not in self.committable:
|
||||||
# Mark it as committable
|
# Mark it as committable
|
||||||
self.committable[prepare.blockhash] = True
|
self.committable[prepare.blockhash] = True
|
||||||
# Start counting commits
|
# Start counting commits
|
||||||
self.commits[prepare.blockhash] = 0
|
self.commits[prepare.blockhash] = []
|
||||||
# If there are dependencies (ie. commits that arrived before there
|
# If there are dependencies (ie. commits that arrived before there
|
||||||
# were enough prepares), since there are now enough prepares we
|
# were enough prepares), since there are now enough prepares we
|
||||||
# can process them
|
# can process them
|
||||||
|
@ -254,7 +302,7 @@ class Node():
|
||||||
del self.dependencies["commit:"+str(prepare.blockhash)]
|
del self.dependencies["commit:"+str(prepare.blockhash)]
|
||||||
# Broadcast a commit
|
# Broadcast a commit
|
||||||
if self.current_epoch == prepare.view:
|
if self.current_epoch == prepare.view:
|
||||||
self.network.broadcast(Commit(prepare.view, prepare.blockhash))
|
self.network.broadcast(Commit(prepare.view, prepare.blockhash, self.id))
|
||||||
print('Committing %d for epoch %d' % (prepare.blockhash, prepare.view))
|
print('Committing %d for epoch %d' % (prepare.blockhash, prepare.view))
|
||||||
self.highest_committed_epoch = prepare.view
|
self.highest_committed_epoch = prepare.view
|
||||||
self.highest_committed_hash = prepare.blockhash
|
self.highest_committed_hash = prepare.blockhash
|
||||||
|
@ -269,12 +317,26 @@ class Node():
|
||||||
if commit.blockhash not in self.received:
|
if commit.blockhash not in self.received:
|
||||||
self.add_dependency(commit.blockhash, commit)
|
self.add_dependency(commit.blockhash, commit)
|
||||||
return False
|
return False
|
||||||
|
# If the sender is not in the commit's dynasty, ignore the commit
|
||||||
|
if commit.sender not in self.received[commit.blockhash].current_dynasty.validators and \
|
||||||
|
commit.sender not in self.received[commit.blockhash].prev_dynasty.validators:
|
||||||
|
return False
|
||||||
# If there have not yet been enough prepares, wait
|
# If there have not yet been enough prepares, wait
|
||||||
if commit.blockhash not in self.committable:
|
if commit.blockhash not in self.committable:
|
||||||
self.add_dependency("commit:"+str(commit.blockhash), commit)
|
self.add_dependency("commit:"+str(commit.blockhash), commit)
|
||||||
return False
|
return False
|
||||||
# Add commits, and update checkpoints if needed
|
# Add the commit by recording the sender
|
||||||
self.commits[commit.blockhash] += 1
|
self.commits[commit.blockhash].append(commit.sender)
|
||||||
|
# Check if the block is finalized
|
||||||
|
current_dynasty_commits = list(set(self.received[commit.blockhash].current_dynasty.validators) & set(self.commits[commit.blockhash]))
|
||||||
|
prev_dynasty_commits = list(set(self.received[commit.blockhash].prev_dynasty.validators) & set(self.commits[commit.blockhash]))
|
||||||
|
if len(current_dynasty_commits) > (POOL_SIZE * 2) // 3 and len(prev_dynasty_commits) > (POOL_SIZE * 2) // 3:
|
||||||
|
# Because the block has been finalized let's record its dynasty as finalized
|
||||||
|
finalized_dynasty = self.received[commit.blockhash].current_dynasty
|
||||||
|
self.finalized_dynasties.add(finalized_dynasty)
|
||||||
|
print('Finalizing dynasty number %d for block number %d' %
|
||||||
|
(finalized_dynasty.number, self.received[commit.blockhash].number))
|
||||||
|
# Update the checkpoints if needed
|
||||||
self.check_checkpoints(self.received[commit.blockhash])
|
self.check_checkpoints(self.received[commit.blockhash])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -299,16 +361,18 @@ class Node():
|
||||||
|
|
||||||
# Called every round
|
# Called every round
|
||||||
def tick(self, _time):
|
def tick(self, _time):
|
||||||
if self.id == (_time // BLOCK_TIME) % NODE_COUNT and _time % BLOCK_TIME == 0:
|
if self.id == (_time // BLOCK_TIME) % POOL_SIZE and _time % BLOCK_TIME == 0:
|
||||||
new_block = Block(self.head)
|
new_block = Block(self.head, self.finalized_dynasties)
|
||||||
self.network.broadcast(new_block)
|
self.network.broadcast(new_block)
|
||||||
self.on_receive(new_block)
|
self.on_receive(new_block)
|
||||||
|
|
||||||
network = Network(poisson_latency(AVG_LATENCY))
|
network = Network(poisson_latency(AVG_LATENCY))
|
||||||
nodes = [Node(network, i) for i in range(NODE_COUNT)]
|
nodes = [Node(network, i) for i in VALIDATOR_IDS]
|
||||||
for t in range(25000):
|
for t in range(25000):
|
||||||
network.tick()
|
network.tick()
|
||||||
if t % 1000 == 999:
|
if t % 1000 == 999:
|
||||||
print('Heads:', [n.head.number for n in nodes])
|
print('Heads:', [n.head.number for n in nodes])
|
||||||
print('Checkpoints:', nodes[0].checkpoints)
|
print('Checkpoints:', nodes[0].checkpoints)
|
||||||
print('Commits:', [nodes[0].commits.get(c, 0) for c in nodes[0].checkpoints])
|
print('Commits:', [nodes[0].commits.get(c, 0) for c in nodes[0].checkpoints])
|
||||||
|
print('Blocks Dynasties:', [(nodes[0].received[c].current_dynasty.number) for c in nodes[0].checkpoints])
|
||||||
|
print('All Node Dynasties:', [(node.tails[node.checkpoints[-1]].current_dynasty.number) for node in nodes])
|
||||||
|
|
Loading…
Reference in New Issue