main.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  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. if missing_parents:
  144. # Check the missing parents from orphan pool and sync with the
  145. # network for missing ones
  146. self.check_and_sync(list(missing_parents), np)
  147. # At this stage all the missing parents must be in the orphan pool
  148. # The next step is to move them to active pool
  149. self.add_linked_events_to_active_pool(missing_parents, [])
  150. # Check again that the parents of the orphan are in the active pool
  151. missing_parents = self.active_pool.check_events(orphan.parents)
  152. missing_parents = [p for p in missing_parents if p not in
  153. self.pruned_events]
  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. old_parents = [e for e in oe_active_p if e in event.parents]
  183. if not old_parents:
  184. continue
  185. # Add the event to tails if it has only old events as parents
  186. if len(event.parents) == len(old_parents):
  187. self.update_tails(event)
  188. def clean_orphan_pool(self):
  189. debug(f"{self.name} clean_orphan_pool()")
  190. while True:
  191. active_list = []
  192. old_events = []
  193. for oh, orphan in self.orphan_pool.events.items():
  194. if self.is_old_event(orphan):
  195. old_events.append(oh)
  196. continue
  197. # Move the orphan to active pool if it doesn't have missing
  198. # parents in active pool
  199. missing_parents = self.active_pool.check_events(orphan.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. if event_hash in self.pruned_events:
  235. continue
  236. request_list.append(event_hash)
  237. else:
  238. # Recursive call
  239. # Climb up for the event parents
  240. self.scan_orphan_pool(request_list, event.parents, visited)
  241. def fetch_events_from_network(self, request_list, np):
  242. debug(f"{self.name} fetch_events() {request_list}")
  243. # XXX
  244. # Send the events in request_list to the node who send this event.
  245. #
  246. # For simulation purpose the node fetch the missed events from the
  247. # network pool which contains all the nodes and its events
  248. result = []
  249. for p in request_list:
  250. debug(f"{self.name} request from the network: {p}")
  251. # Request from the network
  252. requested_event = np.request(p)
  253. if requested_event == None:
  254. if p not in self.pruned_events:
  255. self.pruned_events.append(p)
  256. continue
  257. # Add it to the orphan pool
  258. self.orphan_pool.add_event(requested_event)
  259. self.unpruned_orphan_pool.add_event(requested_event)
  260. result.extend(requested_event.parents)
  261. # Return parents of requested events
  262. return result
  263. def add_linked_events_to_active_pool(self, events, visited):
  264. debug(f"{self.name} add_linked_events_to_active_pool() {events}")
  265. for event_hash in events:
  266. # Check if it already visit this event
  267. if event_hash in visited:
  268. continue
  269. visited.append(event_hash)
  270. if self.active_pool.events.get(event_hash) != None:
  271. continue
  272. if event_hash in self.pruned_events:
  273. continue
  274. # Get the event from the orphan pool
  275. event = self.orphan_pool.events.get(event_hash)
  276. assert (event != None)
  277. # Add it to the active pool
  278. self.add_to_active_pool(event)
  279. # Recursive call
  280. # Climb up for the event parents
  281. self.add_linked_events_to_active_pool(event.parents, visited)
  282. def add_to_active_pool(self, event):
  283. # Add the event to active pool
  284. self.active_pool.add_event(event)
  285. self.unpruned_active_pool.add_event(event)
  286. # Update heads
  287. self.update_heads(event)
  288. # Remove event from orphan pool
  289. self.orphan_pool.remove_event(event.hash())
  290. self.unpruned_orphan_pool.remove_event(event.hash())
  291. # Get an event from orphan pool or active pool
  292. def get_event(self, event_id: EventId):
  293. # Check the active_pool
  294. event = self.active_pool.events.get(event_id)
  295. # Check the orphan_pool
  296. if event == None:
  297. event = self.orphan_pool.events.get(event)
  298. return event
  299. # Check if the event is too old from now, by subtracting current timestamp
  300. # from event timestamp, it must be more than `max_time_diff' to be consider
  301. # old event
  302. def is_old_event(self, event: Event):
  303. # Ignore genesis event
  304. if event.timestamp == 0.0:
  305. return False
  306. current_timestamp = ntp_request()
  307. diff = current_timestamp - event.timestamp
  308. if diff > self.max_time_diff:
  309. return True
  310. return False
  311. def __str__(self):
  312. return f"""
  313. \n Name: {self.name}
  314. \n Active Pool: {self.active_pool}
  315. \n Orphan Pool: {self.orphan_pool}
  316. \n Pruned Events: {self.pruned_events}
  317. \n Heads: {self.heads}
  318. \n Tails: {self.tails}"""
  319. # Each node has `nodes_n` of this function running in the background
  320. # for receiving events from each node separately
  321. async def recv_loop(podm, node, peer, queue, np):
  322. while True:
  323. # Wait new event
  324. event = await queue.get()
  325. queue.task_done()
  326. if event == None:
  327. break
  328. if random() < podm:
  329. debug(f"{node.name} dropped: \n {event}")
  330. continue
  331. node.receive_new_event(event, peer, np)
  332. # Send new event at random intervals
  333. # Each node has this function running in the background
  334. async def send_loop(nodes_n, max_delay, broadcast_attempt, node):
  335. for _ in range(broadcast_attempt):
  336. await asyncio.sleep(randint(0, max_delay))
  337. # Create new event with the last heads as parents
  338. event = node.new_event()
  339. debug(f"{node.name} broadcast event: \n {event}")
  340. for _ in range(nodes_n):
  341. await node.queue.put(event)
  342. await node.queue.join()
  343. """
  344. Run a simulation with the provided params:
  345. nodes_n: number of nodes
  346. podm: probability of dropping events (ex: 0.30 -> %30)
  347. broadcast_attempt: number of events each node should broadcast
  348. max_time_diff: a max difference in time to detect an old event
  349. check: check if all nodes have the same graph
  350. """
  351. async def run(nodes_n=3, podm=0.30, broadcast_attempt=3, max_time_diff=180.0,
  352. check=False, max_delay=None):
  353. debug(f"Running simulation with nodes: {nodes_n}, podm: {podm},\
  354. broadcast_attempt: {broadcast_attempt}")
  355. if max_delay == None:
  356. max_delay = round(math.log(nodes_n))
  357. broadcast_timeout = nodes_n * broadcast_attempt * max_delay
  358. nodes = []
  359. info(f"Run {nodes_n} Nodes")
  360. try:
  361. # Initialize `nodes_n` nodes
  362. for i in range(nodes_n):
  363. queue = asyncio.Queue()
  364. node = Node(f"Node{i}", queue, max_time_diff)
  365. nodes.append(node)
  366. # Initialize NetworkPool contains all nodes
  367. np = NetworkPool(nodes)
  368. # Initialize `nodes_n` * `nodes_n` coroutine tasks for receiving events
  369. # Each node listen to all queues from the running nodes
  370. recv_tasks = []
  371. for node in nodes:
  372. for n in nodes:
  373. recv_tasks.append(recv_loop(podm, node, n.name, n.queue, np))
  374. r_g = asyncio.gather(*recv_tasks)
  375. # Create coroutine task contains send_loop function for each node
  376. # Run and wait for send tasks
  377. s_g = asyncio.gather(
  378. *[send_loop(nodes_n, max_delay, broadcast_attempt, n) for n in nodes])
  379. await asyncio.wait_for(s_g, broadcast_timeout)
  380. # Gracefully stop all receiving tasks
  381. for n in nodes:
  382. for _ in range(nodes_n):
  383. await n.queue.put(None)
  384. await n.queue.join()
  385. await r_g
  386. if check:
  387. for node in nodes:
  388. debug(node)
  389. # Assert if all nodes share the same active pool graph
  390. assert (all(n.active_pool.events.keys() ==
  391. nodes[0].active_pool.events.keys() for n in nodes))
  392. # Assert if all nodes share the same orphan pool graph
  393. assert (all(n.orphan_pool.events.keys() ==
  394. nodes[0].orphan_pool.events.keys() for n in nodes))
  395. # Assert if all heads are equal
  396. assert (all(n.heads == nodes[0].heads for n in nodes))
  397. # Assert if all tails are equal
  398. assert (all(n.tails == nodes[0].tails for n in nodes))
  399. return nodes
  400. except asyncio.exceptions.TimeoutError:
  401. error("Broadcast TimeoutError")
  402. async def main(sim_n=6, nodes_increase=False, podm_increase=False,
  403. time_diff_decrease=False):
  404. # run the simulation `sim_n` times, while enabling one of these params:
  405. # - increasing `podm`
  406. # - increasing `nodes_n`
  407. # - decreasing `max_time_diff`
  408. if nodes_increase:
  409. podm_increase = False
  410. time_diff_decrease = False
  411. if podm_increase:
  412. time_diff_decrease = False
  413. # number of nodes
  414. nodes_n = 100
  415. # probability of dropping events
  416. podm = 0.0
  417. # a max difference in time to detect an old event
  418. max_time_diff = 60 # seconds
  419. # number of events each node should broadcast
  420. broadcast_attempt = 10
  421. # Number of nodes get increase in each simulation
  422. sim_nodes_inc = int(nodes_n / 5)
  423. # A value get add to `podm` in each simulation
  424. sim_podm_inc = podm / 5
  425. # A value get subtract from `max_time_diff` in each simulation
  426. sim_diff_time_dec = max_time_diff / 10
  427. # Contains the nodes for each simulation
  428. simulations = []
  429. # Contains the `podm` variables for each simulation
  430. podm_list = []
  431. # Contains the `max_time_diff` variables for each simulation
  432. mtd_list = []
  433. podm_tmp = podm
  434. nodes_n_tmp = nodes_n
  435. time_diff_tmp = max_time_diff
  436. for _ in range(sim_n):
  437. nodes = await run(nodes_n_tmp, podm_tmp, broadcast_attempt,
  438. max_time_diff=time_diff_tmp)
  439. simulations.append(nodes)
  440. podm_list.append(podm_tmp)
  441. mtd_list.append(time_diff_tmp)
  442. if nodes_increase:
  443. nodes_n_tmp += sim_nodes_inc
  444. if podm_increase:
  445. podm_tmp += sim_podm_inc
  446. if time_diff_decrease:
  447. time_diff_tmp -= sim_diff_time_dec
  448. # Numbers of nodes for each simulation
  449. nodes_n_list = []
  450. # Synced events percentage for each simulation
  451. events_synced_perc = []
  452. # Number of events in active pool for each simulations
  453. active_events = []
  454. # Number of events in pruned events list for each simulations
  455. pruned_events = []
  456. for nodes in simulations:
  457. nodes_n = len(nodes)
  458. nodes_n_list.append(nodes_n)
  459. events = Counter()
  460. p_events = Counter()
  461. for node in nodes:
  462. events.update(list(node.active_pool.events.keys()))
  463. p_events.update(node.pruned_events)
  464. expect_events_synced = (nodes_n * broadcast_attempt) + 1
  465. actual_events_synced = 0
  466. for val in events.values():
  467. # If the event is fully synced with all nodes
  468. if val == nodes_n:
  469. actual_events_synced += 1
  470. pruned_events_synced = 0
  471. for val in p_events.values():
  472. # If the pruned event is fully synced with all nodes
  473. if val == nodes_n:
  474. pruned_events_synced += 1
  475. res = (actual_events_synced * 100) / expect_events_synced
  476. events_synced_perc.append(res)
  477. active_events.append(actual_events_synced)
  478. pruned_events.append(pruned_events_synced)
  479. info(f"nodes_n: {nodes_n}")
  480. info(f"actual_events_synced: {actual_events_synced}")
  481. info(f"expect_events_synced: {expect_events_synced}")
  482. info(f"pruned_events_synced: {pruned_events_synced}")
  483. info(f"res: %{res}")
  484. # Disable logging for matplotlib
  485. logging.disable()
  486. if nodes_increase:
  487. plt.plot(nodes_n_list, events_synced_perc)
  488. plt.ylim(0, 100)
  489. plt.title(
  490. f"Event Graph simulation with %{podm * 100} probability of dropping messages")
  491. plt.ylabel(
  492. f"Events sync percentage (each node broadcast {broadcast_attempt} events)")
  493. plt.xlabel("Number of nodes")
  494. plt.show()
  495. return
  496. if podm_increase:
  497. plt.plot(podm_list, events_synced_perc)
  498. plt.ylim(0, 100)
  499. plt.title(f"Event Graph simulation with {nodes_n} nodes")
  500. plt.ylabel(
  501. f"Events sync percentage (each node broadcast {broadcast_attempt} events)")
  502. plt.xlabel("Probability of dropping messages")
  503. plt.show()
  504. return
  505. if time_diff_decrease:
  506. x = np.arange(len(mtd_list)) # the label locations
  507. width = 0.35 # the width of the bars
  508. fig, ax = plt.subplots()
  509. rects1 = ax.bar(x - width/2, active_events, width, label='Active')
  510. rects2 = ax.bar(x + width/2, pruned_events, width, label='Pruned')
  511. plt.title(
  512. f"Event Graph simulation with %{podm * 100} probability of dropping messages, and {nodes_n} nodes")
  513. plt.ylabel(
  514. f"Number of events broadcasted during the simulation (each node broadcast {broadcast_attempt} events)")
  515. plt.xlabel("A time duration to detect old events (in seconds)")
  516. plt.ylim(0, (broadcast_attempt * nodes_n) + 1)
  517. ax.set_xticks(x, mtd_list)
  518. ax.legend()
  519. ax.bar_label(rects1, padding=3)
  520. ax.bar_label(rects2, padding=3)
  521. fig.tight_layout()
  522. plt.show()
  523. return
  524. def print_network_graph(node, unpruned=False):
  525. logging.disable()
  526. graph = nx.Graph()
  527. if unpruned:
  528. for (h, ev) in node.unpruned_active_pool.events.items():
  529. graph.add_node(h[:5])
  530. graph.add_edges_from([(h[:5], p[:5]) for p in ev.parents])
  531. else:
  532. for (h, ev) in node.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. colors = []
  536. for n in graph.nodes():
  537. if any(n == t[:5] for t in node.tails):
  538. colors.append("red")
  539. elif any(n == h[:5] for h in node.heads):
  540. colors.append("yellow")
  541. else:
  542. colors.append("#697aff")
  543. nx.draw_networkx(graph, with_labels=True, node_color=colors)
  544. plt.show()
  545. if __name__ == "__main__":
  546. logging.basicConfig(level=logging.DEBUG,
  547. handlers=[logging.FileHandler("debug.log", mode="w"),
  548. logging.StreamHandler()])
  549. # nodes = asyncio.run(run(nodes_n=10, podm=0, broadcast_attempt=4,
  550. # max_time_diff=30, max_delay=7))
  551. # print_network_graph(nodes[0])
  552. # print_network_graph(nodes[0], unpruned=True)
  553. asyncio.run(main(time_diff_decrease=True))