From 66e5549fdc4aa19eec3d19864913ba55c0e75b51 Mon Sep 17 00:00:00 2001 From: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com> Date: Fri, 17 May 2024 14:54:23 +0900 Subject: [PATCH] refactor: subclasses for Config --- mixnet/v2/sim/adversary.py | 4 +- mixnet/v2/sim/analysis.py | 2 +- mixnet/v2/sim/config.py | 80 ++++++++++++++++++++++++++----------- mixnet/v2/sim/config.yaml | 53 +++++++++++++----------- mixnet/v2/sim/node.py | 16 ++++---- mixnet/v2/sim/p2p.py | 2 +- mixnet/v2/sim/simulation.py | 4 +- 7 files changed, 101 insertions(+), 60 deletions(-) diff --git a/mixnet/v2/sim/adversary.py b/mixnet/v2/sim/adversary.py index f9a0bf6..af7759c 100644 --- a/mixnet/v2/sim/adversary.py +++ b/mixnet/v2/sim/adversary.py @@ -34,9 +34,9 @@ class Adversary: def is_around_message_interval(self, time: SimTime): now_frac, now_int = math.modf(time) - return now_int % self.config.message_interval == 0 and now_frac <= self.config.max_message_prep_time + return now_int % self.config.mixnet.message_interval == 0 and now_frac <= self.config.mixnet.max_message_prep_time def update_observation_window(self): while True: self.mixed_msgs_per_window.append(defaultdict(int)) - yield self.env.timeout(self.config.io_observation_window) + yield self.env.timeout(self.config.adversary.io_observation_window) diff --git a/mixnet/v2/sim/analysis.py b/mixnet/v2/sim/analysis.py index afdfb1a..ba175d8 100644 --- a/mixnet/v2/sim/analysis.py +++ b/mixnet/v2/sim/analysis.py @@ -20,7 +20,7 @@ class Analysis: def messages_emitted_around_interval(self): df = pd.DataFrame( - [(node.id, cnt, node.id < len(self.sim.config.real_message_prob_weights)) + [(node.id, cnt, node.id < len(self.sim.config.mixnet.real_message_prob_weights)) for node, cnt in self.sim.p2p.adversary.senders_around_interval.items()], columns=["node_id", "msg_count", "expected"] ) diff --git a/mixnet/v2/sim/config.py b/mixnet/v2/sim/config.py index 0f51324..283ab07 100644 --- a/mixnet/v2/sim/config.py +++ b/mixnet/v2/sim/config.py @@ -1,3 +1,4 @@ +from __future__ import annotations from dataclasses import dataclass from typing import Self @@ -7,7 +8,36 @@ import yaml @dataclass class Config: + simulation: SimulationConfig + mixnet: MixnetConfig + p2p: P2pConfig + adversary: AdversaryConfig + + @classmethod + def load(cls, yaml_path: str) -> Self: + with open(yaml_path, "r") as f: + data = yaml.safe_load(f) + config = dacite.from_dict(data_class=Config, data=data) + + # Validations + config.simulation.validate() + config.mixnet.validate() + config.p2p.validate() + config.adversary.validate() + + return config + + +@dataclass +class SimulationConfig: running_time: int + + def validate(self): + assert self.running_time > 0 + + +@dataclass +class MixnetConfig: num_nodes: int num_mix_layers: int # An interval of sending a new real/cover message @@ -23,31 +53,35 @@ class Config: cover_message_prob: float # A maximum preparation time (delay) before sending the message max_message_prep_time: float - # A maximum network latency between nodes directly connected with each other - max_network_latency: float # A maximum delay of messages mixed in a mix node max_mix_delay: float + + def validate(self): + assert self.num_nodes > 0 + assert 0 < self.num_mix_layers <= self.num_nodes + assert self.message_interval > 0 + assert self.real_message_prob > 0 + assert len(self.real_message_prob_weights) <= self.num_nodes + for weight in self.real_message_prob_weights: + assert weight >= 1 + assert self.cover_message_prob >= 0 + assert self.max_message_prep_time >= 0 + assert self.max_mix_delay >= 0 + + +@dataclass +class P2pConfig: + # A maximum network latency between nodes directly connected with each other + max_network_latency: float + + def validate(self): + assert self.max_network_latency >= 0 + + +@dataclass +class AdversaryConfig: # A discrete time window for the adversary to observe inputs and outputs of a certain node io_observation_window: int - @classmethod - def load(cls, yaml_path: str) -> Self: - with open(yaml_path, "r") as f: - data = yaml.safe_load(f) - config = dacite.from_dict(data_class=Config, data=data) - - # Validations - assert config.running_time > 0 - assert config.num_nodes > 0 - assert 0 < config.num_mix_layers <= config.num_nodes - assert config.message_interval > 0 - assert config.real_message_prob > 0 - assert len(config.real_message_prob_weights) <= config.num_nodes - for weight in config.real_message_prob_weights: - assert weight >= 1 - assert config.cover_message_prob >= 0 - assert config.max_message_prep_time >= 0 - assert config.max_network_latency >= 0 - assert config.io_observation_window >= 0 - - return config + def validate(self): + assert self.io_observation_window >= 0 diff --git a/mixnet/v2/sim/config.yaml b/mixnet/v2/sim/config.yaml index 3b4b2fc..350870e 100644 --- a/mixnet/v2/sim/config.yaml +++ b/mixnet/v2/sim/config.yaml @@ -1,23 +1,30 @@ -# The simulation uses a virtual time. Please see README for more details. -running_time: 30 -num_nodes: 100 -num_mix_layers: 3 -# An interval of sending a new real/cover message -# A probability of actually sending a message depends on the following parameters. -message_interval: 1 -# A probability of sending a real message within a cycle -real_message_prob: 0.1 -# A weight of real message emission probability of some nodes -# Each weight is assigned to each node in the order of the node ID. -# The length of the list should be <= num_nodes. i.e. some nodes won't have a weight. -real_message_prob_weights: [10, 8, 12] -# A probability of sending a cover message within a cycle if not sending a real message -cover_message_prob: 0.2 -# A maximum preparation time (delay) before sending the message -max_message_prep_time: 0.3 -# A maximum network latency between nodes directly connected with each other -max_network_latency: 0.5 -# A maximum delay of messages mixed in a mix node -max_mix_delay: 3 -# A discrete time window for the adversary to observe inputs and outputs of a certain node -io_observation_window: 1 \ No newline at end of file +simulation: + # The simulation uses a virtual time. Please see README for more details. + running_time: 30 + +mixnet: + num_nodes: 100 + num_mix_layers: 3 + # An interval of sending a new real/cover message + # A probability of actually sending a message depends on the following parameters. + message_interval: 1 + # A probability of sending a real message within a cycle + real_message_prob: 0.1 + # A weight of real message emission probability of some nodes + # Each weight is assigned to each node in the order of the node ID. + # The length of the list should be <= p2p.num_nodes. i.e. some nodes won't have a weight. + real_message_prob_weights: [10, 8, 12] + # A probability of sending a cover message within a cycle if not sending a real message + cover_message_prob: 0.2 + # A maximum preparation time (delay) before sending the message + max_message_prep_time: 0.3 + # A maximum delay of messages mixed in a mix node + max_mix_delay: 3 + +p2p: + # A maximum network latency between nodes directly connected with each other + max_network_latency: 0.5 + +adversary: + # A discrete time window for the adversary to observe inputs and outputs of a certain node + io_observation_window: 1 \ No newline at end of file diff --git a/mixnet/v2/sim/node.py b/mixnet/v2/sim/node.py index bb3e898..accd592 100644 --- a/mixnet/v2/sim/node.py +++ b/mixnet/v2/sim/node.py @@ -29,13 +29,13 @@ class Node: Creates/encapsulate a message and send it to the network through the mixnet """ while True: - yield self.env.timeout(self.config.message_interval) + yield self.env.timeout(self.config.mixnet.message_interval) payload = self.payload_to_send() if payload is None: # nothing to send in this turn continue - prep_time = random.uniform(0, self.config.max_message_prep_time) + prep_time = random.uniform(0, self.config.mixnet.max_message_prep_time) yield self.env.timeout(prep_time) self.log("Sending a message to the mixnet") @@ -46,22 +46,22 @@ class Node: rnd = random.random() if rnd < self.real_message_prob(): return self.REAL_PAYLOAD - elif rnd < self.config.cover_message_prob: + elif rnd < self.config.mixnet.cover_message_prob: return self.COVER_PAYLOAD else: return None def real_message_prob(self): - weight = self.config.real_message_prob_weights[self.id] \ - if self.id < len(self.config.real_message_prob_weights) else 0 - return self.config.real_message_prob * weight + weight = self.config.mixnet.real_message_prob_weights[self.id] \ + if self.id < len(self.config.mixnet.real_message_prob_weights) else 0 + return self.config.mixnet.real_message_prob * weight def create_message(self, payload: bytes) -> SphinxPacket: """ Creates a message using the Sphinx format @return: """ - mixes = self.p2p.get_nodes(self.config.num_mix_layers) + mixes = self.p2p.get_nodes(self.config.mixnet.num_mix_layers) public_keys = [mix.public_key for mix in mixes] # TODO: replace with realistic tx incentive_txs = [Node.create_incentive_tx(mix.public_key) for mix in mixes] @@ -89,7 +89,7 @@ class Node: self.log("Dropping a cover message: %s" % msg.payload) else: # TODO: use Poisson delay or something else, if necessary - yield self.env.timeout(random.uniform(0, self.config.max_mix_delay)) + yield self.env.timeout(random.uniform(0, self.config.mixnet.max_mix_delay)) self.env.process(self.p2p.broadcast(self, msg)) else: self.log("Receiving SphinxPacket, but not mine") diff --git a/mixnet/v2/sim/p2p.py b/mixnet/v2/sim/p2p.py index e6feef4..33f432a 100644 --- a/mixnet/v2/sim/p2p.py +++ b/mixnet/v2/sim/p2p.py @@ -44,7 +44,7 @@ class P2p: def send(self, msg: SphinxPacket | bytes, node): # simulate network latency - yield self.env.timeout(random.uniform(0, self.config.max_network_latency)) + yield self.env.timeout(random.uniform(0, self.config.p2p.max_network_latency)) self.adversary.observe_incoming_message(node) self.env.process(node.receive_message(msg)) diff --git a/mixnet/v2/sim/simulation.py b/mixnet/v2/sim/simulation.py index 8af2fc5..3600668 100644 --- a/mixnet/v2/sim/simulation.py +++ b/mixnet/v2/sim/simulation.py @@ -13,8 +13,8 @@ class Simulation: self.config = config self.env = simpy.Environment() self.p2p = P2p(self.env, config) - self.nodes = [Node(i, self.env, self.p2p, config) for i in range(config.num_nodes)] + self.nodes = [Node(i, self.env, self.p2p, config) for i in range(config.mixnet.num_nodes)] self.p2p.add_node(self.nodes) def run(self): - self.env.run(until=self.config.running_time) + self.env.run(until=self.config.simulation.running_time)