2024-05-17 19:03:48 +09:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
2024-05-09 19:37:39 +09:00
|
|
|
import random
|
2024-05-17 19:03:48 +09:00
|
|
|
from enum import Enum
|
2024-05-09 19:37:39 +09:00
|
|
|
|
|
|
|
|
import simpy
|
2024-05-12 19:30:04 +09:00
|
|
|
from cryptography.hazmat.primitives import serialization
|
|
|
|
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
2024-05-09 19:37:39 +09:00
|
|
|
|
2024-05-14 21:19:46 +09:00
|
|
|
from config import Config
|
2024-05-31 21:42:56 +09:00
|
|
|
from measurement import Measurement
|
2024-05-13 08:49:09 +09:00
|
|
|
from sphinx import SphinxPacket, Attachment
|
2024-05-23 16:34:39 +09:00
|
|
|
from p2p import P2P
|
2024-05-09 14:29:10 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Node:
|
2024-05-13 13:31:57 +09:00
|
|
|
INCENTIVE_TX_SIZE = 512
|
2024-05-13 14:12:10 +09:00
|
|
|
PADDING_SEPARATOR = b'\x01'
|
2024-05-09 19:37:39 +09:00
|
|
|
|
2024-05-31 21:42:56 +09:00
|
|
|
def __init__(self, id: int, env: simpy.Environment, p2p: P2P, config: Config, measurement: Measurement):
|
2024-05-09 19:37:39 +09:00
|
|
|
self.id = id
|
|
|
|
|
self.env = env
|
|
|
|
|
self.p2p = p2p
|
2024-05-12 19:30:04 +09:00
|
|
|
self.private_key = X25519PrivateKey.generate()
|
|
|
|
|
self.public_key = self.private_key.public_key()
|
2024-05-14 21:19:46 +09:00
|
|
|
self.config = config
|
2024-05-23 15:49:38 +09:00
|
|
|
self.payload_id = 0
|
2024-05-31 21:42:56 +09:00
|
|
|
self.measurement = measurement
|
2024-05-09 19:37:39 +09:00
|
|
|
self.action = self.env.process(self.send_message())
|
2024-05-09 14:29:10 +09:00
|
|
|
|
|
|
|
|
def send_message(self):
|
|
|
|
|
"""
|
|
|
|
|
Creates/encapsulate a message and send it to the network through the mixnet
|
|
|
|
|
"""
|
2024-05-13 09:00:01 +09:00
|
|
|
while True:
|
2024-05-17 14:54:23 +09:00
|
|
|
yield self.env.timeout(self.config.mixnet.message_interval)
|
2024-05-15 16:34:09 +09:00
|
|
|
|
2024-05-17 19:03:48 +09:00
|
|
|
message_type = self.message_type_to_send()
|
|
|
|
|
if message_type is None: # nothing to send in this turn
|
2024-05-15 16:34:09 +09:00
|
|
|
continue
|
2024-05-31 21:42:56 +09:00
|
|
|
elif message_type == MessageType.REAL:
|
|
|
|
|
self.measurement.count_original_sender(self)
|
2024-05-15 16:34:09 +09:00
|
|
|
|
2024-05-17 19:03:48 +09:00
|
|
|
msg = self.create_message(message_type)
|
2024-05-17 14:54:23 +09:00
|
|
|
prep_time = random.uniform(0, self.config.mixnet.max_message_prep_time)
|
2024-05-15 16:34:09 +09:00
|
|
|
yield self.env.timeout(prep_time)
|
|
|
|
|
|
2024-05-13 15:13:21 +09:00
|
|
|
self.log("Sending a message to the mixnet")
|
2024-05-15 18:01:42 +09:00
|
|
|
self.env.process(self.p2p.broadcast(self, msg))
|
2024-05-09 14:29:10 +09:00
|
|
|
|
2024-05-17 19:03:48 +09:00
|
|
|
def message_type_to_send(self) -> MessageType | None:
|
2024-05-15 16:53:26 +09:00
|
|
|
rnd = random.random()
|
2024-05-15 18:55:17 +09:00
|
|
|
if rnd < self.real_message_prob():
|
2024-05-17 19:03:48 +09:00
|
|
|
return MessageType.REAL
|
2024-05-17 14:54:23 +09:00
|
|
|
elif rnd < self.config.mixnet.cover_message_prob:
|
2024-05-17 19:03:48 +09:00
|
|
|
return MessageType.COVER
|
2024-05-15 16:53:26 +09:00
|
|
|
else:
|
|
|
|
|
return None
|
2024-05-15 16:34:09 +09:00
|
|
|
|
2024-05-15 18:55:17 +09:00
|
|
|
def real_message_prob(self):
|
2024-05-17 14:54:23 +09:00
|
|
|
weight = self.config.mixnet.real_message_prob_weights[self.id] \
|
2024-05-21 15:01:07 +09:00
|
|
|
if self.id < len(self.config.mixnet.real_message_prob_weights) else 1
|
2024-05-17 14:54:23 +09:00
|
|
|
return self.config.mixnet.real_message_prob * weight
|
2024-05-15 18:55:17 +09:00
|
|
|
|
2024-05-17 19:03:48 +09:00
|
|
|
def create_message(self, message_type: MessageType) -> SphinxPacket | bytes:
|
2024-05-09 14:29:10 +09:00
|
|
|
"""
|
2024-05-17 19:03:48 +09:00
|
|
|
Creates a real or cover message
|
2024-05-09 14:29:10 +09:00
|
|
|
@return:
|
|
|
|
|
"""
|
2024-05-21 19:57:22 +09:00
|
|
|
if not self.config.mixnet.is_mixing_on():
|
2024-05-20 16:54:32 +09:00
|
|
|
return self.build_payload()
|
|
|
|
|
|
2024-05-17 14:54:23 +09:00
|
|
|
mixes = self.p2p.get_nodes(self.config.mixnet.num_mix_layers)
|
2024-05-12 19:30:04 +09:00
|
|
|
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]
|
2024-05-17 19:03:48 +09:00
|
|
|
if message_type == MessageType.COVER:
|
|
|
|
|
# Set invalid txs for a cover message,
|
|
|
|
|
# so that nobody will recognize that as a real message to be forwarded to the next mix.
|
|
|
|
|
incentive_txs = [Attachment(os.urandom(len(bytes(tx)))) for tx in incentive_txs]
|
2024-05-18 01:13:38 +09:00
|
|
|
return SphinxPacket(public_keys, incentive_txs, self.build_payload())
|
2024-05-09 14:29:10 +09:00
|
|
|
|
2024-05-12 19:30:04 +09:00
|
|
|
def receive_message(self, msg: SphinxPacket | bytes):
|
2024-05-09 14:29:10 +09:00
|
|
|
"""
|
|
|
|
|
Receives a message from the network, processes it,
|
|
|
|
|
and forwards it to the next mix or the entire network if necessary.
|
|
|
|
|
@param msg: the message to be processed
|
|
|
|
|
"""
|
2024-05-12 19:30:04 +09:00
|
|
|
if isinstance(msg, SphinxPacket):
|
|
|
|
|
msg, incentive_tx = msg.unwrap(self.private_key)
|
|
|
|
|
if self.is_my_incentive_tx(incentive_tx):
|
2024-05-25 12:02:51 +09:00
|
|
|
# self.log("Receiving SphinxPacket. It's mine!")
|
2024-05-13 13:31:57 +09:00
|
|
|
if msg.is_all_unwrapped():
|
2024-05-21 15:13:14 +09:00
|
|
|
final_padded_msg = self.pad_payload(msg.payload, len(msg))
|
2024-05-18 01:13:38 +09:00
|
|
|
self.env.process(self.p2p.broadcast(self, final_padded_msg))
|
2024-05-12 19:30:04 +09:00
|
|
|
else:
|
2024-05-16 17:13:43 +09:00
|
|
|
# TODO: use Poisson delay or something else, if necessary
|
2024-05-17 14:54:23 +09:00
|
|
|
yield self.env.timeout(random.uniform(0, self.config.mixnet.max_mix_delay))
|
2024-05-15 18:01:42 +09:00
|
|
|
self.env.process(self.p2p.broadcast(self, msg))
|
2024-05-25 12:02:51 +09:00
|
|
|
# else:
|
|
|
|
|
# self.log("Receiving SphinxPacket, but not mine")
|
2024-05-12 19:30:04 +09:00
|
|
|
else:
|
2024-05-13 14:12:10 +09:00
|
|
|
final_msg = msg[:msg.rfind(self.PADDING_SEPARATOR)]
|
|
|
|
|
self.log("Received final message: %s" % final_msg)
|
2024-05-12 19:30:04 +09:00
|
|
|
|
2024-05-18 01:13:38 +09:00
|
|
|
def build_payload(self) -> bytes:
|
2024-05-23 15:49:38 +09:00
|
|
|
payload = bytes(f"{self.id}-{self.payload_id}", "utf-8")
|
|
|
|
|
self.payload_id += 1
|
|
|
|
|
return payload + bytes(self.config.mixnet.payload_size - len(payload))
|
2024-05-18 01:13:38 +09:00
|
|
|
|
2024-05-21 15:13:14 +09:00
|
|
|
def pad_payload(self, payload: bytes, target_size: int) -> bytes:
|
|
|
|
|
"""
|
|
|
|
|
Pad the final msg to the target size (e.g. the same size as a SphinxPacket),
|
|
|
|
|
assuming that the final msg is going to be sent via secure channels (TLS, Noise, etc.)
|
|
|
|
|
"""
|
|
|
|
|
return (payload
|
|
|
|
|
+ self.PADDING_SEPARATOR
|
|
|
|
|
+ bytes(target_size - len(payload) - len(self.PADDING_SEPARATOR)))
|
|
|
|
|
|
2024-05-13 12:53:14 +09:00
|
|
|
# TODO: This is a dummy logic
|
2024-05-12 19:30:04 +09:00
|
|
|
@classmethod
|
|
|
|
|
def create_incentive_tx(cls, mix_public_key: X25519PublicKey) -> Attachment:
|
2024-05-17 13:51:41 +09:00
|
|
|
public_key = mix_public_key.public_bytes(encoding=serialization.Encoding.Raw,
|
|
|
|
|
format=serialization.PublicFormat.Raw)
|
2024-05-13 12:53:14 +09:00
|
|
|
public_key += bytes(cls.INCENTIVE_TX_SIZE - len(public_key))
|
|
|
|
|
return Attachment(public_key)
|
2024-05-12 19:30:04 +09:00
|
|
|
|
|
|
|
|
def is_my_incentive_tx(self, tx: Attachment) -> bool:
|
|
|
|
|
return tx == Node.create_incentive_tx(self.public_key)
|
|
|
|
|
|
|
|
|
|
def log(self, msg):
|
2024-05-24 16:36:04 +09:00
|
|
|
print(f"t={self.env.now:.3f}: Node:{self.id}: {msg}")
|
2024-05-17 19:03:48 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MessageType(Enum):
|
|
|
|
|
REAL = 0
|
|
|
|
|
COVER = 1
|