main.py 23 KB

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