main.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. from hashlib import sha256
  2. from random import randint, random, getrandbits
  3. from collections import Counter
  4. import time
  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. import numpy as np
  12. EventId = str
  13. EventIds = list[EventId]
  14. def ntp_request() -> float:
  15. # add random clock drift
  16. if bool(getrandbits(1)):
  17. return time.time() + randint(0, 10)
  18. else:
  19. return time.time() - randint(0, 10)
  20. class NetworkPool:
  21. def __init__(self, nodes):
  22. self.nodes = nodes
  23. def request(self, event_id: EventId):
  24. for n in self.nodes:
  25. event = n.get_event(event_id)
  26. if event != None:
  27. return event
  28. return None
  29. class Event:
  30. def __init__(self, parents: EventIds):
  31. self.timestamp = ntp_request()
  32. self.parents = sorted(parents)
  33. def set_timestamp(self, timestamp):
  34. self.timestamp = timestamp
  35. # Hash of timestamp and the parents
  36. def hash(self) -> str:
  37. m = sha256()
  38. m.update(str.encode(str(self.timestamp)))
  39. for p in self.parents:
  40. m.update(str.encode(str(p)))
  41. return m.digest().hex()
  42. def __str__(self):
  43. res = f"{self.hash()}"
  44. for p in self.parents:
  45. res += f"\n |"
  46. res += f"\n - {p}"
  47. res += f"\n"
  48. return res
  49. class Graph:
  50. def __init__(self, max_time_diff):
  51. self.events = dict()
  52. self.heads = []
  53. self.tails = []
  54. self.max_time_diff = max_time_diff
  55. def add_event(self, event: Event):
  56. event_id = event.hash()
  57. if self.events.get(event_id) != None:
  58. return
  59. self.events[event_id] = event
  60. self.update_heads(event)
  61. self.update_tails(event)
  62. def update_tails(self, event):
  63. event_hash = event.hash()
  64. if event_hash in self.tails:
  65. return
  66. # Remove tails if they are parents of the given event
  67. for p in event.parents:
  68. if p in self.tails:
  69. self.tails.remove(p)
  70. # Add the event to tails
  71. self.tails.append(event_hash)
  72. self.tails = sorted(self.tails)
  73. def update_heads(self, event):
  74. event_hash = event.hash()
  75. if event_hash in self.heads:
  76. return
  77. # Remove heads if they are parents of the given event
  78. for p in event.parents:
  79. if p in self.heads:
  80. self.heads.remove(p)
  81. # Add the event to heads
  82. self.heads.append(event_hash)
  83. self.heads = sorted(self.heads)
  84. # Check if the event is too old from now, by subtracting current timestamp
  85. # from event timestamp, it must be more than `max_time_diff' to be consider
  86. # old event
  87. def is_old_event(self, event: Event):
  88. # Ignore genesis event
  89. if event.timestamp == 0.0:
  90. return False
  91. current_timestamp = ntp_request()
  92. diff = current_timestamp - event.timestamp
  93. if diff > self.max_time_diff:
  94. return True
  95. return False
  96. def prune_old_events(self):
  97. # Find the old events
  98. old_events = [eh for eh, ev in self.events.items() if
  99. self.is_old_event(ev)]
  100. # Remove the old events
  101. for eh in old_events:
  102. self.remove_event(eh)
  103. def remove_event(self, eh: EventId):
  104. self.events.pop(eh, None)
  105. # Remove old events from heads
  106. if eh in self.heads:
  107. self.heads.remove(eh)
  108. # Remove old events from tails
  109. if eh in self.tails:
  110. self.tails.remove(eh)
  111. # Check if given events are exist in the graph
  112. # return a list of missing events
  113. def check_events(self, events: EventIds) -> EventIds:
  114. return [e for e in events if self.events.get(e) == None]
  115. def __str__(self):
  116. res = ""
  117. for event in self.events.values():
  118. res += f"\n {event}"
  119. return res
  120. class Node:
  121. def __init__(self, name: str, queue, max_time_diff):
  122. self.name = name
  123. self.orphan_pool = Graph(max_time_diff)
  124. self.active_pool = Graph(max_time_diff)
  125. self.queue = queue
  126. # Pruned events from active pool
  127. self.pruned_events = []
  128. # The active pool should always start with one event
  129. genesis_event = Event([])
  130. genesis_event.set_timestamp(0.0)
  131. self.active_pool.add_event(genesis_event)
  132. # On create new event
  133. def new_event(self):
  134. # Pruning old events from active pool
  135. self.active_pool.prune_old_events()
  136. return Event(self.active_pool.heads)
  137. # On receive new event
  138. def receive_new_event(self, event: Event, peer, np):
  139. debug(f"{self.name} receive event from {peer}: \n {event}")
  140. event_hash = event.hash()
  141. # Reject event with no parents
  142. if not event.parents:
  143. return
  144. # XXX Reject old event
  145. # no need for this simulation
  146. # if self.is_old_event(event):
  147. # return
  148. # Reject event already exist in active pool
  149. if not self.active_pool.check_events([event_hash]):
  150. return
  151. # Reject event already exist in orphan pool
  152. if not self.orphan_pool.check_events([event_hash]):
  153. return
  154. # Add the new event to the orphan pool
  155. self.orphan_pool.add_event(event)
  156. # This function is the core of syncing algorithm
  157. #
  158. # Find all the links from the new event to events in orphan pool
  159. # Bring these events to the active pool then add the new event
  160. self.relink_orphan(event, np)
  161. def relink_orphan(self, orphan, np):
  162. # Check if the parents of the orphan
  163. # are not missing from active pool
  164. missing_parents = self.active_pool.check_events(orphan.parents)
  165. missing_parents = self.check_pruned_events(missing_parents)
  166. if missing_parents:
  167. # Check the missing parents from orphan pool and sync with the
  168. # network for missing ones
  169. self.check_and_sync(list(missing_parents), np)
  170. # At this stage all the missing parents must be in the orphan pool
  171. # The next step is to move them to active pool
  172. self.add_linked_events_to_active_pool(missing_parents, [])
  173. # Check again that the parents of the orphan are in the active pool
  174. missing_parents = self.active_pool.check_events(orphan.parents)
  175. missing_parents = self.check_pruned_events(missing_parents)
  176. assert (not missing_parents)
  177. # Add the event to active pool
  178. self.activate_event(orphan)
  179. else:
  180. self.activate_event(orphan)
  181. # Last stage, Cleaning up the orphan pool:
  182. # - Remove orphan if it is too old according to `max_time_diff`
  183. # - Move orphan to active pool if it doesn't have any missing parents
  184. self.clean_pools()
  185. def clean_pools(self):
  186. self.active_pool.prune_old_events()
  187. for event in self.active_pool.events.values():
  188. # Check if the event parents are old events
  189. old_parents = self.active_pool.check_events(event.parents)
  190. if not old_parents:
  191. continue
  192. # Add the event to tails if it has only old events as parents
  193. self.active_pool.update_tails(event)
  194. self.orphan_pool.prune_old_events()
  195. while True:
  196. active_list = []
  197. for orphan in self.orphan_pool.events.values():
  198. # Move the orphan to active pool if it doesn't have missing
  199. # parents in active pool
  200. missing_parents = self.active_pool.check_events(orphan.parents)
  201. if not missing_parents:
  202. active_list.append(orphan)
  203. if not active_list:
  204. break
  205. for ev in active_list:
  206. self.activate_event(ev)
  207. def check_and_sync(self, missing_events, np):
  208. debug(f"{self.name} check_and_sync() {missing_events}")
  209. while True:
  210. # Check if all missing parents are in orphan pool, otherwise
  211. # add them to request list
  212. request_list = []
  213. self.scan_orphan_pool(request_list, missing_events, [])
  214. if not request_list:
  215. break
  216. missing_events = self.fetch_events_from_network(request_list, np)
  217. # Check the missing links inside orphan pool
  218. def scan_orphan_pool(self, request_list, events: EventIds, visited):
  219. debug(f"{self.name} check_missing_parents() {events}")
  220. for event_hash in events:
  221. # Check if the function already visit this event
  222. if event_hash in visited:
  223. continue
  224. visited.append(event_hash)
  225. # If the event in orphan pool, do recursive call to check its
  226. # parents as well, otherwise add the event to request_list
  227. event = self.orphan_pool.events.get(event_hash)
  228. if event == None:
  229. # Check first if it's not in the active pool
  230. if self.active_pool.events.get(event_hash) != None:
  231. continue
  232. # Check if it's not in pruned events
  233. if event_hash in self.pruned_events:
  234. continue
  235. request_list.append(event_hash)
  236. else:
  237. # Recursive call
  238. # Climb up for the event parents
  239. self.scan_orphan_pool(request_list, event.parents, visited)
  240. def fetch_events_from_network(self, request_list, np):
  241. debug(f"{self.name} fetch_events() {request_list}")
  242. # XXX
  243. # Send the events in request_list to the node who send this event.
  244. #
  245. # For simulation purpose the node fetch the missed events from the
  246. # network pool which contains all the nodes and its events
  247. result = []
  248. for p in request_list:
  249. debug(f"{self.name} request from the network: {p}")
  250. # Request from the network
  251. requested_event = np.request(p)
  252. if requested_event == None:
  253. if p not in self.pruned_events:
  254. self.pruned_events.append(p)
  255. continue
  256. # Add it to the orphan pool
  257. self.orphan_pool.add_event(requested_event)
  258. result.extend(requested_event.parents)
  259. # Return parents of requested events
  260. return result
  261. def add_linked_events_to_active_pool(self, events, visited):
  262. debug(f"{self.name} add_linked_events_to_active_pool() {events}")
  263. for event_hash in events:
  264. # Check if it already visit this event
  265. if event_hash in visited:
  266. continue
  267. visited.append(event_hash)
  268. if self.active_pool.events.get(event_hash) != None:
  269. continue
  270. if event_hash in self.pruned_events:
  271. continue
  272. # Get the event from the orphan pool
  273. event = self.orphan_pool.events.get(event_hash)
  274. assert (event != None)
  275. # Add it to the active pool
  276. self.activate_event(event)
  277. # Recursive call
  278. # Climb up for the event parents
  279. self.add_linked_events_to_active_pool(event.parents, visited)
  280. def activate_event(self, event):
  281. # Add the event to active pool
  282. self.active_pool.add_event(event)
  283. # Remove event from orphan pool
  284. self.orphan_pool.remove_event(event.hash())
  285. # Get an event from orphan pool or active pool
  286. def get_event(self, event_id: EventId):
  287. # Check the active_pool
  288. event = self.active_pool.events.get(event_id)
  289. # Check the orphan_pool
  290. if event == None:
  291. event = self.orphan_pool.events.get(event)
  292. return event
  293. # Clean up the given events from pruned events
  294. def check_pruned_events(self, events):
  295. return [ev for ev in events if ev not in self.pruned_events]
  296. def __str__(self):
  297. return f"""
  298. \n Name: {self.name}
  299. \n Active Pool: {self.active_pool}
  300. \n Orphan Pool: {self.orphan_pool}"""
  301. # Each node has `nodes_n` of this function running in the background
  302. # for receiving events from each node separately
  303. async def recv_loop(podm, node, peer, queue, np):
  304. while True:
  305. # Wait new event
  306. event = await queue.get()
  307. queue.task_done()
  308. if event == None:
  309. break
  310. if random() < podm:
  311. debug(f"{node.name} dropped: \n {event}")
  312. continue
  313. node.receive_new_event(event, peer, np)
  314. # Send new event at random intervals
  315. # Each node has this function running in the background
  316. async def send_loop(nodes_n, max_delay, broadcast_attempt, node):
  317. for _ in range(broadcast_attempt):
  318. await asyncio.sleep(randint(0, max_delay))
  319. # Create new event with the last heads as parents
  320. event = node.new_event()
  321. debug(f"{node.name} broadcast event: \n {event}")
  322. for _ in range(nodes_n):
  323. await node.queue.put(event)
  324. await node.queue.join()
  325. """
  326. Run a simulation with the provided params:
  327. nodes_n: number of nodes
  328. podm: probability of dropping events (ex: 0.30 -> %30)
  329. broadcast_attempt: number of events each node should broadcast
  330. max_time_diff: a max difference in time to detect an old event
  331. check: check if all nodes have the same graph
  332. """
  333. async def run(nodes_n=3, podm=0.30, broadcast_attempt=3, max_time_diff=180.0,
  334. check=False, max_delay=None):
  335. debug(f"Running simulation with nodes: {nodes_n}, podm: {podm},\
  336. broadcast_attempt: {broadcast_attempt}")
  337. if max_delay == None:
  338. max_delay = round(math.log(nodes_n))
  339. broadcast_timeout = nodes_n * broadcast_attempt * max_delay
  340. nodes = []
  341. info(f"Run {nodes_n} Nodes")
  342. try:
  343. # Initialize `nodes_n` nodes
  344. for i in range(nodes_n):
  345. queue = asyncio.Queue()
  346. node = Node(f"Node{i}", queue, max_time_diff)
  347. nodes.append(node)
  348. # Initialize NetworkPool contains all nodes
  349. np = NetworkPool(nodes)
  350. # Initialize `nodes_n` * `nodes_n` coroutine tasks for receiving events
  351. # Each node listen to all queues from the running nodes
  352. recv_tasks = []
  353. for node in nodes:
  354. for n in nodes:
  355. recv_tasks.append(recv_loop(podm, node, n.name, n.queue, np))
  356. r_g = asyncio.gather(*recv_tasks)
  357. # Create coroutine task contains send_loop function for each node
  358. # Run and wait for send tasks
  359. s_g = asyncio.gather(
  360. *[send_loop(nodes_n, max_delay, broadcast_attempt, n) for n in nodes])
  361. await asyncio.wait_for(s_g, broadcast_timeout)
  362. # Gracefully stop all receiving tasks
  363. for n in nodes:
  364. for _ in range(nodes_n):
  365. await n.queue.put(None)
  366. await n.queue.join()
  367. await r_g
  368. if check:
  369. for node in nodes:
  370. debug(node)
  371. # Assert if all nodes share the same active pool graph
  372. assert (all(n.active_pool.events.keys() ==
  373. nodes[0].active_pool.events.keys() for n in nodes))
  374. # Assert if all nodes share the same orphan pool graph
  375. assert (all(n.orphan_pool.events.keys() ==
  376. nodes[0].orphan_pool.events.keys() for n in nodes))
  377. return nodes
  378. except asyncio.exceptions.TimeoutError:
  379. error("Broadcast TimeoutError")
  380. async def main(sim_n=6, nodes_increase=False, podm_increase=False,
  381. time_diff_decrease=False):
  382. # run the simulation `sim_n` times, while enabling one of these params:
  383. # - increasing `podm`
  384. # - increasing `nodes_n`
  385. # - decreasing `max_time_diff`
  386. if nodes_increase:
  387. podm_increase = False
  388. time_diff_decrease = False
  389. if podm_increase:
  390. time_diff_decrease = False
  391. # number of nodes
  392. nodes_n = 100
  393. # probability of dropping events
  394. podm = 0.0
  395. # a max difference in time to detect an old event
  396. max_time_diff = 60 # seconds
  397. # number of events each node should broadcast
  398. broadcast_attempt = 10
  399. # Number of nodes get increase in each simulation
  400. sim_nodes_inc = int(nodes_n / 5)
  401. # A value get add to `podm` in each simulation
  402. sim_podm_inc = podm / 5
  403. # A value get subtract from `max_time_diff` in each simulation
  404. sim_diff_time_dec = max_time_diff / 10
  405. # Contains the nodes for each simulation
  406. simulations = []
  407. # Contains the `podm` variables for each simulation
  408. podm_list = []
  409. # Contains the `max_time_diff` variables for each simulation
  410. mtd_list = []
  411. podm_tmp = podm
  412. nodes_n_tmp = nodes_n
  413. time_diff_tmp = max_time_diff
  414. for _ in range(sim_n):
  415. nodes = await run(nodes_n_tmp, podm_tmp, broadcast_attempt,
  416. max_time_diff=time_diff_tmp)
  417. simulations.append(nodes)
  418. podm_list.append(podm_tmp)
  419. mtd_list.append(time_diff_tmp)
  420. if nodes_increase:
  421. nodes_n_tmp += sim_nodes_inc
  422. if podm_increase:
  423. podm_tmp += sim_podm_inc
  424. if time_diff_decrease:
  425. time_diff_tmp -= sim_diff_time_dec
  426. # Numbers of nodes for each simulation
  427. nodes_n_list = []
  428. # Synced events percentage for each simulation
  429. events_synced_perc = []
  430. # Number of events in active pool for each simulations
  431. active_events = []
  432. # Number of events in pruned events list for each simulations
  433. pruned_events = []
  434. for nodes in simulations:
  435. nodes_n = len(nodes)
  436. nodes_n_list.append(nodes_n)
  437. events = Counter()
  438. p_events = Counter()
  439. for node in nodes:
  440. events.update(list(node.active_pool.events.keys()))
  441. p_events.update(node.pruned_events)
  442. expect_events_synced = (nodes_n * broadcast_attempt) + 1
  443. actual_events_synced = 0
  444. for val in events.values():
  445. # If the event is fully synced with all nodes
  446. if val == nodes_n:
  447. actual_events_synced += 1
  448. pruned_events_synced = 0
  449. for val in p_events.values():
  450. # If the pruned event is fully synced with all nodes
  451. if val == nodes_n:
  452. pruned_events_synced += 1
  453. res = (actual_events_synced * 100) / expect_events_synced
  454. events_synced_perc.append(res)
  455. active_events.append(actual_events_synced)
  456. pruned_events.append(pruned_events_synced)
  457. info(f"nodes_n: {nodes_n}")
  458. info(f"actual_events_synced: {actual_events_synced}")
  459. info(f"expect_events_synced: {expect_events_synced}")
  460. info(f"pruned_events_synced: {pruned_events_synced}")
  461. info(f"res: %{res}")
  462. # Disable logging for matplotlib
  463. logging.disable()
  464. if nodes_increase:
  465. plt.plot(nodes_n_list, events_synced_perc)
  466. plt.ylim(0, 100)
  467. plt.title(
  468. f"Event Graph simulation with %{podm * 100} probability of dropping messages")
  469. plt.ylabel(
  470. f"Events sync percentage (each node broadcast {broadcast_attempt} events)")
  471. plt.xlabel("Number of nodes")
  472. plt.show()
  473. return
  474. if podm_increase:
  475. plt.plot(podm_list, events_synced_perc)
  476. plt.ylim(0, 100)
  477. plt.title(f"Event Graph simulation with {nodes_n} nodes")
  478. plt.ylabel(
  479. f"Events sync percentage (each node broadcast {broadcast_attempt} events)")
  480. plt.xlabel("Probability of dropping messages")
  481. plt.show()
  482. return
  483. if time_diff_decrease:
  484. x = np.arange(len(mtd_list)) # the label locations
  485. width = 0.35 # the width of the bars
  486. fig, ax = plt.subplots()
  487. rects1 = ax.bar(x - width/2, active_events, width, label='Active')
  488. rects2 = ax.bar(x + width/2, pruned_events, width, label='Pruned')
  489. plt.title(
  490. f"Event Graph simulation with %{podm * 100} probability of dropping messages, and {nodes_n} nodes")
  491. plt.ylabel(
  492. f"Number of events broadcasted during the simulation (each node broadcast {broadcast_attempt} events)")
  493. plt.xlabel("A time duration to detect old events (in seconds)")
  494. plt.ylim(0, (broadcast_attempt * nodes_n) + 1)
  495. ax.set_xticks(x, mtd_list)
  496. ax.legend()
  497. ax.bar_label(rects1, padding=3)
  498. ax.bar_label(rects2, padding=3)
  499. fig.tight_layout()
  500. plt.show()
  501. return
  502. def print_network_graph(node, unpruned=False):
  503. logging.disable()
  504. graph = nx.Graph()
  505. if unpruned:
  506. for (h, ev) in node.unpruned_active_pool.events.items():
  507. graph.add_node(h[:5])
  508. graph.add_edges_from([(h[:5], p[:5]) for p in ev.parents])
  509. else:
  510. for (h, ev) in node.active_pool.events.items():
  511. graph.add_node(h[:5])
  512. graph.add_edges_from([(h[:5], p[:5]) for p in ev.parents])
  513. colors = []
  514. for n in graph.nodes():
  515. if any(n == t[:5] for t in node.tails):
  516. colors.append("red")
  517. elif any(n == h[:5] for h in node.heads):
  518. colors.append("yellow")
  519. else:
  520. colors.append("#697aff")
  521. nx.draw_networkx(graph, with_labels=True, node_color=colors)
  522. plt.show()
  523. if __name__ == "__main__":
  524. logging.basicConfig(level=logging.DEBUG,
  525. handlers=[logging.FileHandler("debug.log", mode="w"),
  526. logging.StreamHandler()])
  527. #nodes = asyncio.run(run(nodes_n=14, podm=0, broadcast_attempt=4,
  528. # max_time_diff=30,check=True))
  529. # print_network_graph(nodes[0])
  530. # print_network_graph(nodes[0], unpruned=True)
  531. # asyncio.run(main(time_diff_decrease=True))