research-lab/source-code/Simulator/Simulator.py
Brandon Goodell 939b597edb
Poisson process on graph for blockchain sims
COMPLETELY untested code. Does the following:

Create a random graph with a specified number of vertices. Vertices are nodes on a cryptocurrency network, edges are their p2p connections. Node additions occur in a nonhomogeneous Poisson process with a (piecewise) constant intensity function. Node deletions occur in a homogeneous Poisson process. Block discovery occurs at each node in a nonhomogeneous Poisson process with a (piecewise) constant intensity function. Blocks, upon discovery, are propagated to neighbors along edges with a random (unique for each edge) delay. Blocks arrive at the end of edges in a deterministic way.

Currently, I'm measuring difficulty in the most simple way: use the MLE estimate for a homogeneous Poisson process's intensity function. 

TODO:
1) Plot block arrivals, true network hash rate, and the difficulty score of some node (or maybe the average difficulty score) over time.
2) Gather statistics on the above.
3) Different difficulty metrics
4) Different birthrate functions
5) Oh, I should compile this at some point? I bet it doesn't even work yet.
2018-01-15 22:21:35 +01:00

275 lines
12 KiB
Python

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))