main.py 11 KB

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