Mixnet: mix client emission (#50)

This commit is contained in:
Youngjoon Lee 2024-01-23 10:46:00 +09:00 committed by GitHub
parent d963d6cb51
commit 30d52791c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 154 additions and 1 deletions

75
mixnet/client.py Normal file
View File

@ -0,0 +1,75 @@
from __future__ import annotations
import queue
import time
from datetime import datetime, timedelta
from threading import Thread
from mixnet.mixnet import Mixnet, MixnetTopology
from mixnet.node import PacketQueue
from mixnet.packet import PacketBuilder
from mixnet.poisson import poisson_interval_sec
class MixClientRunner(Thread):
"""
Emit packets at the Poisson emission_rate_per_min.
If a real packet is scheduled to be sent, this thread sends the real packet to the mixnet,
and schedules redundant real packets to be emitted in the next turns.
If no real packet is not scheduled, this thread emits a cover packet according to the emission_rate_per_min.
"""
def __init__(
self,
mixnet: Mixnet,
topology: MixnetTopology,
emission_rate_per_min: int, # Poisson rate parameter: lambda in the spec
redundancy: int, # b in the spec
real_packet_queue: PacketQueue,
outbound_socket: PacketQueue,
):
super().__init__()
self.mixnet = mixnet
self.topology = topology
self.emission_rate_per_min = emission_rate_per_min
self.redundancy = redundancy
self.real_packet_queue = real_packet_queue
self.redundant_real_packet_queue: PacketQueue = queue.Queue()
self.outbound_socket = outbound_socket
def run(self) -> None:
# Here in Python, this thread is implemented in synchronous manner.
# In the real implementation, consider implementing this in asynchronous if possible.
next_emission_ts = datetime.now() + timedelta(
seconds=poisson_interval_sec(self.emission_rate_per_min)
)
while True:
time.sleep(1 / 1000)
if datetime.now() < next_emission_ts:
continue
next_emission_ts += timedelta(
seconds=poisson_interval_sec(self.emission_rate_per_min)
)
if not self.redundant_real_packet_queue.empty():
addr, packet = self.redundant_real_packet_queue.get()
self.outbound_socket.put((addr, packet))
continue
if not self.real_packet_queue.empty():
addr, packet = self.real_packet_queue.get()
# Schedule redundant real packets
for _ in range(self.redundancy - 1):
self.redundant_real_packet_queue.put((addr, packet))
self.outbound_socket.put((addr, packet))
packet, route = PacketBuilder.drop_cover(
b"drop cover", self.mixnet, self.topology
).next()
self.outbound_socket.put((route[0].addr, packet))

78
mixnet/test_client.py Normal file
View File

@ -0,0 +1,78 @@
import queue
from datetime import datetime
from typing import Tuple
from unittest import TestCase
import numpy
import timeout_decorator
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from mixnet.bls import generate_bls
from mixnet.client import MixClientRunner
from mixnet.mixnet import Mixnet, MixnetTopology
from mixnet.node import MixNode, PacketQueue
from mixnet.packet import PacketBuilder
from mixnet.poisson import poisson_mean_interval_sec
from mixnet.utils import random_bytes
class TestMixClientRunner(TestCase):
@timeout_decorator.timeout(180)
def test_mixclient_runner_emission_rate(self):
mixnet, topology = self.init()
real_packet_queue: PacketQueue = queue.Queue()
outbound_socket: PacketQueue = queue.Queue()
emission_rate_per_min = 30
redundancy = 3
client = MixClientRunner(
mixnet,
topology,
emission_rate_per_min,
redundancy,
real_packet_queue,
outbound_socket,
)
client.daemon = True
client.start()
# Create packets. At least two packets are expected to be generated from a 3500-byte msg
builder = PacketBuilder.real(random_bytes(3500), mixnet, topology)
# Schedule two packets to the mix client without any interval
packet, route = builder.next()
real_packet_queue.put((route[0].addr, packet))
packet, route = builder.next()
real_packet_queue.put((route[0].addr, packet))
# Calculate intervals between packet emissions from the mix client
intervals = []
ts = datetime.now()
for _ in range(30):
_ = outbound_socket.get()
now = datetime.now()
intervals.append((now - ts).total_seconds())
ts = now
# Check if packets were emitted at the Poisson emission_rate
# If emissions follow the Poisson distribution with a rate `lambda`,
# a mean interval between emissions must be `1/lambda`.
self.assertAlmostEqual(
float(numpy.mean(intervals)),
poisson_mean_interval_sec(emission_rate_per_min),
delta=1.0,
)
@staticmethod
def init() -> Tuple[Mixnet, MixnetTopology]:
mixnet = Mixnet(
[
MixNode(
generate_bls(),
X25519PrivateKey.generate(),
random_bytes(32),
)
for _ in range(12)
]
)
topology = mixnet.build_topology(b"entropy", 3, 3)
return mixnet, topology

View File

@ -83,7 +83,7 @@ class TestMixNodeRunner(TestCase):
self.assertAlmostEqual(
float(numpy.mean(num_jobs)),
round(emission_rate_per_min / delay_rate_per_min),
delta=1.0,
delta=1.5,
)
@staticmethod