mirror of
https://github.com/monero-project/research-lab.git
synced 2024-12-23 12:09:58 +00:00
939b597edb
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.
275 lines
12 KiB
Python
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))
|