| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- from hashlib import sha256
- from datetime import datetime
- from random import randint, random
- from collections import Counter
- import math
- import asyncio
- import logging
- from logging import debug, error, info
- import matplotlib.pyplot as plt
- import networkx as nx
- EventId = str
- EventIds = list[EventId]
- class NetworkPool:
- def __init__(self, nodes):
- self.nodes = nodes
- def request(self, event_id: EventId):
- for n in self.nodes:
- event = n.get_event(event_id)
- if event != None:
- return event
- return None
- class Event:
- def __init__(self, parents: EventIds):
- self.timestamp = datetime.now().timestamp
- self.parents = sorted(parents)
- def set_timestamp(self, timestamp):
- self.timestamp = timestamp
- # Hash of timestamp and the parents
- def hash(self) -> str:
- m = sha256()
- m.update(str.encode(str(self.timestamp)))
- for p in self.parents:
- m.update(str.encode(str(p)))
- return m.digest().hex()
- def __str__(self):
- res = f"{self.hash()}"
- for p in self.parents:
- res += f"\n |"
- res += f"\n - {p}"
- res += f"\n"
- return res
- """
- # Graph Example
- E1: []
- E2: [E1]
- E3: [E1]
- E4: [E3]
- E5: [E3]
- E6: [E4, E5]
- E7: [E4]
- E8: [E2]
- """
- class Graph:
- def __init__(self):
- self.events = dict()
- def add_event(self, event: Event):
- self.events[event.hash()] = event
- def remove_event(self, event_id: EventId):
- if self.events.get(event_id) != None:
- del self.events[event_id]
- # Check if given events are exist in the graph
- # return a list of missing events
- def check(self, events: EventIds) -> EventIds:
- missing_events = []
- for e in events:
- if self.events.get(e) == None:
- missing_events.append(e)
- return missing_events
- def __str__(self):
- res = ""
- for event in self.events.values():
- res += f"\n {event}"
- return res
- class Node:
- def __init__(self, name: str, queue):
- self.name = name
- self.orphan_pool = Graph()
- self.active_pool = Graph()
- self.queue = queue
- # The active pool should always start with one event
- genesis_event = Event([])
- genesis_event.set_timestamp(0.0)
- self.genesis_event = genesis_event
- self.active_pool.add_event(genesis_event)
- # On the initialization make the root node as head
- self.heads = [genesis_event.hash()]
- # Remove the parents for the event if they are exist in heads
- def remove_heads(self, event):
- for p in event.parents:
- if p in self.heads:
- self.heads.remove(p)
- # Add the event to heads
- def update_heads(self, event):
- event_hash = event.hash()
- self.remove_heads(event)
- self.heads.append(event_hash)
- self.heads = sorted(self.heads)
- # On receive new event
- def receive_new_event(self, event: Event, peer, np):
- debug(f"{self.name} receive event from {peer}: \n {event}")
- event_hash = event.hash()
- # Reject event with no parents
- if not event.parents:
- return
- # Reject event already exist in active pool
- if not self.active_pool.check([event_hash]):
- return
- # Reject event already exist in orphan pool
- if not self.orphan_pool.check([event_hash]):
- return
- # Add the new event to the orphan pool
- self.orphan_pool.add_event(event)
- # This function is the core of syncing algorithm
- #
- # Find all the links from the new event to events in orphan pool
- # Bring these events to the active pool then add the new event
- self.relink_orphan(event, np)
- def relink_orphan(self, orphan, np):
- # Check if the parents of the orphan
- # are not missing from active pool
- missing_parents = self.active_pool.check(orphan.parents)
- if not missing_parents:
- self.add_to_active_pool(orphan)
- return
- # Check the missing parents from orphan pool and sync with the network for
- # missing ones
- self.check_orphan_pool(list(missing_parents), np)
- # At this stage all the missing parents must be in the orphan pool
- # The next step is to move them to active pool
- self.update_active_pool(missing_parents, [])
- # Check again that the parents of the orphan are in the active pool
- missing_parents = self.active_pool.check(orphan.parents)
- assert (not missing_parents)
- # Last stage, add the event to active pool
- self.add_to_active_pool(orphan)
- def check_orphan_pool(self, missing_events, np):
- debug(f"{self.name} check_orphan_pool() {missing_events}")
- while True:
- # Check if all missing parents are in orphan pool, otherwise
- # add them to request list
- request_list = []
- self.check_missing_parents(request_list, missing_events, [])
- if not request_list:
- break
- missing_events = self.fetch_events(request_list, np)
- def check_missing_parents(self, request_list, events: EventIds, visited):
- debug(f"{self.name} check_missing_parents() {events}")
- for event_hash in events:
- # Check if the function already visit this event
- if event_hash in visited:
- continue
- visited.append(event_hash)
- # If the event in orphan pool, do recursive call to check its
- # parents as well, otherwise add the event to request_list
- event = self.orphan_pool.events.get(event_hash)
- if event == None:
- # Check first if it's not in the active pool
- if self.active_pool.events.get(event_hash) == None:
- request_list.append(event_hash)
- else:
- # Recursive call
- # Climb up for the event parents
- self.check_missing_parents(request_list, event.parents, visited)
- def fetch_events(self, request_list, np):
- debug(f"{self.name} fetch_events() {request_list}")
- # XXX
- # Send the events in request_list to the node who send this event.
- #
- # For simulation purpose the node fetch the missed events from the
- # network pool which contains all the nodes and its events
- result = []
- for p in request_list:
- debug(f"{self.name} request from the network: {p}")
- # Request from the network
- requested_event = np.request(p)
- assert (requested_event != None)
- # Add it to the orphan pool
- self.orphan_pool.add_event(requested_event)
- result.extend(requested_event.parents)
- # Return parents of requested events
- return result
- def update_active_pool(self, events, visited):
- debug(f"{self.name} update_active_pool() {events}")
- for event_hash in events:
- # Check if it already visit this event
- if event_hash in visited:
- continue
- visited.append(event_hash)
- if self.active_pool.events.get(event_hash) != None:
- continue
- # Get the event from the orphan pool
- event = self.orphan_pool.events.get(event_hash)
- assert (event != None)
- # Add it to the active pool
- self.add_to_active_pool(event)
- # Recursive call
- # Climb up for the event parents
- self.update_active_pool(event.parents, visited)
- def add_to_active_pool(self, event):
- # Add the event to active pool
- self.active_pool.add_event(event)
- # Update heads
- self.update_heads(event)
- # Remove event from orphan pool
- self.orphan_pool.remove_event(event.hash())
- def get_event(self, event_id: EventId):
- # Check the active_pool
- event = self.active_pool.events.get(event_id)
- # Check the orphan_pool
- if event == None:
- event = self.orphan_pool.events.get(event)
- return event
- def __str__(self):
- return f"""
- \n Name: {self.name}
- \n Active Pool: {self.active_pool}
- \n Orphan Pool: {self.orphan_pool}
- \n Heads: {self.heads}"""
- # Each node has nodes_n of this function running in the background
- # for receiving events from each node separately
- async def recv_loop(podm, node, peer, queue, np):
- while True:
- # Wait new event
- event = await queue.get()
- queue.task_done()
- if event == None:
- break
- if random() < podm:
- debug(f"{node.name} dropped: \n {event}")
- continue
- node.receive_new_event(event, peer, np)
- # Send new event at random intervals
- # Each node has this function running in the background
- async def send_loop(nodes_n, max_delay, broadcast_attempt, node):
- for _ in range(broadcast_attempt):
- await asyncio.sleep(randint(0, max_delay))
- # Create new event with the last heads as parents
- event = Event(node.heads)
- debug(f"{node.name} broadcast event: \n {event}")
- for _ in range(nodes_n):
- await node.queue.put(event)
- await node.queue.join()
- """
- Run a simulation with the provided params:
- nodes_n: number of nodes
- podm: probability of dropping events (ex: 0.30 -> %30)
- broadcast_attempt: number of events each node should broadcast
- check: check if all nodes have the same graph
- """
- async def run(nodes_n=3, podm=0.30, broadcast_attempt=3, check=False):
- debug(f"Running simulation with nodes: {nodes_n}, podm: {podm},\
- broadcast_attempt: {broadcast_attempt}")
- max_delay = round(math.log(nodes_n))
- broadcast_timeout = nodes_n * broadcast_attempt * max_delay
- nodes = []
- info(f"Run {nodes_n} Nodes")
- try:
- # Initialize nodes_n nodes
- for i in range(nodes_n):
- queue = asyncio.Queue()
- node = Node(f"Node{i}", queue)
- nodes.append(node)
- # Initialize NetworkPool contains all nodes
- np = NetworkPool(nodes)
- # Initialize nodes_n * nodes_n coroutine tasks for receiving events
- # Each node listen to all queues from the running nodes
- recv_tasks = []
- for node in nodes:
- for n in nodes:
- recv_tasks.append(recv_loop(podm, node, n.name, n.queue, np))
- r_g = asyncio.gather(*recv_tasks)
- # Create coroutine task contains send_loop function for each node
- # Run and wait for send tasks
- s_g = asyncio.gather(
- *[send_loop(nodes_n, max_delay, broadcast_attempt, n) for n in nodes])
- await asyncio.wait_for(s_g, broadcast_timeout)
- # Gracefully stop all receiving tasks
- for n in nodes:
- for _ in range(nodes_n):
- await n.queue.put(None)
- await n.queue.join()
- await r_g
- if check:
- for node in nodes:
- debug(node)
- # Assert if all nodes share the same active pool graph
- assert (all(n.active_pool.events.keys() ==
- nodes[0].active_pool.events.keys() for n in nodes))
- # Assert if all nodes share the same orphan pool graph
- assert (all(n.orphan_pool.events.keys() ==
- nodes[0].orphan_pool.events.keys() for n in nodes))
- # Assert if all heads are equal
- assert (all(n.heads == nodes[0].heads for n in nodes))
- return nodes
- except asyncio.exceptions.TimeoutError:
- error("Broadcast TimeoutError")
- async def main(sim_n=6, nodes_increase=False, podm_increase=False ):
- # run the simulation `sim_n` times with increasing `podm` and `nodes_n`
- if nodes_increase:
- podm_increase = False
- # number of nodes
- nodes_n = 5
- # probability of dropping events
- podm = 0.20
- # number of events each node should broadcast
- broadcast_attempt = 5
- sim_nodes_inc = int(nodes_n / 5)
- sim_podm_inc = podm / 5
- sim = []
- nodes_n_list = []
- events_synced = []
- podm_list = []
- podm_tmp = podm
- nodes_n_tmp = nodes_n
- for _ in range(sim_n):
- nodes = await run(nodes_n_tmp, podm_tmp, broadcast_attempt)
- sim.append(nodes)
- podm_list.append(podm_tmp)
- if nodes_increase:
- nodes_n_tmp += sim_nodes_inc
- if podm_increase:
- podm_tmp += sim_podm_inc
- for nodes in sim:
- nodes_n = len(nodes)
- nodes_n_list.append(nodes_n)
- events = Counter()
- for node in nodes:
- events.update(list(node.active_pool.events.keys()))
- # Remove the genesis event
- del events["8aed642bf5118b9d3c859bd4be35ecac75b6e873cce34e7b6f554b06f75550d7"]
- expect_events_synced = (nodes_n * broadcast_attempt)
- actual_events_synced = 0
- for val in events.values():
- # if the event is fully synced with all nodes
- if val == nodes_n:
- actual_events_synced += 1
- res = (actual_events_synced * 100) / expect_events_synced
- events_synced.append(res)
- info(events)
- info(f"nodes_n: {nodes_n}")
- info(f"actual_events_synced: {actual_events_synced}")
- info(f"expect_events_synced: {expect_events_synced}")
- info(f"res: %{res}")
- logging.disable()
- if nodes_increase:
- plt.plot(nodes_n_list, events_synced)
- plt.ylim(0, 100)
- plt.title(f"Event Graph simulation with %{podm * 100} probability of dropping messages")
- plt.ylabel("Events sync percentage")
- plt.xlabel("Number of nodes")
- plt.show()
- if podm_increase:
- plt.plot(podm_list, events_synced)
- plt.ylim(0, 100)
- plt.title(f"Event Graph simulation with {nodes_n} nodes")
- plt.ylabel("Events sync percentage")
- plt.xlabel("Probability of dropping messages")
- plt.show()
- def print_network_graph(nodes):
- for (i, node) in enumerate(nodes):
- graph = nx.Graph()
- for (h, ev) in node.active_pool.events.items():
- graph.add_node(h[:5])
- graph.add_edges_from([(h[:5], p[:5]) for p in ev.parents])
- colors = []
- node_heads = [h[:5] for h in node.heads]
- for n in graph.nodes():
- if n == "8aed6":
- colors.append("red")
- elif n in node_heads:
- colors.append("yellow")
- else:
- colors.append("blue")
- plt.figure(i)
- nx.draw_networkx(graph, with_labels=True, node_color=colors)
- plt.show()
- if __name__ == "__main__":
- logging.basicConfig(level=logging.DEBUG,
- handlers=[logging.FileHandler("debug.log", mode="w"),
- logging.StreamHandler()])
- asyncio.run(main(nodes_increase=True))
|