From 4593f2b8ea951501a72fd5f5dbe864791ac12e54 Mon Sep 17 00:00:00 2001 From: Brandon Goodell Date: Sun, 28 Jan 2018 12:16:13 -0700 Subject: [PATCH] Failing loudly and often. --- source-code/Poisson-Graphs/FishGraph.py | 746 ++++++++++++++++++++---- 1 file changed, 639 insertions(+), 107 deletions(-) diff --git a/source-code/Poisson-Graphs/FishGraph.py b/source-code/Poisson-Graphs/FishGraph.py index 2e15b49..0d1e4f5 100644 --- a/source-code/Poisson-Graphs/FishGraph.py +++ b/source-code/Poisson-Graphs/FishGraph.py @@ -1,98 +1,537 @@ -import unittest, copy, random, math +import unittest, copy, random, math, time +from scipy.stats import skew +from numpy import var +from numpy import random as nprandom -class stochasticProcess(object): +#TODO: Node.data["blockchain"] != node.data + +def newIdent(params): + nonce = params + # Generate new random identity. + return hash(str(nonce) + str(random.random())) + +def newIntensity(params): + mode = params + if mode=="uniform": + return random.random() + +def newOffset(params): + mode = params + if mode=="unifDST": + r = 2.0*random.random() - 1.0 # hours + r = 60.0*60.0*r #60 min/hr, 60 sec/min + return r + if mode=="sumOfSkellams": + # This mode uses a skellam distribution, which is + # the difference of two poisson-distributed random + # variables. + # HourOffset = skellam + # SecondOffset = skellam + # TotalOffset = 60*60*HourOffset + 60*MinuteOffset + SecondOffset + # Each skellam = poisson(1) - poisson(1) + # Reasoning: We consider most computers' local time offset from UTC + # to be a two time-scale random variable, one on the hour scale and one on + # the second scale. We make + x = nprandom.poisson(1, (2,2)) + totalOffset = 60.0*60.0*float(x[0][0] - x[1][0]) + float((x[0][1] - x[1][1])) + return totalOffset + +class StochasticProcess(object): + ''' + Stochastic processes have a clock and a state. + The clock moves forward, and then the state updates. + More detail requires knowledge of the underlying stochProc. + ''' def __init__(self, params=None): + # initialize with initial data self.data = params - self.t = 0.0 - self.state = 0.0 - self.maxTime = 1000.0 + self.t = 0.0 # should always start at t=0.0 + self.state = 0.0 # magic number + self.maxTime = 1000.0 # magic number + self.verbose = True def go(self): - assert self.maxTime > 0.0 - while self.t <= self.maxTime: - deltaT = self.getNextTime() - self.updateState(deltaT) - #print(str(self.t) + ", " + str(self.state)) + # Executes stochastic process. + assert self.maxTime > 0.0 # Check loop will eventually terminate. + t = self.t + while t <= self.maxTime: + deltaT = self.getNextTime() # Pick the next "time until event" and a description of the event. + self.updateState(t, deltaT) # Update state with deltaT input + t = self.t + if self.verbose: + print("Recording...") def getNextTime(self): - return 1 + return 1 # Magic number right now - def updateState(self, deltaT): + def updateState(self, t, deltaT): + # Update the state of the system. In this case, + # we are doing a random walk on the integers. self.state += random.randrange(-1,2,1) # [-1, 0, 1] self.t += deltaT - -class Test_stochasticProcess(unittest.TestCase): +class Test_StochasticProcess(unittest.TestCase): def test_sp(self): - sally = stochasticProcess() + sally = StochasticProcess() sally.go() -suite = unittest.TestLoader().loadTestsFromTestCase(Test_stochasticProcess) -unittest.TextTestRunner(verbosity=1).run(suite) +#suite = unittest.TestLoader().loadTestsFromTestCase(Test_StochasticProcess) +#unittest.TextTestRunner(verbosity=1).run(suite) -class Node(object): - def __init__(self, params=["", {}]): - self.ident = params[0] - self.data = params[1] - self.edges = {} +class Block(object): + ''' + Each block has: an identity, a timestamp of discovery (possibly false), + has a timestamp of arrival at the local node (possibly unnecessary), a pointer to a parent + block's identity, and a difficulty score. + ''' + def __init__(self, params=[]): + try: + assert len(params)==6 + except AssertionError: + print("Error in Block(): Tried to add a malformed block. We received params = " + str(params) + ", but should have had something of the form [ident, disco, arriv, parent, diff, verbose].") + else: + self.ident = params[0] + self.discoTimestamp = params[1] + self.arrivTimestamp = params[2] + self.parent = params[3] + self.diff = params[4] + self.verbose = params[5] +class Test_Block(unittest.TestCase): + def test_b(self): + #bill = Block() + name = newIdent(0) + t = time.time() + diff = 1.0 + params = [name, t, t+1, None, diff, False] + bill = Block(params) + self.assertEqual(bill.ident,name) + self.assertEqual(bill.discoTimestamp,t) + self.assertEqual(bill.arrivTimestamp,t+1) + self.assertTrue(bill.parent is None) + self.assertEqual(bill.diff,diff) + +suite = unittest.TestLoader().loadTestsFromTestCase(Test_Block) +unittest.TextTestRunner(verbosity=1).run(suite) + +class Blockchain(object): + ''' + Not a true blockchain, of course, but tracks block objects (timestamps) as above. + Each node should be responsible for finding the chain with most cumulative work. + Right now we assume Nakamoto consensus (konsensnakamoto). + ''' + def __init__(self, params=[], verbosity=True): + self.blocks = {} + self.leaves = {} + self.miningIdent = None + self.verbose = verbosity + + def addBlock(self, blockToAdd): + # In our model we assume difficulty scores of blocks are correct (otherwise they would + # be rejected in the real life network, and we aren't trying to model spam attacks). + try: + assert blockToAdd.ident not in self.blocks + except AssertionError: + print("Error, tried to add block that already exists in blockchain.") + else: + self.blocks.update({blockToAdd.ident:blockToAdd}) + self.leaves.update({blockToAdd.ident:blockToAdd}) + if blockToAdd.parent in self.leaves: + del self.leaves[blockToAdd.parent] + self.whichLeaf() + + def whichLeaf(self): + # Determine which leaf shall be the parent leaf. + if len(self.leaves) == 1: + # If the chain has never forked, we have no decision to make: + for ident in self.leaves: + self.miningIdent = ident + elif len(self.leaves) > 1: + # If the chain has forked *ever* this will not be the case. + maxCumDiff = 0.0 + for ident in self.leaves: + tempCumDiff = 0.0 + tempCumDiff += self.blocks[ident].diff + nextIdent = self.blocks[ident].parent + if nextIdent is not None and nextIdent in self.blocks: + while self.blocks[nextIdent].parent is not None: + tempCumDiff += self.blocks[nextIdent].diff + nextIdent = self.blocks[nextIdent].parent + if tempCumDiff > maxCumDiff: + # If more than one leaf ties for maxCumDiff, each node in the + # network should pick one of these two arbitrarily. Since we + # are storing each blockchain in a hash table (unordered!), for + # each node in the network that observes a tie, each possible leaf + # is equally likely to have been the first one found! So + # we don't need to do anything for the node to select which chain + # to work off of. + self.miningIdent = ident + else: + print("Error, tried to assess an empty blockchain.") + +class Test_Blockchain(unittest.TestCase): + def test_bc(self): + bill = Blockchain([], verbosity=True) + + name = newIdent(0) + t = time.time() + diff = 1.0 + params = [name, t, t+1, None, diff, bill.verbose] + genesis = Block(params) + + self.assertEqual(genesis.ident,name) + self.assertEqual(genesis.discoTimestamp,t) + self.assertEqual(genesis.arrivTimestamp,t+1) + self.assertTrue(genesis.parent is None) + self.assertEqual(genesis.diff,diff) + + bill.addBlock(genesis) + + self.assertTrue(genesis.ident in bill.blocks) + self.assertTrue(genesis.ident in bill.leaves) + self.assertEqual(genesis.ident, bill.miningIdent) + + name = newIdent(1) + t = time.time() + diff = 2.0 + params = [name, t, t+1, genesis.ident, diff, bill.verbose] + blockA = Block(params) + bill.addBlock(blockA) + + self.assertTrue(blockA.ident in bill.blocks) + self.assertTrue(blockA.ident in bill.leaves) + self.assertFalse(genesis.ident in bill.leaves) + self.assertTrue(genesis.ident in bill.blocks) + self.assertEqual(blockA.ident, bill.miningIdent) + + name = newIdent(1) + t = time.time() + diff = 2.5 + params = [name, t, t+1, None, diff, bill.verbose] + blockB = Block(params) + bill.addBlock(blockB) + + self.assertTrue(blockB.ident in bill.blocks) + self.assertTrue(blockB.ident in bill.leaves) + self.assertFalse(genesis.ident in bill.leaves) + self.assertTrue(genesis.ident in bill.blocks) + self.assertTrue(blockA.ident in bill.leaves) + self.assertTrue(blockB.ident in bill.leaves) + self.assertEqual(blockB.ident, bill.miningIdent) + +suite = unittest.TestLoader().loadTestsFromTestCase(Test_Blockchain) +unittest.TextTestRunner(verbosity=1).run(suite) + +class Node(object): + ''' + Node object. params [identity, blockchain (data), verbosity, difficulty] + ''' + def __init__(self, params=["", {}, True]): + try: + assert len(params)==4 + except AssertionError: + print("Error, Tried to create malformed node.") + else: + self.ident = params[0] + self.data = params[1] #Blockchain object + self.verbose = params[2] + self.diff = params[3] + self.edges = {} + + def updateBlockchain(self, incBlocks, diffUpdateRate=1, mode="Nakamoto", targetRate=1.0/1209600.0): + # dataToUpdate shall be a dictionary of block identities (as keys) and their associated blocks (as values) + # to be added to the local data. We assume difficulty scores have been reported honestly for now. + + # Stash a copy of incoming blocks so removing keys won't shrink the size of the dictionary over which + # we are looping. + tempData = copy.deepcopy(incBlocks) + for key in incBlocks: + if incBlocks[key].parent in self.data["blockchain"].blocks: + self.data["blockchain"].addBlock(incBlocks[key]) + #if len(self.data["blockchain"]) % diffUpdateRate == 0: + # self.updateDifficulty(mode, targetRate) + del tempData[key] + incBlocks = copy.deepcopy(tempData) + while len(incBlocks)>0: + for key in incBlocks: + if incBlocks[key].parent in self.data["blockchain"].blocks: + self.data["blockchain"].addBlock(incBlocks[key]) + #if len(self.data["blockchain"]) % diffUpdateRate == 0: + # self.updateDifficulty(mode, targetRate) + del tempData[key] + incBlocks = copy.deepcopy(tempData) + + def updateDifficulty(self, mode="Nakamoto", targetRate=1.0/1209600.0): + # Compute the difficulty of the next block + # Note for default, targetRate = two weeks/period, seven days/week, 24 hours/day, 60 minutes/hour, 60 seconds/minute) = 1209600 seconds/period + if mode=="Nakamoto": + # Use MLE estimate of poisson process, compare to targetRate, update by multiplying by resulting ratio. + count = 2016 + ident = self.data.miningIdent + topTime = copy.deepcopy(int(round(self.data.blocks[ident].discoTimestamp))) + parent = self.data.blocks[ident].parent + count = count - 1 + while count > 0 and parent is not None: + ident = copy.deepcopy(parent) + parent = self.data.blocks[ident].parent + count = count - 1 + botTime = copy.deepcopy(int(round(self.data.blocks[ident].discoTimestamp))) + + # Algebra is okay: + assert 0 <= 2016 - count and 2016 - count < 2017 + assert topTime > botTime + + # MLE estimate of arrivals per second: + mleDiscoRate = float(2016 - count)/float(topTime - botTime) + + # How much should difficulty change? + self.diff = self.diff*(mleDiscoRate/targetRate) + + elif mode=="vanSaberhagen": + # Similar to above, except use 1200 blocks, discard top 120 and bottom 120 after sorting. + # 4 minute blocks in the original cryptonote, I believe... targetRate = 1.0/ + # 4 minutes/period, 60 seconds/minute ~ 240 seconds/period + assert targetRate==1.0/240.0 + count = 1200 + ident = self.data.miningIdent + bl = [] + bl.append(copy.deepcopy(self.data.blocks[ident].discoTimestamp)) + parent = self.data.blocks[ident].parent + count = count - 1 + while count > 0 and parent is not NOne: + ident = copy.deepcopy(parent) + bl.append(copy.deepcopy(self.data.blocks[ident].discoTimestamp)) + parent = self.data.blocks[ident].parent + count = count-1 + # sort + bl = sorted(bl) + + # remove outliers + bl = bl[120:-120] + + # get topTime and botTime + topTime = bl[-1] + botTime = bl[0] + + # Assert algebra will work + assert 0 <= 960 - count and 960 - count < 961 + assert topTime > botTime + + # Sort of the MLE: # blocks/difference in reported times + # But not the MLE, since the reported times may not be + # the actual times, the "difference in reported times" != + # "ground truth difference in block discoery times" in general + naiveDiscoRate = (960 - count)/(topTime - botTime) + + # How much should difficulty change? + self.diff = self.diff*(naiveDiscoRate/targetRate) + + elif mode=="MOM:expModGauss": + # Similar to "vanSaberhagen" except with 2-minute blocks and + # we attempt to take into account that "difference in timestamps" + # can be negative by: + # 1) insisting that the ordering induced by the blockchain and + # 2) modeling timestamps as exponentially modified gaussian. + # If timestamps are T = X + Z where X is exponentially dist- + # ributed with parameter lambda and Z is some Gaussian + # noise with average mu and variance sigma2, then we can est- + # imate sigma2, mu, and lambda: + # mu ~ mean - stdev*(skewness/2)**(1.0/3.0) + # sigma2 ~ variance*(1-(skewness/2)**(2.0/3.0)) + # lambda ~ (1.0/(stdev))*(2/skewness)**(1.0/3.0) + assert targetRate==1.0/120.0 + count = 1200 + ident = self.data.miningIdent + bl = [] + bl.append(copy.deepcopy(self.data.blocks[ident].discoTimestamp)) + parent = self.data.blocks[ident].parent + count = count - 1 + while count > 0 and parent is not NOne: + ident = copy.deepcopy(parent) + bl.append(copy.deepcopy(self.data.blocks[ident].discoTimestamp)) + parent = self.data.blocks[ident].parent + count = count-1 + sk = skew(bl) + va = var(bl) + stdv = sqrt(va) + lam = (1.0/stdv)*(2.0/sk)**(1.0/3.0) + self.diff = self.diff*(lam/targetRate) + else: + print("Error, invalid difficulty mode entered.") + + def propagate(self, blockIdent): + for edgeIdent in self.edges: + e = self.edges[edgeIdent] + l = e.data["length"] + toa = self.t + l + mIdent = e.getNeighbor(n.ident) + m = e.nodes[mIdent] + if blockIdent not in m.data["blockchain"]: + pB = e.data["pendingBlocks"] + pendingIdent = newIdent(len(pB)) + pendingDat = {"timeOfArrival":toa, "destIdent":mIdent, "block":self.blocks[blockIdent]} + pB.update({pendingIdent:pendingDat}) + + +class Test_Node(unittest.TestCase): + def test_node(self): + nellyIdent = newIdent(0) + bill = Blockchain([], verbosity=True) + + name = newIdent(0) + t = time.time() + diff = 1.0 + params = [name, t, t+1, None, diff, bill.verbose] # Genesis block has no parent, so parent = None + genesis = Block(params) + bill.addBlock(genesis) + + time.sleep(10) + + name = newIdent(1) + t = time.time() + diff = 1.0 + params = [name, t, t+1, genesis.ident, diff, bill.verbose] + blockA = Block(params) + bill.addBlock(blockA) + + # Nodes need an identity and a blockchain object and verbosity and difficulty + nelly = Node([nellyIdent, copy.deepcopy(bill), bill.verbosity, diff]) + nelly.updateDifficulty(mode="Nakamoto") + + time.sleep(9) + + name = newIdent(len(nelly.data)) + t = time.time() + params = [name, t, t+1, blockA.ident, nelly.diff, nelly.verbose] + blockB = Block(params) + nelly.updateBlockchain({blockB.ident:blockB}) + + time.sleep(8) + + name = newIdent(len(nelly.data)) + t = time.time() + params = [name, t, t+1, blockB.ident, nelly.diff, nelly.verbose] + blockC = Block(params) + nelly.updateBlockchain({blockC.ident:blockC}) + + time.sleep(1) + name = newIdent(len(nelly.data)) + t = time.time() + params = [name, t, t+1, blockB.ident, nelly.diff, nelly.verbose] # Fork off + blockD = Block(params) + nelly.updateBlockchain({blockD.ident:blockD}) + + time.sleep(7) + name = newIdent(len(nelly.data)) + t = time.time() + params = [name, t, t+1, blockD.ident, nelly.diff, nelly.verbose] + blockE = Block(params) + nelly.updateBlockchain({blockE.ident:blockE}) + + + time.sleep(6) + name = newIdent(len(nelly.data)) + t = time.time() + params = [name, t, t+1, blockE.ident, nelly.diff, nelly.verbose] + blockF = Block(params) + nelly.updateBlockchain({blockF.ident:blockF}) + + +suite = unittest.TestLoader().loadTestsFromTestCase(Test_Blockchain) +unittest.TextTestRunner(verbosity=1).run(suite) + + class Edge(object): - def __init__(self, params=["", {}]): - self.ident = params[0] - self.data = params[1] - self.nodes = {} + ''' + Edge object. Has an identity, some data, and a dict of nodes. + ''' + def __init__(self, params=["", {}, True]): + try: + assert len(params)==3 + except AssertionError: + print("Error, tried to create mal-formed edge.") + else: + self.ident = params[0] + self.data = params[1] + self.verbose = params[2] + self.nodes = {} def getNeighbor(self, nodeIdent): - result = nodeIdent in self.nodes + # Given one node identity, check that the node + # identity is in the edge's node list and + # return the identity of the other adjacent node. + result = (nodeIdent in self.nodes) if result: for otherIdent in self.nodes: if otherIdent != nodeIdent: result = otherIdent + assert result in self.nodes return result class Graph(object): - def __init__(self, params={}): + ''' + Graph object. Contains some data, a dict of nodes, and a dict of edges. + ''' + def __init__(self, params={}, verbosity=True): self.data=params + self.verbose = verbosity self.nodes = {} self.edges = {} - def newIdent(self, nonce): - return hash(str(nonce) + str(random.random())) + def createGraph(self, numNodes, probEdge, maxNeighbors): - self.data.update({"numNodes":numNodes, "probEdge":probEdge, "maxNeighbors":maxNeighbors}) + # Create a new random graph with numNodes nodes, a + # likelihood any unordered pair of vertices has an edge + # probEdge, and maximum number of neighbors per node + # maxNeighbors. + + # First, include inputted information into self.data + self.data.update({"probEdge":probEdge, "maxNeighbors":maxNeighbors}) + + # Next, for each node to be added, create the node and name it. for i in range(numNodes): - nIdent = self.newIdent(i) - n = Node([nIdent,{}]) + nIdent = newIdent(i) + bl = Blockchain([], verbosity=True) + dat = {"blockchain":bl, "intensity":newIntensity(["uniform"]), "offset":newOffset("sumOfSkellams")} + # A node needs an ident, a data object, a verbosity, and a difficulty + n = Node([nIdent, dat, self.verbose, 1.0]) self.nodes.update({n.ident:n}) - touched = {} + + # Next, for each possible node pair, decide if an edge exists. + touched = {} # Dummy list of node pairs we have already considered. for nIdent in self.nodes: - n = self.nodes[nIdent] + n = self.nodes[nIdent] # Pick a node for mIdent in self.nodes: - m = self.nodes[mIdent] - notSameNode = (nIdent != mIdent) - nOpenSlots = (len(n.edges) < self.data["maxNeighbors"]) + m = self.nodes[mIdent] # Pick a pair element + notSameNode = (nIdent != mIdent) # Ensure we aren't dealing with (x,x) + nOpenSlots = (len(n.edges) < self.data["maxNeighbors"]) # ensure both nodes have open slots available for new edges mOpenSlots = (len(m.edges) < self.data["maxNeighbors"]) - untouched = ((nIdent, mIdent) not in touched) + untouched = ((nIdent, mIdent) not in touched) # make sure the pair and its transposition have not been touched dehcuotnu = ((mIdent, nIdent) not in touched) if notSameNode and nOpenSlots and mOpenSlots and untouched and dehcuotnu: + # Mark pair as touhed touched.update({(nIdent,mIdent):True, (mIdent,nIdent):True}) if random.random() < self.data["probEdge"]: + # Determine if edge should exist and if so, add it. nonce = len(self.edges) - e = Edge([self.newIdent(nonce),{"length":random.random(), "pendingBlocks":[]}]) + e = Edge([newIdent(nonce),{"length":random.random(), "pendingBlocks":[]},self.verbose]) e.nodes.update({n.ident:n, m.ident:m}) self.nodes[nIdent].edges.update({e.ident:e}) self.nodes[mIdent].edges.update({e.ident:e}) self.edges.update({e.ident:e}) + def addNode(self): - n = Node([self.newIdent(len(self.nodes)), {}]) + # Add new node + n = Node([self.newIdent(len(self.nodes)), {}, self.verbose, 1.0]) self.nodes.update({n.ident:n}) for mIdent in self.nodes: + # For every other node, check if an edge should exist and if so add it. m = self.nodes[mIdent] notSameNode = (n.ident != mIdent) nOpenSlots = (len(n.edges) < self.data["maxNeighbors"]) mOpenSlots = (len(m.edges) < self.data["maxNeighbors"]) if notSameNode and nOpenSlots and mOpenSlots and random.random() < self.data["probEdge"]: nonce = len(self.edges) - e = Edge([self.newIdent(nonce), {"length":random.random(), "pendingBlocks":[]}]) + e = Edge([self.newIdent(nonce), {"length":random.random(), "pendingBlocks":[]}, self.verbose]) e.nodes.update({n.ident:n, m.ident:m}) n.edges.update({e.ident:e}) self.nodes[mIdent].edges.update({e.ident:e}) @@ -100,10 +539,13 @@ class Graph(object): return n.ident def delNode(self, ident): + # Remove a node and wipe all memory of its edges from history. edgesToDelete = self.nodes[ident].edges for edgeIdent in edgesToDelete: - if edgeIdent in self.edges: - del self.edges[edgeIdent] + e = edgesToDelete[edgeIdent] + otherIdent = e.getNeighbor(ident) + del self.edges[edgeIdent] + del self.nodes[otherIdent].edges[edgeIdent] del self.nodes[ident] class Test_Graph(unittest.TestCase): @@ -111,8 +553,6 @@ class Test_Graph(unittest.TestCase): greg = Graph() greg.createGraph(3, 0.5, 10) self.assertEqual(len(greg.nodes),3) - for edge in greg.edges: - print(greg.edges[edge].ident, "\t", [greg.edges[edge].nodes[n].ident for n in greg.edges[edge].nodes], "\n") greg.addNode() self.assertEqual(len(greg.nodes),4) for edge in greg.edges: @@ -127,43 +567,69 @@ class Test_Graph(unittest.TestCase): suite = unittest.TestLoader().loadTestsFromTestCase(Test_Graph) unittest.TextTestRunner(verbosity=1).run(suite) - - - - - -class FishGraph(stochasticProcess): - def __init__(self, params=None): +class FishGraph(StochasticProcess): + ''' + Stochastic process on a graph + with the graph growing in a stochastic process too + ''' + # TODO: Check if output.txt exists before beginning. If so, clear it and create a new one. + # TODO: Instead of/in addition to storing graph data in a text file, can we plot with ggplot in R? + def __init__(self, params=None, verbosity=True): + # Initialize + + assert "maxTime" in params + self.maxTime = copy.deepcopy(params["maxTime"]) + del params["maxTime"] + + assert "numNodes" in params + numNodes = params["numNodes"] + del params["numNodes"] + self.data = params self.t = 0.0 self.state = Graph() - self.state.createGraph(self.data["numNodes"], self.data["probEdge"], self.data["maxNeighbors"]) + self.filename = "output.txt" + self.verbose = verbosity + + # Create graph + self.state.createGraph(numNodes, self.data["probEdge"], self.data["maxNeighbors"]) + + # Update node data for nIdent in self.state.nodes: n = self.state.nodes[nIdent] - difficulty = 10000.0 - intensity = random.random()/difficulty - offset = 2.0*random.random() - 1.0 - n.data.update({"intensity":intensity, "offset":offset, "blockchain":{}}) + difficulty = 1.0 + intensity = newIntensity(params="uniform") + offset = newOffset(params="sumOfSkellams") + dat = {"intensity":intensity, "offset":offset, "blockchain":Blockchain([], verbosity=self.verbose)} + n.data.update(dat) + + # Update edge data. for eIdent in self.state.edges: e = self.state.edges[eIdent] - e.data.update({"pendingBlocks":[]}) - self.maxTime = self.data["maxTime"] + e.data.update({"pendingBlocks":{}}) def go(self): assert self.maxTime > 0.0 while self.t <= self.maxTime and len(self.state.nodes) > 0: deltaT = self.getNextTime() - self.updateState(deltaT) - #print(str(self.t) + ", " + str(self.state)) + self.updateState(self.t, deltaT) + self.record() def getNextTime(self): + # Each Poisson process event generates an exponential random variable. + # The smallest of these is selected + # The rate of the smallest determines event type. eventTag = None - u = copy.deepcopy(random.random()) + u = 0.0 + while(u == 0.0): + u = copy.deepcopy(random.random()) u = -1.0*math.log(copy.deepcopy(u))/self.data["birthRate"] # Time until next stochastic birth eventTag = "birth" - v = copy.deepcopy(random.random()) + v = 0.0 + while(v == 0.0): + v = copy.deepcopy(random.random()) v = -1.0*math.log(copy.deepcopy(v))/self.data["deathRate"] # Time until next stochastic death if v < u: u = copy.deepcopy(v) @@ -171,94 +637,160 @@ class FishGraph(stochasticProcess): for nIdent in self.state.nodes: n = self.state.nodes[nIdent] # n.ident = nIdent - v = copy.deepcopy(random.random()) + v = 0.0 + while(v == 0.0): + v = copy.deepcopy(random.random()) v = -1.0*math.log(copy.deepcopy(v))/n.data["intensity"] if v < u: u = copy.deepcopy(v) eventTag = ["discovery", n.ident] + # Now that all the STOCHASTIC arrivals have been decided, + # We check if any of the deterministic events fire off instead. for eIdent in self.state.edges: e = self.state.edges[eIdent] # e.ident = eIdent - bufferedBlocks = e.data["pendingBlocks"] - if len(bufferedBlocks) > 0: - for pendingIdent in bufferedBlocks: - pB = bufferedBlocks[pendingIdent] - v = pB["timeOfArrival"] - if v < u: + pB = e.data["pendingBlocks"] + if len(pB) > 0: + for pendingIdent in pB: + arrivalInfo = pB[pendingIdent] + v = arrivalInfo["timeOfArrival"] - self.t + if v < u and 0.0 < v: u = copy.deepcopy(v) eventTag = ["arrival", e.ident, pendingIdent] deltaT = (u, eventTag) - # eventTag = ["arrival", e.ident, pendingIdent] - # eventTag = ["discovery", n.ident] - # eventTag = "death" - # eventTag = "birth" + # Formats: + # eventTag = ["arrival", e.ident, pendingIdent] + # eventTag = ["discovery", n.ident] + # eventTag = "death" + # eventTag = "birth" return deltaT - def updateState(self, deltaT): + def updateState(self, t, deltaT, mode="Nakamoto", targetRate=1.0/1209600.0): + # Depending on eventTag, update the state... u = deltaT[0] + shout = "" eventTag = deltaT[1] if type(eventTag)==type("birthordeath"): if eventTag == "death": # Picks random nodeIdent and kills it toDie = random.choice(list(self.state.nodes.keys())) - print("DEATH EVENT:") - print("Pre-death population = ", len(self.state.nodes)) + x = len(self.state.nodes) + shout += "DEATH, Pop(Old)=" + str(x) + ", Pop(New)=" + if self.verbose: + print(shout) self.state.delNode(toDie) - print("Post-death population = ", len(self.state.nodes)) - print("Done. \n") + y = len(self.state.nodes) + assert y == x - 1 + shout += str(y) + "\n" elif eventTag == "birth": # Adds node with some randomly determined edges - print("BIRTH EVENT:") - print("Pre-birth population = ", len(self.state.nodes)) + x = len(self.state.nodes) + shout += "BIRTH, Pop(Old)=" + str(x) + ", Pop(New)=" + if self.verbose: + print(shout) nIdent = self.state.addNode() - print("Post-death population = ", len(self.state.nodes)) n = self.state.nodes[nIdent] - intensity = random.random()/10000.0 + intensity = random.random()/1000.0 offset = 2.0*random.random() - 1.0 n.data.update({"intensity":intensity, "offset":offset, "blockchain":{}}) # Auto syncs new node. - print("Auto syncing new node...") for eIdent in n.edges: e = n.edges[eIdent] - e.data.update({"pendingBlocks":[]}) + e.data.update({"pendingBlocks":{}}) mIdent = e.getNeighbor(n.ident) m = self.state.nodes[mIdent] mdata = m.data["blockchain"] n.data["blockchain"].update(mdata) - print("Done. \n") + y = len(self.state.nodes) + assert y == x + 1 + shout += str(y) + "\n" else: - print("Error: eventTag had length 1 but was neighter a birth or a death. We had ", eventTag) + print("Error: eventTag had length 1 but was neighter a birth or a death, this shouldn't happen so this else case will eventually be removed, I guess? Our eventTag = ", eventTag) elif len(eventTag)==2: + # Block is discovered and plunked into each edge's pendingBlock list. + + shout += "DISCOVERY" + if self.verbose: + print(shout) + assert eventTag[0]=="discovery" - nIdent = eventTag[1] - n = self.state.nodes[n] - s = self.t + n.data["offset"] - n.data["blockchain"].append(s) - for edgeIdent in n.edges: - e = n.edges[edgeIdent] - l = e.data["length"] - toa = self.t + l - mIdent = e.getNeighbor(n.ident) - e.data["pendingBlocks"].append({"timestamp":s, "timeOfArrival":toa, "destIdent":mIdent}) - print("Block Discovery event. To be coded. For now, blah blah.") - elif len(eventTag)==3: + assert eventTag[1] in self.state.nodes + nIdent = eventTag[1] # get founding node's identity + n = self.state.nodes[nIdent] # get founding node + s = self.t + n.data["offset"] # get founding node's wall clock + + newBlockIdent = newIdent(len(n.data["blockchain"].blocks)) # generate new identity + disco = s + arriv = s + parent = n.data["blockchain"].miningIdent + diff = copy.deepcopy(n.diff) + verbosity = self.verbose + + newBlock = Block([newBlockIdent, disco, arriv, parent, diff, verbosity]) + n.updateBlockchain({newBlockIdent:newBlock}) + n.updateDifficulty(mode, targetRate) + n.propagate(newBlockIdent) + + elif len(eventTag)==3: + #eventTag = ("arrival", e.ident, pendingIdent) + # A block deterministically arrives at the end of an edge. + assert eventTag[0]=="arrival" - print("Block Arrival event. To be coded. For now, blah blah.") + shout += "ARRIVAL" + if self.verbose: + print(shout) + + eIdent = eventTag[1] + pendingIdent = eventTag[2] + e = self.state.edges[eIdent] + pB = e.data["pendingBlocks"] + arrivalInfo = pB[pendingIdent] # arrivalInfo = {"timeOfArrival":toa, "destIdent":mIdent, "block":newBlock} + + assert arrivalInfo["destIdent"] in self.state.nodes + assert self.t + u == arrivalInfo["timeOfArrival"] + receiver = self.state.nodes[arrivalInfo["destIdent"]] + arriv = self.t + u + receiver.data["offset"] + newBlock = arrivalInfo["block"] + newBlock.arrivTimestamp = copy.deepcopy(arriv) + receiver.data["blockchain"].updateBlockchain({newBlock.ident:newBlock}) + receiver.updateDifficulty(mode, targetRate) + receiver.propagate(newBlock.ident) + else: print("Error: eventTag was not a string, or not an array length 2 or 3. In fact, we have eventTag = ", eventTag) - + if self.verbose: + print("u = ", u) + self.t += u + if self.verbose: + print(str(self.t) + "\t" + shout) + + def record(self): + with open(self.filename, "a") as f: + line = "" + # Format will be edgeIdent,nodeAident,nodeBident + line += str("t=" + str(self.t) + ",") + ordKeyList = sorted(list(self.state.edges.keys())) + for key in ordKeyList: + entry = [] + entry.append(key) + nodeKeyList = sorted(list(self.state.edges[key].nodes)) + for kkey in nodeKeyList: + entry.append(kkey) + line += str(entry) + "," + f.write(line + "\n") class Test_FishGraph(unittest.TestCase): def test_fishGraph(self): - params = {"numNodes":10, "probEdge":0.5, "maxNeighbors":10, "maxTime":100.0, "birthRate":1.1, "deathRate":0.2} - greg = FishGraph(params) - greg.go() - - + for i in range(10): + params = {"numNodes":10, "probEdge":0.5, "maxNeighbors":10, "maxTime":10.0, "birthRate":0.001, "deathRate":0.001} + greg = FishGraph(params, verbosity=True) + greg.go() + + suite = unittest.TestLoader().loadTestsFromTestCase(Test_FishGraph) unittest.TextTestRunner(verbosity=1).run(suite)