diff --git a/mixnet/__init__.py b/mixnet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mixnet/bls.py b/mixnet/bls.py new file mode 100644 index 0000000..a4278b0 --- /dev/null +++ b/mixnet/bls.py @@ -0,0 +1,13 @@ +from typing import TypeAlias + +import blspy + +from mixnet.utils import random_bytes + +BlsPrivateKey: TypeAlias = blspy.PrivateKey +BlsPublicKey: TypeAlias = blspy.G1Element + + +def generate_bls() -> BlsPrivateKey: + seed = random_bytes(32) + return blspy.BasicSchemeMPL.key_gen(seed) diff --git a/mixnet/fisheryates.py b/mixnet/fisheryates.py new file mode 100644 index 0000000..70c92ff --- /dev/null +++ b/mixnet/fisheryates.py @@ -0,0 +1,21 @@ +import random +from typing import List + + +class FisherYates: + @staticmethod + def shuffle(elements: List, entropy: bytes) -> List: + """ + Fisher-Yates shuffling algorithm. + In Python, random.shuffle implements the Fisher-Yates shuffling. + https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + https://softwareengineering.stackexchange.com/a/215780 + :param elements: elements to be shuffled + :param entropy: a seed for deterministic sampling + """ + out = elements.copy() + random.seed(a=entropy, version=2) + random.shuffle(out) + # reset seed + random.seed() + return out diff --git a/mixnet/mixnet.py b/mixnet/mixnet.py new file mode 100644 index 0000000..4fd5cb1 --- /dev/null +++ b/mixnet/mixnet.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, TypeAlias + +from cryptography.hazmat.primitives.asymmetric.x25519 import ( + X25519PrivateKey, + X25519PublicKey, +) + +from mixnet.bls import BlsPrivateKey, BlsPublicKey +from mixnet.fisheryates import FisherYates + +NodeId: TypeAlias = BlsPublicKey +# 32-byte that represents an IP address and a port of a mix node. +NodeAddress: TypeAlias = bytes + + +@dataclass +class Mixnet: + mix_nodes: List[MixNode] + + # Build a new topology deterministically using an entropy. + # The entropy is expected to be injected from outside. + # + # TODO: Implement constructing a new topology in advance to minimize the topology transition time. + # https://www.notion.so/Mixnet-Specification-807b624444a54a4b88afa1cc80e100c2?pvs=4#9a7f6089e210454bb11fe1c10fceff68 + def build_topology( + self, + entropy: bytes, + n_layers: int, + n_nodes_per_layer: int, + ) -> MixnetTopology: + num_nodes = n_nodes_per_layer * n_layers + assert num_nodes < len(self.mix_nodes) + + shuffled = FisherYates.shuffle(self.mix_nodes, entropy) + sampled = shuffled[:num_nodes] + layers = [] + for l in range(n_layers): + start = l * n_nodes_per_layer + layer = sampled[start : start + n_nodes_per_layer] + layers.append(layer) + return MixnetTopology(layers) + + +@dataclass +class MixNode: + identity_public_key: BlsPublicKey + encryption_public_key: X25519PublicKey + addr: NodeAddress + + def __init__( + self, + identity_private_key: BlsPrivateKey, + encryption_private_key: X25519PrivateKey, + addr: NodeAddress, + ): + self.identity_public_key = identity_private_key.get_g1() + self.encryption_public_key = encryption_private_key.public_key() + self.addr = addr + + +@dataclass +class MixnetTopology: + layers: List[List[MixNode]] diff --git a/mixnet/test_fisheryates.py b/mixnet/test_fisheryates.py new file mode 100644 index 0000000..a32554c --- /dev/null +++ b/mixnet/test_fisheryates.py @@ -0,0 +1,21 @@ +from unittest import TestCase + +from mixnet.fisheryates import FisherYates + + +class TestFisherYates(TestCase): + def test_shuffle(self): + entropy = b"hello" + elems = [1, 2, 3, 4, 5] + + shuffled1 = FisherYates.shuffle(elems, entropy) + self.assertEqual(sorted(elems), sorted(shuffled1)) + + # shuffle again with the same entropy + shuffled2 = FisherYates.shuffle(elems, entropy) + self.assertEqual(shuffled1, shuffled2) + + # shuffle with a different entropy + shuffled3 = FisherYates.shuffle(elems, b"world") + self.assertNotEqual(shuffled1, shuffled3) + self.assertEqual(sorted(elems), sorted(shuffled3)) diff --git a/mixnet/test_mixnet.py b/mixnet/test_mixnet.py new file mode 100644 index 0000000..d63e914 --- /dev/null +++ b/mixnet/test_mixnet.py @@ -0,0 +1,21 @@ +from unittest import TestCase + +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey + +from mixnet.bls import generate_bls +from mixnet.mixnet import Mixnet, MixNode +from mixnet.utils import random_bytes + + +class TestMixnet(TestCase): + def test_build_topology(self): + nodes = [ + MixNode(generate_bls(), X25519PrivateKey.generate(), random_bytes(32)) + for _ in range(12) + ] + mixnet = Mixnet(nodes) + + topology = mixnet.build_topology(b"entropy", 3, 3) + self.assertEqual(len(topology.layers), 3) + for layer in topology.layers: + self.assertEqual(len(layer), 3) diff --git a/mixnet/utils.py b/mixnet/utils.py new file mode 100644 index 0000000..6b45176 --- /dev/null +++ b/mixnet/utils.py @@ -0,0 +1,6 @@ +from random import randint + + +def random_bytes(size: int) -> bytes: + assert size >= 0 + return bytes([randint(0, 255) for _ in range(size)]) diff --git a/requirements.txt b/requirements.txt index 4ea0792..9f987b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ -blspy~=1.0.16 -scipy~=1.10.1 \ No newline at end of file +blspy==1.0.16 +cffi==1.16.0 +cryptography==41.0.7 +numpy==1.26.2 +pycparser==2.21 +scipy==1.10.1