main.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  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. from logging import debug, error, info
  9. import matplotlib.pyplot as plt
  10. import networkx as nx
  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 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 self.events.get(event_id) != None:
  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. 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. # Add the new event to the orphan pool
  112. self.orphan_pool.add_event(event)
  113. # This function is the core of syncing algorithm
  114. #
  115. # Find all the links from the new event to events in orphan pool
  116. # Bring these events to the active pool then add the new event
  117. self.relink_orphan(event, np)
  118. def relink_orphan(self, orphan, np):
  119. # Check if the parents of the orphan
  120. # are not missing from active pool
  121. missing_parents = self.active_pool.check(orphan.parents)
  122. if not missing_parents:
  123. self.add_to_active_pool(orphan)
  124. return
  125. # Check the missing parents from orphan pool and sync with the network for
  126. # missing ones
  127. self.check_orphan_pool(list(missing_parents), np)
  128. # At this stage all the missing parents must be in the orphan pool
  129. # The next step is to move them to active pool
  130. self.update_active_pool(missing_parents, [])
  131. # Check again that the parents of the orphan are in the active pool
  132. missing_parents = self.active_pool.check(orphan.parents)
  133. assert (not missing_parents)
  134. # Last stage, add the event to active pool
  135. self.add_to_active_pool(orphan)
  136. def check_orphan_pool(self, missing_events, np):
  137. debug(f"{self.name} check_orphan_pool() {missing_events}")
  138. while True:
  139. # Check if all missing parents are in orphan pool, otherwise
  140. # add them to request list
  141. request_list = []
  142. self.check_missing_parents(request_list, missing_events, [])
  143. if not request_list:
  144. break
  145. missing_events = self.fetch_events(request_list, np)
  146. def check_missing_parents(self, request_list, events: EventIds, visited):
  147. debug(f"{self.name} check_missing_parents() {events}")
  148. for event_hash in events:
  149. # Check if the function already visit this event
  150. if event_hash in visited:
  151. continue
  152. visited.append(event_hash)
  153. # If the event in orphan pool, do recursive call to check its
  154. # parents as well, otherwise add the event to request_list
  155. event = self.orphan_pool.events.get(event_hash)
  156. if event == None:
  157. # Check first if it's not in the active pool
  158. if self.active_pool.events.get(event_hash) == None:
  159. request_list.append(event_hash)
  160. else:
  161. # Recursive call
  162. # Climb up for the event parents
  163. self.check_missing_parents(request_list, event.parents, visited)
  164. def fetch_events(self, request_list, np):
  165. debug(f"{self.name} fetch_events() {request_list}")
  166. # XXX
  167. # Send the events in request_list to the node who send this event.
  168. #
  169. # For simulation purpose the node fetch the missed events from the
  170. # network pool which contains all the nodes and its events
  171. result = []
  172. for p in request_list:
  173. debug(f"{self.name} request from the network: {p}")
  174. # Request from the network
  175. requested_event = np.request(p)
  176. assert (requested_event != None)
  177. # Add it to the orphan pool
  178. self.orphan_pool.add_event(requested_event)
  179. result.extend(requested_event.parents)
  180. # Return parents of requested events
  181. return result
  182. def update_active_pool(self, events, visited):
  183. debug(f"{self.name} update_active_pool() {events}")
  184. for event_hash in events:
  185. # Check if it already visit this event
  186. if event_hash in visited:
  187. continue
  188. visited.append(event_hash)
  189. if self.active_pool.events.get(event_hash) != None:
  190. continue
  191. # Get the event from the orphan pool
  192. event = self.orphan_pool.events.get(event_hash)
  193. assert (event != None)
  194. # Add it to the active pool
  195. self.add_to_active_pool(event)
  196. # Recursive call
  197. # Climb up for the event parents
  198. self.update_active_pool(event.parents, visited)
  199. def add_to_active_pool(self, event):
  200. # Add the event to active pool
  201. self.active_pool.add_event(event)
  202. # Update heads
  203. self.update_heads(event)
  204. # Remove event from orphan pool
  205. self.orphan_pool.remove_event(event.hash())
  206. def get_event(self, event_id: EventId):
  207. # Check the active_pool
  208. event = self.active_pool.events.get(event_id)
  209. # Check the orphan_pool
  210. if event == None:
  211. event = self.orphan_pool.events.get(event)
  212. return event
  213. def __str__(self):
  214. return f"""
  215. \n Name: {self.name}
  216. \n Active Pool: {self.active_pool}
  217. \n Orphan Pool: {self.orphan_pool}
  218. \n Heads: {self.heads}"""
  219. # Each node has nodes_n of this function running in the background
  220. # for receiving events from each node separately
  221. async def recv_loop(podm, node, peer, queue, np):
  222. while True:
  223. # Wait new event
  224. event = await queue.get()
  225. queue.task_done()
  226. if event == None:
  227. break
  228. if random() < podm:
  229. debug(f"{node.name} dropped: \n {event}")
  230. continue
  231. node.receive_new_event(event, peer, np)
  232. # Send new event at random intervals
  233. # Each node has this function running in the background
  234. async def send_loop(nodes_n, max_delay, broadcast_attempt, node):
  235. for _ in range(broadcast_attempt):
  236. await asyncio.sleep(randint(0, max_delay))
  237. # Create new event with the last heads as parents
  238. event = Event(node.heads)
  239. debug(f"{node.name} broadcast event: \n {event}")
  240. for _ in range(nodes_n):
  241. await node.queue.put(event)
  242. await node.queue.join()
  243. """
  244. Run a simulation with the provided params:
  245. nodes_n: number of nodes
  246. podm: probability of dropping events (ex: 0.30 -> %30)
  247. broadcast_attempt: number of events each node should broadcast
  248. check: check if all nodes have the same graph
  249. """
  250. async def run(nodes_n=3, podm=0.30, broadcast_attempt=3, check=False):
  251. debug(f"Running simulation with nodes: {nodes_n}, podm: {podm},\
  252. broadcast_attempt: {broadcast_attempt}")
  253. max_delay = round(math.log(nodes_n))
  254. broadcast_timeout = nodes_n * broadcast_attempt * max_delay
  255. nodes = []
  256. info(f"Run {nodes_n} Nodes")
  257. try:
  258. # Initialize nodes_n nodes
  259. for i in range(nodes_n):
  260. queue = asyncio.Queue()
  261. node = Node(f"Node{i}", queue)
  262. nodes.append(node)
  263. # Initialize NetworkPool contains all nodes
  264. np = NetworkPool(nodes)
  265. # Initialize nodes_n * nodes_n coroutine tasks for receiving events
  266. # Each node listen to all queues from the running nodes
  267. recv_tasks = []
  268. for node in nodes:
  269. for n in nodes:
  270. recv_tasks.append(recv_loop(podm, node, n.name, n.queue, np))
  271. r_g = asyncio.gather(*recv_tasks)
  272. # Create coroutine task contains send_loop function for each node
  273. # Run and wait for send tasks
  274. s_g = asyncio.gather(
  275. *[send_loop(nodes_n, max_delay, broadcast_attempt, n) for n in nodes])
  276. await asyncio.wait_for(s_g, broadcast_timeout)
  277. # Gracefully stop all receiving tasks
  278. for n in nodes:
  279. for _ in range(nodes_n):
  280. await n.queue.put(None)
  281. await n.queue.join()
  282. await r_g
  283. if check:
  284. for node in nodes:
  285. debug(node)
  286. # Assert if all nodes share the same active pool graph
  287. assert (all(n.active_pool.events.keys() ==
  288. nodes[0].active_pool.events.keys() for n in nodes))
  289. # Assert if all nodes share the same orphan pool graph
  290. assert (all(n.orphan_pool.events.keys() ==
  291. nodes[0].orphan_pool.events.keys() for n in nodes))
  292. # Assert if all heads are equal
  293. assert (all(n.heads == nodes[0].heads for n in nodes))
  294. return nodes
  295. except asyncio.exceptions.TimeoutError:
  296. error("Broadcast TimeoutError")
  297. async def main(sim_n=6, nodes_increase=False, podm_increase=False ):
  298. # run the simulation `sim_n` times with increasing `podm` and `nodes_n`
  299. if nodes_increase:
  300. podm_increase = False
  301. # number of nodes
  302. nodes_n = 5
  303. # probability of dropping events
  304. podm = 0.20
  305. # number of events each node should broadcast
  306. broadcast_attempt = 5
  307. sim_nodes_inc = int(nodes_n / 5)
  308. sim_podm_inc = podm / 5
  309. sim = []
  310. nodes_n_list = []
  311. events_synced = []
  312. podm_list = []
  313. podm_tmp = podm
  314. nodes_n_tmp = nodes_n
  315. for _ in range(sim_n):
  316. nodes = await run(nodes_n_tmp, podm_tmp, broadcast_attempt)
  317. sim.append(nodes)
  318. podm_list.append(podm_tmp)
  319. if nodes_increase:
  320. nodes_n_tmp += sim_nodes_inc
  321. if podm_increase:
  322. podm_tmp += sim_podm_inc
  323. for nodes in sim:
  324. nodes_n = len(nodes)
  325. nodes_n_list.append(nodes_n)
  326. events = Counter()
  327. for node in nodes:
  328. events.update(list(node.active_pool.events.keys()))
  329. # Remove the genesis event
  330. del events["8aed642bf5118b9d3c859bd4be35ecac75b6e873cce34e7b6f554b06f75550d7"]
  331. expect_events_synced = (nodes_n * broadcast_attempt)
  332. actual_events_synced = 0
  333. for val in events.values():
  334. # if the event is fully synced with all nodes
  335. if val == nodes_n:
  336. actual_events_synced += 1
  337. res = (actual_events_synced * 100) / expect_events_synced
  338. events_synced.append(res)
  339. info(events)
  340. info(f"nodes_n: {nodes_n}")
  341. info(f"actual_events_synced: {actual_events_synced}")
  342. info(f"expect_events_synced: {expect_events_synced}")
  343. info(f"res: %{res}")
  344. logging.disable()
  345. if nodes_increase:
  346. plt.plot(nodes_n_list, events_synced)
  347. plt.ylim(0, 100)
  348. plt.title(f"Event Graph simulation with %{podm * 100} probability of dropping messages")
  349. plt.ylabel("Events sync percentage")
  350. plt.xlabel("Number of nodes")
  351. plt.show()
  352. if podm_increase:
  353. plt.plot(podm_list, events_synced)
  354. plt.ylim(0, 100)
  355. plt.title(f"Event Graph simulation with {nodes_n} nodes")
  356. plt.ylabel("Events sync percentage")
  357. plt.xlabel("Probability of dropping messages")
  358. plt.show()
  359. def print_network_graph(nodes):
  360. for (i, node) in enumerate(nodes):
  361. graph = nx.Graph()
  362. for (h, ev) in node.active_pool.events.items():
  363. graph.add_node(h[:5])
  364. graph.add_edges_from([(h[:5], p[:5]) for p in ev.parents])
  365. colors = []
  366. node_heads = [h[:5] for h in node.heads]
  367. for n in graph.nodes():
  368. if n == "8aed6":
  369. colors.append("red")
  370. elif n in node_heads:
  371. colors.append("yellow")
  372. else:
  373. colors.append("blue")
  374. plt.figure(i)
  375. nx.draw_networkx(graph, with_labels=True, node_color=colors)
  376. plt.show()
  377. if __name__ == "__main__":
  378. logging.basicConfig(level=logging.DEBUG,
  379. handlers=[logging.FileHandler("debug.log", mode="w"),
  380. logging.StreamHandler()])
  381. asyncio.run(main(nodes_increase=True))