import copy, hashlib, time class Event(object): ''' Generalized event object ''' def __init__(self,params): self.data = {} self.timeOfEvent = None self.eventType = None class Block(object): ''' Block object, very simple... has an identity and a timestamp''' def __init__(self, params): self.ident = params[0] self.timestamp = params[1] class Node(object): ''' Node object, represents a computer on the network. Has an identity, a dict (?) of edges, a time offset on [-1,1] representing how inaccurate the node's wall clock appears to be, an intensity representing the node's hash rate, a blockchain which is merely a list of block objects (ordered by their arrival at the node, for simplicity), and a difficulty score (which is a function of the blockchain) ''' def __init__(self, params): self.ident = params[0] # string self.edges = params[1] # dict of edges self.timeOffset = params[2] # float self.intensity = params[3] # float (positive) self.difficulty = params[4] self.blockchain = [] def makeBlock(self, t): ts = t + self.timeOffset newBlock = Block() salt = random.random() n = len(self.blockchain) x = hash(str(n) + str(salt)) newBlock.ident = x newBlock.timestamp = ts self.blockchain.append(newBlock) self.computeDifficulty() return newBlock # node object needs receiveBlock(block) method def receiveBlock(self, blockToReceive): self.blockchain.append(blockToReceive) def computeDifficulty(self, target, sampleSize): N = min(sampleSize,len(self.blockchain)) tempSum = 0.0 for i in range(N): tempSum += abs(self.blockchain[-i].timestamp - self.blockchain[-i-1].timestamp) tempSum = float(tempSum)/float(N) # Average absolute time difference of last N blocks lambdaMLE = 1.0/tempSum self.difficulty = float(lambdaMLE/target)*self.difficulty class Edge(object): ''' Edge object representing the connection between one node and another. Has two node objects, a and b, a length l, and a dictionary of pending blocks. ''' def __init__(self, params): self.ident = params[0] # edge ident self.a = params[1] # node ident of one incident node self.b = params[2] # node ident of the other incident node (may be None when creating new blocks?) self.l = params[3] # length of edge as measured in propagation time. self.pendingBlocks = {} # blockIdent:(block, destination node ident, deterministic time of arrival) def addBlock(self, blockToAdd, blockFinderIdent, curTime): # Include new block to self.pendingBlocks timeOfArrival = curTime + self.l if blockFinderIdent == self.a.ident: self.pendingBlocks.update({blockToAdd.ident:(blockToAdd, self.b.ident, timeOfArrival)}) elif blockFinderIdent == self.b.ident: self.pendingBlocks.update({blockToAdd.ident:(blockToAdd, self.a.ident, timeOfArrival)}) else: print("fish sticks.") class Network(object): ''' Network object consisting of a number of vertices, a probability that any pair of vertices has an edge between them, a death rate of vertices, a dictionary of vertices, a dictionary of edges, and a clock t. ''' def __init__(self, params): self.numVertices = params[0] # integer self.probOfEdge = params[1] # float (between 0.0 and 1.0) self.deathRate = params[2] # float 1.0/(avg vertex lifespan) self.vertices = {} # Dict with keys=node idents and values=nodes self.edges = {} # Dict with keys=edge idents and values=edges self.t = 0.0 self.defaultNodeLength = 30.0 # milliseconds self.initialize() def initialize(self): # Generate self.numVertices new nodes with probability self.probOfEdge # that any pair are incident. Discard any disconnected nodes (unless # there is only one node) try: assert self.numVertices > 1 except AssertionError: print("Fish sticks... AGAIN! Come ON, fellas!") count = self.numVertices - 1 e = Event() e.eventType = "node birth" e.timeOfEvent = 0.0 e.data = {"neighbors":[]} self.birthNode(e) while count > 0: count -= 1 e.eventType = "node birth" e.timeOfEvent = 0.0 e.data = {"neighbors":[]} for x in self.vertices: u = random.random() if u < self.probOfEdge: e.data["neighbors"].append(x) self.birthNode(e) def run(self, maxTime, birthrate=lambda x:math.exp(-(x-10.0)**2.0)): # Run the simulation for maxTime and birthrate function (of time) while self.t < maxTime: if type(birthrate) is float: # We may pass in a constant birthrate birthrate = lambda x:birthrate # but we want it treated as a function e = self.nextEvent(birthrate) # Generate next event. try: assert e is not None except AssertionError: print("Got null event in run, bailing...") break self.t = e.timeOfEvent # Get time until next event. self.execute(e) # Run the execute method def nextEvent(self, birthrate, t): # The whole network experiences stochastic birth and death as a Poisson # process, each node experiences stochastic block discovery as a (non- # homogeneous) Poisson process. Betwixt these arrivals, other # deterministic events occur as blocks are propagated along edges, # changing the (local) block discovery rates. # Birth of node? u = random.random() u = math.ln(1.0-u)/birthrate(t) e = Event() e.eventType = "node birth" e.timeOfEvent = self.t + u e.data = {"neighbors":[]} for x in self.vertices: u = random.random() if u < self.probOfEdge: e.data["neighbors"].append(x) for x in self.vertices: u = random.random() u = math.ln(1.0 - u)/self.deathRate tempTime = self.t + u if tempTime < e.timeOfEvent: e.eventType = "node death" e.timeOfEvent = tempTime e.data = {"identToKill":x} u = random.random() localIntensity = self.vertices[x].intensity/self.vertices[x].difficulty u = math.ln(1.0 - u)/localIntensity tempTime = self.t + u if tempTime < e.timeOfEvent: e.eventType = "block found" e.timeOfEvent = tempTime e.data = {"blockFinderIdent":x} for edgeIdent in self.vertices[x].edges: for pendingBlockIdent in self.edges[edgeIdent]: timeOfBlockArrival = self.edges[edgeIdent].pendingBlocks[pendingBlockIdent][2] if timeOfBlockArrival < e.timeOfEvent: e.eventType = "block propagated" e.timeOfEvent = timeOfBlockArrival e.data = {"propEdgeIdent":edgeIdent, "pendingBlockIdent":blockIdent} return e def execute(self, e): # Take an event e as input. Depending on eventType of e, execute # the correspondsing method. if e.eventType == "node birth": self.birthNode(e) elif e.eventType == "node death": self.killNode(e) elif e.eventType == "block found": self.foundBlock(e) elif e.eventType == "block propagated": self.propBlock(e) def birthNode(self, e): # In this event, a new node is added to the network and edges are randomly decided upon. # I will probably limit the number of peers for a new node eventually: a fixed probability # of even 1% of edges per pair of nodes, in a graph with, say 1000 nodes, will see 10 peers # per node... # Also, I was kind of thinking this code should probably run with less than 50 nodes at a time, in general, # otherwise the simulation needs to be highly optimized to make for reasonable simulation times. newNodeIdent = hash(str(len(self.vertices)) + str(random.random())) # Pick a new random node ident newOffset = 2.0*random.random() - 1.0 # New time offset for the new node newIntensity = random.random() # New node hash rate newDifficulty = 1.0 # Dummy variable, will be replaced newbc = [] # This will be the union of the blockchains of all neighbors in e.data["neighbors"] count = 0 # This will be a nonce to be combined with a salt for a unique identifier newEdges = {} for neighborIdent in e.data["neighbors"]: newbc += [z for z in self.vertices[neighborIdent].blockchain if z not in newbc] newEdgeIdent = hash(str(count) + str(random.random())) count += 1 newLength = random.random()*self.defaultNodeLength otherSide = random.choice(self.vertices.keys()) newEdge = Edge([newEdgeIdent, newNodeIdent, otherSide, newLength]) newEdges.update({newEdgeIdent:newEdge}) params = [newNodeIdent, newEdges, newOffset, newIntensity, newDifficulty] newNode = Node(params) # Create new node newNode.blockchain = newbc # Store new blockchain newNode.computeDifficulty() # Compute new node's difficulty self.vertices.update({newIdent:newNode}) # Add new node to self.vertices self.edges.update(newEdges) # Add all new edges to self.edges def killNode(self, e): # Remove node and all incident edges nodeIdentToKill = e.data["identToKill"] #edgesToKill = e.data["edgesToKill"] for edgeIdentToKill in self.vertices[nodeIdentToKill].edges: del self.edges[edgeIdentToKill] del self.vertices[nodeIdentToKill] def foundBlock(self, e): # In this instance, a node found a new block. blockFinderIdent = e.data["blockFinderIdent"] # get identity of node that found the block. blockFound = self.vertices[blockFinderIdent].makeBlock() # Nodes need a makeBlock method for nextEdge in self.vertices[blockFinderIdent].edges: # propagate to edges self.edges[nextEdge].addBlock(blockFound, blockFinderIdent, self.t) # Edges need an addBlock method def propBlock(self, e): # In this instance, a block on an edge is plunked onto its destination node and then # propagated to the resulting edges. propEdgeIdent = e.data["propEdgeIdent"] # get the identity of the edge along which the block was propagating blockToPropIdent = e.data["blockIdent"] # get the identity of the block beign propagated # Get the block being propagated and the node identity of the receiver. (blockToAdd, destIdent) = self.edges[propEdgeIdent].pendingBlocks[blockToPropIdent] self.vertices[destIdent].receiveBlock(blockToAdd) # Call receiveBlock from the destination node. del self.edges[propEdgeIdent].pendingBlocks[blockToPropIdent] # Now that the block has been received for nextEdge in self.vertices[destIdent].edges: if nextEdge != propEdgeIdent: if self.edges[nextEdge].a.ident == destIdent: otherSideIdent = self.edges[nextEdge].b.ident elif self.edges[nextEdge].b.ident == destIdent: otherSideIdent = self.edges[nextEdge].a.ident else: print("awww fish sticks, fellas") if blockToAdd.ident not in self.vertices[otherSideIdent].blockchain: self.edges[nextEdge].addBlock(blockToAdd, destIdent, self.t) class Simulator(object): def __init__(self, params): #todo lol cryptonote style pass def go(self): nelly = Network([43, 0.25, 1.0/150.0]) self.run(maxTime, birthrate=lambda x:math.exp((-(x-500.0)**2.0))/(2.0*150.0))