main.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. from hashlib import sha256
  2. from datetime import datetime
  3. from random import randint
  4. import math
  5. import asyncio
  6. import matplotlib.pyplot as plt
  7. import networkx as nx
  8. EventId = str
  9. EventIds = list[EventId]
  10. class Event:
  11. def __init__(self, parents: EventIds):
  12. self.timestamp = datetime.now().timestamp
  13. self.parents = sorted(parents)
  14. def set_timestamp(self, timestamp):
  15. self.timestamp = timestamp
  16. # Hash of timestamp and the parents
  17. def hash(self) -> str:
  18. m = sha256()
  19. m.update(str.encode(str(self.timestamp)))
  20. for p in self.parents:
  21. m.update(str.encode(str(p)))
  22. return m.digest().hex()
  23. def __str__(self):
  24. res = f"{self.hash()}"
  25. for p in self.parents:
  26. res += f"\n |"
  27. res += f"\n - {p}"
  28. res += f"\n"
  29. return res
  30. """
  31. ## Graph Example
  32. E1: []
  33. E2: [E1]
  34. E3: [E1]
  35. E4: [E3]
  36. E5: [E3]
  37. E6: [E4, E5]
  38. E7: [E4]
  39. E8: [E2]
  40. """
  41. class Graph:
  42. def __init__(self):
  43. self.events = dict()
  44. def add_event(self, event: Event):
  45. self.events[event.hash()] = event
  46. def remove_event(self, event_id: EventId):
  47. if event_id in self.events:
  48. del self.events[event_id]
  49. # Check if given events are exist in the graph
  50. # return a list of missing events
  51. def check(self, events: EventIds) -> EventIds:
  52. missing_events = []
  53. for e in events:
  54. if self.events.get(e) == None:
  55. missing_events.append(e)
  56. return missing_events
  57. def __str__(self):
  58. res = ""
  59. for event in self.events.values():
  60. res += f"\n {event}"
  61. return res
  62. class Node:
  63. def __init__(self, name: str):
  64. self.name = name
  65. self.orphan_pool = Graph()
  66. self.active_pool = Graph()
  67. # The active pool should always start with one event
  68. genesis_event = Event([])
  69. genesis_event.set_timestamp(0.0)
  70. self.genesis_event = genesis_event
  71. self.active_pool.add_event(genesis_event)
  72. # On the initialization make the root node as head
  73. self.heads = [genesis_event.hash()]
  74. # Remove the parents for the event if they are exist in heads
  75. def remove_heads(self, event):
  76. for p in event.parents:
  77. if p in self.heads:
  78. self.heads.remove(p)
  79. # Add the event to heads
  80. def update_heads(self, event):
  81. event_hash = event.hash()
  82. self.remove_heads(event)
  83. self.heads.append(event_hash)
  84. # On receive new event
  85. def receive_new_event(self, event: Event):
  86. event_hash = event.hash()
  87. # Reject event with no parents
  88. if not event.parents:
  89. return
  90. # Reject event already exist in active pool
  91. if not self.active_pool.check([event_hash]):
  92. return
  93. # Reject event already exist in orphan pool
  94. if not self.orphan_pool.check([event_hash]):
  95. return
  96. # Check if parents for this event are missing from active pool
  97. missing_parents = self.active_pool.check(event.parents)
  98. if not missing_parents:
  99. # Add the event to active pool
  100. self.active_pool.add_event(event)
  101. self.update_heads(event)
  102. # Move events from oprhan pool to active pool if they are child of
  103. # the new added event
  104. remove_list: EventIds = []
  105. self.relink(event, remove_list)
  106. # Clean up orphan pool
  107. for ev in remove_list:
  108. self.orphan_pool.remove_event(ev)
  109. else:
  110. # Add the received event to the orphan pool
  111. self.orphan_pool.add_event(event)
  112. # Check if all missing parents are in orphan pool, otherwise
  113. # request them from the network
  114. request_list = []
  115. self.check_parents(request_list, missing_parents)
  116. print(f"{self.name} request from the network: {request_list}")
  117. # XXX
  118. # Send all the missing parents in request_list
  119. # to the node who send this event
  120. # This will check if passed parents are in the orphan pool, and fill
  121. # request_list with missing parents
  122. def check_parents(self, request_list, parents: EventIds, visited=[]):
  123. for parent_hash in parents:
  124. # Check if the function already visit this parent
  125. if parent_hash in visited:
  126. continue
  127. visited.append(parent_hash)
  128. # If the parent in orphan pool, do recursive call to check its
  129. # parents as well, otherwise add the parent to request_list
  130. if parent_hash in self.orphan_pool.events:
  131. parent = self.orphan_pool.events[parent_hash]
  132. # Recursive call
  133. self.check_parents(request_list, parent.parents, visited)
  134. else:
  135. request_list.append(parent_hash)
  136. # Check if the orphan pool has an event linked
  137. # to the passed event and relink it accordingly
  138. def relink(self, event: Event, remove_list=[]):
  139. event_hash = event.hash()
  140. for (orphan_hash, orphan) in self.orphan_pool.events.items():
  141. # Check if the orphan is not already in remove_list
  142. if orphan_hash in remove_list:
  143. continue
  144. # Check if the event is a parent of orphan event
  145. if event_hash not in orphan.parents:
  146. continue
  147. # Check if the remain parents of the orphan
  148. # are not missing from active pool
  149. missing_parents = self.active_pool.check(orphan.parents)
  150. if not missing_parents:
  151. # Add the orphan to active pool
  152. self.active_pool.add_event(orphan)
  153. self.update_heads(orphan)
  154. # Add the orphan to remove_list
  155. remove_list.append(orphan_hash)
  156. # Recursive call
  157. self.relink(orphan, remove_list)
  158. def __str__(self):
  159. return f"""------
  160. \n Name: {self.name}
  161. \n Active Pool: {self.active_pool}
  162. \n Orphan Pool: {self.orphan_pool}"""
  163. # Number of nodes
  164. NODES_N = 15
  165. # Broadcast attempt for each node
  166. BROADCAST_ATTEMPT = 3
  167. MAX_BROADCAST_DELAY = round(math.log(NODES_N))
  168. MIN_BROADCAST_DELAY = 0
  169. # Timeout for sending tasks to finish
  170. BROADCAST_TIMEOUT = NODES_N * BROADCAST_ATTEMPT * MAX_BROADCAST_DELAY
  171. # Each node has NODES_N of this function running in the background
  172. # for receiving events from each node separately
  173. async def recv_loop(node, peer, queue):
  174. while True:
  175. # Wait new event
  176. event = await queue.get()
  177. node.receive_new_event(event)
  178. queue.task_done()
  179. print(f"{node.name} receive event from {peer}: \n {event}")
  180. # Send new event at random intervals
  181. # Each node has this function running in the background
  182. async def send_loop(node, queue):
  183. for _ in range(BROADCAST_ATTEMPT):
  184. await asyncio.sleep(randint(MIN_BROADCAST_DELAY, MAX_BROADCAST_DELAY))
  185. # Create new event with the last heads as parents
  186. event = Event(node.heads)
  187. print(f"{node.name} broadcast event: \n {event}")
  188. for _ in range(NODES_N):
  189. await queue.put(event)
  190. await queue.join()
  191. async def main():
  192. nodes = []
  193. queues = dict()
  194. print(f"Run {NODES_N} Nodes")
  195. try:
  196. # Initialize new nodes and create coroutine task for each node
  197. # contains send_loop function
  198. for i in range(NODES_N):
  199. node = Node(f"Node{i}")
  200. nodes.append(node)
  201. queue = asyncio.Queue()
  202. queues[node.name] = queue
  203. # Initialize NODES_N * NODES_N coroutine tasks for receiving events
  204. for node in nodes:
  205. for (peer, queue) in queues.items():
  206. asyncio.create_task(recv_loop(node, peer, queue))
  207. # Run and wait for send tasks
  208. s_g = asyncio.gather(*[send_loop(n, queues[n.name]) for n in nodes])
  209. await asyncio.wait_for(s_g, BROADCAST_TIMEOUT)
  210. # Assert if all nodes share the same active pool graph
  211. assert (all(n.active_pool.events.keys() ==
  212. nodes[0].active_pool.events.keys() for n in nodes))
  213. # Assert if all nodes share the same orphan pool graph
  214. assert (all(n.orphan_pool.events.keys() ==
  215. nodes[0].orphan_pool.events.keys() for n in nodes))
  216. print_graph([nodes[0]])
  217. except asyncio.exceptions.TimeoutError:
  218. print("Broadcast TimeoutError")
  219. def print_graph(nodes):
  220. for (i, node) in enumerate(nodes):
  221. graph = nx.Graph()
  222. for (h, ev) in node.active_pool.events.items():
  223. graph.add_node(h[:5])
  224. graph.add_edges_from([(h[:5], p[:5]) for p in ev.parents])
  225. colors = []
  226. node_heads = [h[:5] for h in node.heads]
  227. for n in graph.nodes():
  228. if n == "8aed6":
  229. colors.append("red")
  230. elif n in node_heads:
  231. colors.append("yellow")
  232. else:
  233. colors.append("blue")
  234. plt.figure(i)
  235. nx.draw_networkx(graph, with_labels=True, node_color=colors)
  236. plt.show()
  237. def test_node():
  238. node_a = Node("NodeA")
  239. event0 = node_a.genesis_event
  240. event1 = Event([event0.hash()])
  241. event2 = Event([event1.hash()])
  242. event3 = Event([event2.hash(), event0.hash()])
  243. event4 = Event([event1.hash(), event3.hash()])
  244. event5 = Event([event4.hash(), "FAKEHASH"])
  245. event6 = Event([event5.hash(), event3.hash()])
  246. node_a.receive_new_event(event3)
  247. node_a.receive_new_event(event2)
  248. node_a.receive_new_event(event1)
  249. node_a.receive_new_event(event5)
  250. node_a.receive_new_event(event6)
  251. node_a.receive_new_event(event4)
  252. print(node_a)
  253. if __name__ == "__main__":
  254. # test_node()
  255. asyncio.run(main())