main.py 16 KB

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