main.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. from hashlib import sha256
  2. from datetime import datetime
  3. from random import randint, random
  4. from collections import Counter
  5. import math
  6. import asyncio
  7. import logging
  8. import matplotlib.pyplot as plt
  9. import networkx as nx
  10. import numpy as np
  11. EventId = str
  12. EventIds = list[EventId]
  13. class NetworkPool:
  14. def __init__(self, nodes):
  15. self.nodes = nodes
  16. def request(self, event_id: EventId):
  17. for n in self.nodes:
  18. event = n.get_event(event_id)
  19. if event != None:
  20. return (n.name, event)
  21. return None
  22. class Event:
  23. def __init__(self, parents: EventIds):
  24. self.timestamp = datetime.now().timestamp
  25. self.parents = sorted(parents)
  26. def set_timestamp(self, timestamp):
  27. self.timestamp = timestamp
  28. # Hash of timestamp and the parents
  29. def hash(self) -> str:
  30. m = sha256()
  31. m.update(str.encode(str(self.timestamp)))
  32. for p in self.parents:
  33. m.update(str.encode(str(p)))
  34. return m.digest().hex()
  35. def __str__(self):
  36. res = f"{self.hash()}"
  37. for p in self.parents:
  38. res += f"\n |"
  39. res += f"\n - {p}"
  40. res += f"\n"
  41. return res
  42. """
  43. # Graph Example
  44. E1: []
  45. E2: [E1]
  46. E3: [E1]
  47. E4: [E3]
  48. E5: [E3]
  49. E6: [E4, E5]
  50. E7: [E4]
  51. E8: [E2]
  52. """
  53. class Graph:
  54. def __init__(self):
  55. self.events = dict()
  56. def add_event(self, event: Event):
  57. self.events[event.hash()] = event
  58. def remove_event(self, event_id: EventId):
  59. if event_id in self.events:
  60. del self.events[event_id]
  61. # Check if given events are exist in the graph
  62. # return a list of missing events
  63. def check(self, events: EventIds) -> EventIds:
  64. missing_events = []
  65. for e in events:
  66. if self.events.get(e) == None:
  67. missing_events.append(e)
  68. return missing_events
  69. def __str__(self):
  70. res = ""
  71. for event in self.events.values():
  72. res += f"\n {event}"
  73. return res
  74. class Node:
  75. def __init__(self, name: str, queue):
  76. self.name = name
  77. self.orphan_pool = Graph()
  78. self.active_pool = Graph()
  79. self.queue = queue
  80. # The active pool should always start with one event
  81. genesis_event = Event([])
  82. genesis_event.set_timestamp(0.0)
  83. self.genesis_event = genesis_event
  84. self.active_pool.add_event(genesis_event)
  85. # On the initialization make the root node as head
  86. self.heads = [genesis_event.hash()]
  87. # Remove the parents for the event if they are exist in heads
  88. def remove_heads(self, event):
  89. for p in event.parents:
  90. if p in self.heads:
  91. self.heads.remove(p)
  92. # Add the event to heads
  93. def update_heads(self, event):
  94. event_hash = event.hash()
  95. self.remove_heads(event)
  96. self.heads.append(event_hash)
  97. self.heads = sorted(self.heads)
  98. # On receive new event
  99. def receive_new_event(self, event: Event, peer, np):
  100. logging.debug(f"{self.name} receive event from {peer}: \n {event}")
  101. event_hash = event.hash()
  102. # Reject event with no parents
  103. if not event.parents:
  104. return
  105. # Reject event already exist in active pool
  106. if not self.active_pool.check([event_hash]):
  107. return
  108. # Reject event already exist in orphan pool
  109. if not self.orphan_pool.check([event_hash]):
  110. return
  111. # Check if parents for this event are missing from active pool
  112. missing_parents = self.active_pool.check(event.parents)
  113. if not missing_parents:
  114. # Add the event to active pool
  115. self.active_pool.add_event(event)
  116. self.update_heads(event)
  117. # Move events from oprhan pool to active pool if they are child of
  118. # the new added event
  119. remove_list: EventIds = []
  120. self.relink(event, remove_list)
  121. # Clean up orphan pool
  122. for ev in remove_list:
  123. self.orphan_pool.remove_event(ev)
  124. else:
  125. # Add the received event to the orphan pool
  126. self.orphan_pool.add_event(event)
  127. # Check if all missing parents are in orphan pool, otherwise
  128. # request them from the network
  129. request_list = []
  130. self.check_parents(request_list, missing_parents)
  131. logging.debug(
  132. f"{self.name} request from the network: {request_list}")
  133. # XXX
  134. # Send all the missing parents in request_list
  135. # to the node who send this event
  136. # For simulation purpose the node fetch the missed parents from the
  137. # network pool which contains all the nodes and its messages
  138. for event in request_list:
  139. peer, requested_event = np.request(event)
  140. if requested_event != None:
  141. self.receive_new_event(requested_event, peer, np)
  142. else:
  143. # It must always find the missed event from the network
  144. logging.error(
  145. f"Error: {self.name} requested {event} not found")
  146. # This will check if passed parents are in the orphan pool, and fill
  147. # request_list with missing parents
  148. def check_parents(self, request_list, parents: EventIds, visited=[]):
  149. for parent_hash in parents:
  150. # Check if the function already visit this parent
  151. if parent_hash in visited:
  152. continue
  153. visited.append(parent_hash)
  154. # If the parent in orphan pool, do recursive call to check its
  155. # parents as well, otherwise add the parent to request_list
  156. if parent_hash in self.orphan_pool.events:
  157. parent = self.orphan_pool.events[parent_hash]
  158. # Recursive call
  159. self.check_parents(request_list, parent.parents, visited)
  160. else:
  161. request_list.append(parent_hash)
  162. # Check if the orphan pool has an event linked
  163. # to the passed event and relink it accordingly
  164. def relink(self, event: Event, remove_list):
  165. event_hash = event.hash()
  166. for (orphan_hash, orphan) in self.orphan_pool.events.items():
  167. # Check if the orphan is not already in remove_list
  168. if orphan_hash in remove_list:
  169. continue
  170. # Check if the event is a parent of orphan event
  171. if event_hash not in orphan.parents:
  172. continue
  173. # Check if the remain parents of the orphan
  174. # are not missing from active pool
  175. missing_parents = self.active_pool.check(orphan.parents)
  176. if not missing_parents:
  177. # Add the orphan to active pool
  178. self.active_pool.add_event(orphan)
  179. self.update_heads(orphan)
  180. # Add the orphan to remove_list
  181. remove_list.append(orphan_hash)
  182. # Recursive call
  183. self.relink(orphan, remove_list)
  184. def get_event(self, event_id: EventId):
  185. # Check the active_pool
  186. event = self.active_pool.events.get(event_id)
  187. # Check the orphan_pool
  188. if event == None:
  189. event = self.orphan_pool.events.get(event)
  190. return event
  191. def __str__(self):
  192. return f"""
  193. \n Name: {self.name}
  194. \n Active Pool: {self.active_pool}
  195. \n Orphan Pool: {self.orphan_pool}
  196. \n Heads: {self.heads}"""
  197. # Each node has nodes_n of this function running in the background
  198. # for receiving events from each node separately
  199. async def recv_loop(podm, node, peer, queue, np):
  200. while True:
  201. # Wait new event
  202. event = await queue.get()
  203. queue.task_done()
  204. if event == None:
  205. break
  206. if random() <= podm:
  207. logging.debug(f"{node.name} dropped: \n {event}")
  208. continue
  209. node.receive_new_event(event, peer, np)
  210. # Send new event at random intervals
  211. # Each node has this function running in the background
  212. async def send_loop(nodes_n, max_delay, broadcast_attempt, node):
  213. for _ in range(broadcast_attempt):
  214. await asyncio.sleep(randint(0, max_delay))
  215. # Create new event with the last heads as parents
  216. event = Event(node.heads)
  217. logging.debug(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. """
  222. Run a simulation with the provided params:
  223. nodes_n: number of nodes
  224. podm: probability of dropping messages (ex: 0.30 -> %30)
  225. broadcast_attempt: number of messages each node should broadcast
  226. """
  227. async def run(nodes_n=3, podm=0.30, broadcast_attempt=3, check=False):
  228. logging.debug(f"Running simulation with nodes: {nodes_n}, podm: {podm},\
  229. broadcast_attempt: {broadcast_attempt}")
  230. max_delay = round(math.log(nodes_n))
  231. broadcast_timeout = nodes_n * broadcast_attempt * max_delay
  232. nodes = []
  233. logging.info(f"Run {nodes_n} Nodes")
  234. try:
  235. # Initialize nodes_n nodes
  236. for i in range(nodes_n):
  237. queue = asyncio.Queue()
  238. node = Node(f"Node{i}", queue)
  239. nodes.append(node)
  240. # Initialize NetworkPool contains all nodes
  241. np = NetworkPool(nodes)
  242. # Initialize nodes_n * nodes_n coroutine tasks for receiving events
  243. # Each node listen to all queues from the running nodes
  244. recv_tasks = []
  245. for node in nodes:
  246. for n in nodes:
  247. recv_tasks.append(recv_loop(podm, node, n.name, n.queue, np))
  248. r_g = asyncio.gather(*recv_tasks)
  249. # Create coroutine task contains send_loop function for each node
  250. # Run and wait for send tasks
  251. s_g = asyncio.gather(
  252. *[send_loop(nodes_n, max_delay, broadcast_attempt, n) for n in nodes])
  253. await asyncio.wait_for(s_g, broadcast_timeout)
  254. # Gracefully stop all receiving tasks
  255. for n in nodes:
  256. for _ in range(nodes_n):
  257. await n.queue.put(None)
  258. await n.queue.join()
  259. await r_g
  260. if check:
  261. for node in nodes:
  262. logging.debug(node)
  263. # Assert if all nodes share the same active pool graph
  264. assert (all(n.active_pool.events.keys() ==
  265. nodes[0].active_pool.events.keys() for n in nodes))
  266. # Assert if all nodes share the same orphan pool graph
  267. assert (all(n.orphan_pool.events.keys() ==
  268. nodes[0].orphan_pool.events.keys() for n in nodes))
  269. return nodes
  270. except asyncio.exceptions.TimeoutError:
  271. logging.error("Broadcast TimeoutError")
  272. async def main():
  273. # run the simulation `sim_n` times with a fixed `podm`
  274. # and increase number of nodes by `sim_nodes_inc`
  275. sim = []
  276. sim_n = 5
  277. sim_nodes_inc = 2
  278. # number of nodes
  279. nodes_n = 10
  280. # probability of dropping messages
  281. podm = 0.10
  282. # number of messages each node should broadcast
  283. broadcast_attempt = 10
  284. for _ in range(sim_n):
  285. nodes = await run(nodes_n, podm, broadcast_attempt)
  286. sim.append(nodes)
  287. nodes_n += sim_nodes_inc
  288. nodes_n_list = []
  289. msgs_synced = []
  290. for nodes in sim:
  291. nodes_n = len(nodes)
  292. nodes_n_list.append(nodes_n)
  293. events = Counter()
  294. for node in nodes:
  295. events.update(list(node.active_pool.events.keys()))
  296. # Remove the genesis event
  297. del events["8aed642bf5118b9d3c859bd4be35ecac75b6e873cce34e7b6f554b06f75550d7"]
  298. expect_msgs_synced = (nodes_n * broadcast_attempt)
  299. actual_msgs_synced = 0
  300. for val in events.values():
  301. # if the event is fully synced with all nodes
  302. if val == nodes_n:
  303. actual_msgs_synced += 1
  304. res = (actual_msgs_synced * 100) / expect_msgs_synced
  305. msgs_synced.append(res)
  306. logging.info(events)
  307. logging.info(f"nodes_n: {nodes_n}")
  308. logging.info(f"actual_msg_synced: {actual_msgs_synced}")
  309. logging.info(f"expect_msgs_synced: {expect_msgs_synced}")
  310. logging.info(f"res: %{res}")
  311. logging.disable()
  312. plt.plot(nodes_n_list, msgs_synced)
  313. plt.ylim(0, 100)
  314. plt.title(
  315. f"Event Graph simulation with %{podm * 100} probability of dropping messages")
  316. plt.ylabel("Events sync percentage")
  317. plt.xlabel("Number of nodes")
  318. plt.show()
  319. def print_network_graph(nodes):
  320. for (i, node) in enumerate(nodes):
  321. graph = nx.Graph()
  322. for (h, ev) in node.active_pool.events.items():
  323. graph.add_node(h[:5])
  324. graph.add_edges_from([(h[:5], p[:5]) for p in ev.parents])
  325. colors = []
  326. node_heads = [h[:5] for h in node.heads]
  327. for n in graph.nodes():
  328. if n == "8aed6":
  329. colors.append("red")
  330. elif n in node_heads:
  331. colors.append("yellow")
  332. else:
  333. colors.append("blue")
  334. plt.figure(i)
  335. nx.draw_networkx(graph, with_labels=True, node_color=colors)
  336. plt.show()
  337. if __name__ == "__main__":
  338. logging.basicConfig(level=logging.DEBUG,
  339. handlers=[logging.FileHandler("debug.log", mode="w"),
  340. logging.StreamHandler()])
  341. asyncio.run(main())