# NOTE: This script is totally untested and *absolutely* has bugs. # Be warned! # Implements Minimal Slashing Conditions and dynamic validator rotation, description here: # https://docs.google.com/document/d/1ecFPYhe7YsKNQUAx48S8hoyK9Y4Rbe9be_lCe_vj2ek import random POOL_SIZE = 10 VALIDATOR_IDS = range(0, POOL_SIZE*2) BLOCK_TIME = 100 EPOCH_LENGTH = 5 INITIAL_VALIDATORS = range(0, POOL_SIZE) AVG_LATENCY = 250 def poisson_latency(latency): return lambda: 1 + int(random.gammavariate(1, 1) * latency) class Network(): def __init__(self, latency): self.nodes = [] self.latency = latency self.time = 0 self.msg_arrivals = {} def broadcast(self, msg): for i, n in enumerate(self.nodes): delay = self.latency() if self.time + delay not in self.msg_arrivals: self.msg_arrivals[self.time + delay] = [] self.msg_arrivals[self.time + delay].append((i, msg)) def tick(self): if self.time in self.msg_arrivals: for node_index, msg in self.msg_arrivals[self.time]: self.nodes[node_index].on_receive(msg) del self.msg_arrivals[self.time] for n in self.nodes: n.tick(self.time) self.time += 1 class Block(): 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: self.number = 0 self.prevhash = 0 self.prev_dynasty = self.current_dynasty = Dynasty(INITIAL_VALIDATORS) self.next_dynasty = self.generate_next_dynasty(self.current_dynasty.number) return # Set our block number and our prevhash self.number = parent.number + 1 self.prevhash = parent.hash # If the current_dynasty was finalized, generate a new dynasty if parent.current_dynasty in finalized_dynasties: self.prev_dynasty = parent.current_dynasty self.current_dynasty = parent.next_dynasty self.next_dynasty = self.generate_next_dynasty(parent.current_dynasty.number) 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 self.next_dynasty = parent.next_dynasty @property def epoch(self): 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(): def __init__(self, view, _hash, view_source, sender): self.view = view self.hash = random.randrange(10**30) self.blockhash = _hash self.view_source = view_source self.sender = sender class Commit(): def __init__(self, view, _hash, sender): self.view = view self.hash = random.randrange(10**30) 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() # Fork choice rule: # 1. HEAD = genesis # 2. Find the descendant with the highest number of commits # 3. Repeat 2 until 0 commits # 4. Longest chain rule class Node(): def __init__(self, network, id): # List of highest-commit descendants along with their commit counts, in oldest-to-newest order self.checkpoints = [GENESIS.hash] # Received blocks self.received = {GENESIS.hash: GENESIS} # Messages that will be processed once a given message is received self.dependencies = {} # Checkpoint to view source to prepare count self.prepare_count = {} # Checkpoints that can be committed self.committable = {} # Commits for any given checkpoint # Genesis is an immutable start of the chain self.commits = {GENESIS.hash: INITIAL_VALIDATORS} # Set of finalized dynasties self.finalized_dynasties = set() self.finalized_dynasties.add(Dynasty(INITIAL_VALIDATORS)) # My current epoch self.current_epoch = 0 # My highest committed epoch and hash self.highest_committed_epoch = -1 self.highest_committed_hash = GENESIS.hash # Network I am connected to self.network = network network.nodes.append(self) # Longest tail from each checkpoint self.tails = {GENESIS.hash: GENESIS} # Tail that each block belongs to self.tail_membership = {GENESIS.hash: GENESIS.hash} # This node's ID self.id = id @property def head(self): latest_checkpoint = self.checkpoints[-1] latest_block = self.tails[latest_checkpoint] return latest_block # Get the checkpoint immediately before a given checkpoint def get_checkpoint_parent(self, block): if block.number == 0: return None return self.received[self.tail_membership[block.prevhash]] # If we received an object but did not receive some dependencies # needed to process it, save it to be processed later def add_dependency(self, _hash, obj): if _hash not in self.dependencies: self.dependencies[_hash] = [] self.dependencies[_hash].append(obj) # Is a given checkpoint an ancestor of another given checkpoint? def is_ancestor(self, anc, desc): if not isinstance(anc, Block): anc = self.received[anc] if not isinstance(desc, Block): desc = self.received[desc] assert anc.number % EPOCH_LENGTH == 0 assert desc.number % EPOCH_LENGTH == 0 while True: if desc is None: return False if desc.hash == anc.hash: return True desc = self.get_checkpoint_parent(desc) def get_last_committed_checkpoint(self): z = len(self.checkpoints) - 1 while self.score_checkpoint(self.received[self.checkpoints[z]]) < 1: z -= 1 return self.checkpoints[z] # Called on receiving a block def accept_block(self, block): # If we didn't receive the block's parent yet, wait if block.prevhash not in self.received: self.add_dependency(block.prevhash, block) return False # We recived the block self.received[block.hash] = block # print(self.id, 'got a block', block.number, block.hash) # If it's an epoch block (in general) if block.number % EPOCH_LENGTH == 0: # Start a tail object for it self.tail_membership[block.hash] = block.hash self.tails[block.hash] = block # Otherwise... else: # See if it's part of the longest tail, if so set the tail accordingly assert block.prevhash in self.received assert block.prevhash in self.tail_membership self.tail_membership[block.hash] = self.tail_membership[block.prevhash] if block.number > self.tails[self.tail_membership[block.hash]].number: self.tails[self.tail_membership[block.hash]] = block self.check_checkpoints(self.received[self.tail_membership[block.hash]]) self.maybe_prepare_last_checkpoint() return True def maybe_prepare_last_checkpoint(self): target_block = self.received[self.checkpoints[-1]] # If the block is an epoch block of a higher epoch than what we've seen so far if target_block.epoch > self.current_epoch: print('now in epoch %d' % target_block.epoch) # Increment our epoch self.current_epoch = target_block.epoch # If our highest committed hash is in the main chain (in most cases # it should be), then send a prepare last_committed_checkpoint = self.get_last_committed_checkpoint() if self.is_ancestor(self.highest_committed_hash, last_committed_checkpoint): print('Preparing %d for epoch %d with view source %d' % (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.id)) assert self.received[target_block.hash] # Pick a checkpoint by number of commits first, epoch number # (ie. longest chain rule) second def score_checkpoint(self, block): # Only count current_dynasty commits for the checkpoint score number_of_commits = len(list(set(block.current_dynasty.validators) & set(self.commits.get(block.hash, [])))) return number_of_commits + 0.000000001 * self.tails[block.hash].number # See if a given epoch block requires us to reorganize our checkpoint list def check_checkpoints(self, block): # Is this hash already in our main chain? Then do nothing if block.hash in self.checkpoints: # prev_checkpoint = self.received[self.checkpoints[self.checkpoints.index(block.hash) - 1]] # if score_checkpoint(block) < score_checkpoint(prev_checkpoint): return # Figure out how many of our checkpoints we need to revert z = len(self.checkpoints) - 1 new_score = self.score_checkpoint(block) while new_score > self.score_checkpoint(self.received[self.checkpoints[z]]): z -= 1 # If none, do nothing if z == len(self.checkpoints) - 1 and block.number <= self.received[self.checkpoints[z-1]].number: return # Delete the checkpoints that need to be superseded self.checkpoints = self.checkpoints[:z + 1] # Re-run the fork choice rule while 1: # Find the descendant with the highest score (commits first, epoch second) max_score = 0 max_descendant = None for _hash in self.tails: if self.is_ancestor(self.checkpoints[-1], _hash) and _hash != self.checkpoints[-1]: new_score = self.score_checkpoint(self.received[_hash]) if new_score > max_score: max_score = new_score max_descendant = _hash # Append to the chain that checkpoint, and all checkpoints between the # last checkpoint and the new one if max_descendant: new_chain = [max_descendant] while new_chain[0] != self.checkpoints[-1]: new_chain.insert(0, self.get_checkpoint_parent(self.received[new_chain[0]]).hash) self.checkpoints.extend(new_chain[1:]) # If there were no suitable descendants found, break else: break print('New checkpoints: %r' % [self.received[b].epoch for b in self.checkpoints]) # Called on receiving a prepare message def accept_prepare(self, prepare): if self.id == 0: print('got a prepare', prepare.view, prepare.view_source, prepare.blockhash, prepare.blockhash in self.received) # If the block has not yet been received, wait if prepare.blockhash not in self.received: self.add_dependency(prepare.blockhash, prepare) return False # If the sender is not in it's corresponding 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 if prepare.blockhash not in self.prepare_count: self.prepare_count[prepare.blockhash] = {} self.prepare_count[prepare.blockhash][prepare.view_source] = self.prepare_count[prepare.blockhash].get(prepare.view_source, 0) + 1 # If there are enough prepares and the previous dynasty is finalized... 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: # Mark it as committable self.committable[prepare.blockhash] = True # Start counting commits self.commits[prepare.blockhash] = [] # If there are dependencies (ie. commits that arrived before there # were enough prepares), since there are now enough prepares we # can process them if "commit:"+str(prepare.blockhash) in self.dependencies: for c in self.dependencies["commit:"+str(prepare.blockhash)]: self.accept_commit(c) del self.dependencies["commit:"+str(prepare.blockhash)] # Broadcast a commit if self.current_epoch == prepare.view: self.network.broadcast(Commit(prepare.view, prepare.blockhash, self.id)) print('Committing %d for epoch %d' % (prepare.blockhash, prepare.view)) self.highest_committed_epoch = prepare.view self.highest_committed_hash = prepare.blockhash self.current_epoch = prepare.view + 0.5 return True # Called on receiving a commit message def accept_commit(self, commit): if self.id == 0: print('got a commmit', commit.view, commit.blockhash, commit.blockhash in self.received, commit.blockhash in self.committable) # If the block has not yet been received, wait if commit.blockhash not in self.received: self.add_dependency(commit.blockhash, commit) return False # If the sender is not in it's corresponding 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 commit.blockhash not in self.committable: self.add_dependency("commit:"+str(commit.blockhash), commit) return False # Add the commit by recording the sender 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]) return True # Called on receiving any object def on_receive(self, obj): if obj.hash in self.received: return False if isinstance(obj, Block): o = self.accept_block(obj) elif isinstance(obj, Prepare): o = self.accept_prepare(obj) elif isinstance(obj, Commit): o = self.accept_commit(obj) # If the object was successfully processed # (ie. not flagged as having unsatisfied dependencies) if o: self.received[obj.hash] = obj if obj.hash in self.dependencies: for d in self.dependencies[obj.hash]: self.on_receive(d) del self.dependencies[obj.hash] # Called every round def tick(self, _time): if self.id == (_time // BLOCK_TIME) % POOL_SIZE and _time % BLOCK_TIME == 0: new_block = Block(self.head, self.finalized_dynasties) self.network.broadcast(new_block) self.on_receive(new_block) network = Network(poisson_latency(AVG_LATENCY)) nodes = [Node(network, i) for i in VALIDATOR_IDS] for t in range(25000): network.tick() if t % 1000 == 999: print('Heads:', [n.head.number for n in nodes]) print('Checkpoints:', nodes[0].checkpoints) print('Commits:', [nodes[0].commits.get(c, 0) for c in nodes[0].checkpoints]) print('Finalized Dynasties:', nodes[0].finalized_dynasties) print('Current Dynasties:', [(i, node.tails[node.checkpoints[-1]].current_dynasty.number) for i, node in enumerate(nodes)])