From 87dea94f6533ddc1ec0b53ad35a26269f8386692 Mon Sep 17 00:00:00 2001 From: Prem Chaitanya Prathi Date: Thu, 30 Apr 2026 22:07:35 +0530 Subject: [PATCH 1/2] chore: extract mix to logos-co/nim-libp2p-mix Mix has been moved out of nim-libp2p into its own repository at https://github.com/logos-co/nim-libp2p-mix so it can evolve independently. This commit removes the mix code from nim-libp2p: - libp2p/protocols/mix/ (tree) - libp2p/protocols/mix.nim (facade) - tests/libp2p/mix/ (mix tests) - examples/mix_ping.nim (mix example) - libp2p/peerstore.nim: drop MixPubKeyBook (was only used by mix) - cbind: drop the libp2p_mix_* C API and Mix-specific types/fields: * 7 libp2p_mix_* exported procs in cbind/libp2p.nim * Mix APIs section in cbind/libp2p.h * MixCurve25519Key, MixSecp256k1PubKey, MixReadBehaviorKind in ffi_types.nim * mix and mixNodeInfo fields on the LibP2P context type * MIX_DIAL / MIX_REGISTER_DEST_READ / MIX_SET_NODE_INFO / MIX_NODEPOOL_ADD request kinds and their processors * cbind/examples/mix.c - libp2p.nimble: drop -d:libp2p_mix_experimental_exit_is_dest from the default test cfg (the new repo sets the flag in its own nimble file) - docs/protocols_mix.md and docs/protocols_mix_spam_protection.md - README, docs/README, .github/copilot-instructions: drop mix entries Consumers have already been migrated: - logos-co/mix-rln-spam-protection-plugin#6 - logos-messaging/logos-delivery#3842 Net change: 49 files deleted, 16 modified (-538 / +6). History for the extracted mix code is preserved in logos-co/nim-libp2p-mix with original authors, dates, and PR references rewritten as cross-repo links to vacp2p/nim-libp2p#NNNN. --- .github/copilot-instructions.md | 15 +- README.md | 2 - cbind/cbind.nimble | 2 - cbind/examples/mix.c | 339 ----- cbind/ffi_types.nim | 11 - cbind/libp2p.h | 53 +- cbind/libp2p.nim | 200 +-- .../libp2p_thread_request.nim | 8 - .../requests/libp2p_lifecycle_requests.nim | 17 - .../requests/libp2p_stream_requests.nim | 166 --- cbind/types.nim | 4 - docs/README.md | 3 - docs/development.md | 1 - docs/protocols_mix.md | 114 -- docs/protocols_mix_spam_protection.md | 44 - examples/examples_run.nim | 2 +- examples/mix_ping.nim | 97 -- libp2p.nimble | 1 - libp2p/peerstore.nim | 4 - libp2p/protocols/mix.nim | 67 - libp2p/protocols/mix/benchmark.nim | 39 - libp2p/protocols/mix/cover_traffic.nim | 436 ------- libp2p/protocols/mix/crypto.nim | 56 - libp2p/protocols/mix/curve25519.nim | 55 - libp2p/protocols/mix/delay.nim | 22 - libp2p/protocols/mix/delay_strategy.nim | 144 --- libp2p/protocols/mix/entry_connection.nim | 139 --- libp2p/protocols/mix/exit_connection.nim | 53 - libp2p/protocols/mix/exit_layer.nim | 171 --- libp2p/protocols/mix/fragmentation.nim | 98 -- libp2p/protocols/mix/mix_message.nim | 50 - libp2p/protocols/mix/mix_metrics.nim | 29 - libp2p/protocols/mix/mix_node.nim | 85 -- libp2p/protocols/mix/mix_protocol.nim | 1108 ----------------- libp2p/protocols/mix/multiaddr.nim | 111 -- libp2p/protocols/mix/pool.nim | 110 -- libp2p/protocols/mix/reply_connection.nim | 47 - libp2p/protocols/mix/seqno_generator.nim | 32 - libp2p/protocols/mix/serialization.nim | 239 ---- libp2p/protocols/mix/spam_protection.nim | 152 --- libp2p/protocols/mix/sphinx.nim | 412 ------ libp2p/protocols/mix/tag_manager.nim | 97 -- .../mix/component/test_connection_api.nim | 93 -- .../mix/component/test_cover_traffic.nim | 162 --- .../mix/component/test_message_delivery.nim | 265 ---- .../mix/component/test_node_failures.nim | 297 ----- tests/libp2p/mix/component/test_security.nim | 82 -- .../mix/component/test_spam_protection.nim | 103 -- tests/libp2p/mix/mock_mix.nim | 85 -- tests/libp2p/mix/spam_protection_impl.nim | 138 -- tests/libp2p/mix/test_cover_traffic.nim | 323 ----- tests/libp2p/mix/test_crypto.nim | 129 -- tests/libp2p/mix/test_curve25519.nim | 48 - tests/libp2p/mix/test_delay_strategy.nim | 180 --- tests/libp2p/mix/test_fragmentation.nim | 114 -- tests/libp2p/mix/test_mix_message.nim | 95 -- tests/libp2p/mix/test_multiaddr.nim | 61 - tests/libp2p/mix/test_pool.nim | 232 ---- tests/libp2p/mix/test_seq_no_generator.nim | 96 -- tests/libp2p/mix/test_serialization.nim | 135 -- .../mix/test_spam_protection_interface.nim | 156 --- tests/libp2p/mix/test_sphinx.nim | 471 ------- tests/libp2p/mix/test_tag_manager.nim | 110 -- tests/libp2p/mix/utils.nim | 197 --- tests/libp2p/test_peer_store.nim | 55 - 65 files changed, 6 insertions(+), 8456 deletions(-) delete mode 100644 cbind/examples/mix.c delete mode 100644 docs/protocols_mix.md delete mode 100644 docs/protocols_mix_spam_protection.md delete mode 100644 examples/mix_ping.nim delete mode 100644 libp2p/protocols/mix.nim delete mode 100644 libp2p/protocols/mix/benchmark.nim delete mode 100644 libp2p/protocols/mix/cover_traffic.nim delete mode 100644 libp2p/protocols/mix/crypto.nim delete mode 100644 libp2p/protocols/mix/curve25519.nim delete mode 100644 libp2p/protocols/mix/delay.nim delete mode 100644 libp2p/protocols/mix/delay_strategy.nim delete mode 100644 libp2p/protocols/mix/entry_connection.nim delete mode 100644 libp2p/protocols/mix/exit_connection.nim delete mode 100644 libp2p/protocols/mix/exit_layer.nim delete mode 100644 libp2p/protocols/mix/fragmentation.nim delete mode 100644 libp2p/protocols/mix/mix_message.nim delete mode 100644 libp2p/protocols/mix/mix_metrics.nim delete mode 100644 libp2p/protocols/mix/mix_node.nim delete mode 100644 libp2p/protocols/mix/mix_protocol.nim delete mode 100644 libp2p/protocols/mix/multiaddr.nim delete mode 100644 libp2p/protocols/mix/pool.nim delete mode 100644 libp2p/protocols/mix/reply_connection.nim delete mode 100644 libp2p/protocols/mix/seqno_generator.nim delete mode 100644 libp2p/protocols/mix/serialization.nim delete mode 100644 libp2p/protocols/mix/spam_protection.nim delete mode 100644 libp2p/protocols/mix/sphinx.nim delete mode 100644 libp2p/protocols/mix/tag_manager.nim delete mode 100644 tests/libp2p/mix/component/test_connection_api.nim delete mode 100644 tests/libp2p/mix/component/test_cover_traffic.nim delete mode 100644 tests/libp2p/mix/component/test_message_delivery.nim delete mode 100644 tests/libp2p/mix/component/test_node_failures.nim delete mode 100644 tests/libp2p/mix/component/test_security.nim delete mode 100644 tests/libp2p/mix/component/test_spam_protection.nim delete mode 100644 tests/libp2p/mix/mock_mix.nim delete mode 100644 tests/libp2p/mix/spam_protection_impl.nim delete mode 100644 tests/libp2p/mix/test_cover_traffic.nim delete mode 100644 tests/libp2p/mix/test_crypto.nim delete mode 100644 tests/libp2p/mix/test_curve25519.nim delete mode 100644 tests/libp2p/mix/test_delay_strategy.nim delete mode 100644 tests/libp2p/mix/test_fragmentation.nim delete mode 100644 tests/libp2p/mix/test_mix_message.nim delete mode 100644 tests/libp2p/mix/test_multiaddr.nim delete mode 100644 tests/libp2p/mix/test_pool.nim delete mode 100644 tests/libp2p/mix/test_seq_no_generator.nim delete mode 100644 tests/libp2p/mix/test_serialization.nim delete mode 100644 tests/libp2p/mix/test_spam_protection_interface.nim delete mode 100644 tests/libp2p/mix/test_sphinx.nim delete mode 100644 tests/libp2p/mix/test_tag_manager.nim delete mode 100644 tests/libp2p/mix/utils.nim diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a944b0e558..295ce3dc8c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -28,7 +28,6 @@ nim-libp2p/ │ │ ├── connectivity/ # AutoNAT, DCUtR, Circuit Relay │ │ ├── pubsub/ # GossipSub, FloodSub │ │ ├── kademlia/ # Kademlia DHT -│ │ ├── mix/ # Sphinx Mix Network (privacy) │ │ ├── perf/ # Performance measurement protocol │ │ └── secure/ # Noise, Plaintext │ ├── transports/ # TCP, QUIC, WebSocket, Tor, Memory @@ -49,8 +48,6 @@ nim-libp2p/ │ ├── contributing.md # Contribution guidelines │ ├── compile_time_flags.md # All compile-time flags documented │ ├── common_hurdles.md # Known issues and fixes -│ ├── protocols_mix.md # Mix protocol documentation -│ ├── protocols_mix_spam_protection.md # Mix spam protection documentation │ └── interop_hole_punching.md # Hole punching interop test guide ├── tools/ # Developer tools (dependency pinner, markdown runner, etc.) ├── libp2p.nim # Main entry point (re-exports public APIs) @@ -81,7 +78,6 @@ nimble test # Run tests matching a path substring nimble testpath quic # all quic tests nimble testpath transports/test_ws # specific test file -nimble testpath mix # mix protocol tests # Run specific test suites nimble testmultiformatexts # MultiFormat extension tests @@ -144,7 +140,6 @@ These flags are used in CI and tests: | Flag | Purpose | |------|---------| | `-d:libp2p_autotls_support` | Enable AutoTLS support | -| `-d:libp2p_mix_experimental_exit_is_dest` | MIX protocol: exit node is destination | | `-d:libp2p_expensive_metrics` | Per-peer cardinality metrics | | `-d:libp2p_agents_metrics -d:KnownLibP2PAgents=nimbus,...` | Known agent metrics | | `-d:KnownLibP2PTopics=topic1,topic2` | GossipSub topic metrics | @@ -155,7 +150,7 @@ These flags are used in CI and tests: | `-d:libp2p_contentids_exts=` | ContentIds extensions file | The test runner (`libp2p.nimble`) always compiles with: -`-d:libp2p_autotls_support -d:libp2p_mix_experimental_exit_is_dest` +`-d:libp2p_autotls_support` --- @@ -357,7 +352,7 @@ The test runner (`libp2p.nimble`) always compiles with: ### API Stability - Treat the intended public API surface, especially modules re-exported from `libp2p.nim`, as backward-compatible within a MAJOR version. - If a PR introduces a breaking change to that public API surface, add a comment in the PR description that clearly documents the breaking change, the affected modules or APIs, and any required migration notes. -- Do not warn about breaking changes in the following modules, because they are not yet considered stable and remain under active development: `kademlia`, `mix`, `service_discovery`. +- Do not warn about breaking changes in the following modules, because they are not yet considered stable and remain under active development: `kademlia`, `service_discovery`. - Internal procedures and other non-public implementation details may change in MINOR versions. ### Experimental GossipSub Extensions @@ -423,10 +418,6 @@ The test runner (`libp2p.nimble`) always compiles with: - `rendezvous.nim` + `rendezvous/` — Rendezvous server protocol - `service_discovery.nim` + `service_discovery/` — Service discovery (random find, routing table manager) -### Privacy (`protocols/mix/`) -- Sphinx mix network for privacy-preserving message routing -- Curve25519, fragmentation, delay strategies, spam protection - ### Services (`services/`) - `autorelayservice.nim` — Automatic relay selection and connection - `hpservice.nim` — Hole punching service @@ -473,7 +464,7 @@ The `cbind/` directory contains the C/FFI layer for using nim-libp2p from C/C++: - `types.nim` — Additional C-compatible type implementations - `alloc.nim` — Cross-thread memory allocation helpers - `libp2p_thread/` — Thread management for async operations from C -- `examples/cbindings.c`, `examples/echo.c`, `examples/mix.c` — C usage examples +- `examples/cbindings.c`, `examples/echo.c` — C usage examples ```sh cd cbind diff --git a/README.md b/README.md index 02b44f90c0..8721451391 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,5 @@ List of packages modules implemented in nim-libp2p: | **DHT** | | | [kademlia](nim-libp2p/libp2p/protocols/kademlia.nim) | [Kademlia DHT](https://github.com/libp2p/specs/tree/master/kad-dht) for peer/value discovery | | [kademlia-discovery](nim-libp2p/libp2p/protocols/kad_disco.nim) | Kademlia-based peer discovery | -| **Privacy** | | -| [mix](nim-libp2p/libp2p/protocols/mix.nim) | [Mix](https://lip.logos.co/ift-ts/raw/mix.html#5-protocol-overview) network protocol with [Sphinx](https://cypherpunks.ca/~iang/pubs/Sphinx_Oakland09.pdf) packet format for anonymity | | **Performance** | | | [perf](nim-libp2p/libp2p/protocols/perf/core.nim) | [Perf](https://github.com/libp2p/specs/blob/master/perf/perf.md) protocol for benchmarking libp2p nodes | \ No newline at end of file diff --git a/cbind/cbind.nimble b/cbind/cbind.nimble index 510b6d8b6a..d9331e59c4 100644 --- a/cbind/cbind.nimble +++ b/cbind/cbind.nimble @@ -46,8 +46,6 @@ task libStatic, "Generate static bindings": task examples, "Build and run C bindings examples": buildCBindings "static", "" exec "g++ -I. -o ../build/cbindings ./examples/cbindings.c ../build/libp2p.a -pthread" - exec "g++ -I. -o ../build/mix ./examples/mix.c ../build/libp2p.a -pthread" exec "g++ -I. -o ../build/echo ./examples/echo.c ../build/libp2p.a -pthread" exec "../build/cbindings" - exec "../build/mix" exec "../build/echo" diff --git a/cbind/examples/mix.c b/cbind/examples/mix.c deleted file mode 100644 index be888aba25..0000000000 --- a/cbind/examples/mix.c +++ /dev/null @@ -1,339 +0,0 @@ -#include "../../cbind/libp2p.h" -#include -#include -#include -#include -#include - -#define NUM_NODES 5 - -pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; -pthread_cond_t cond = PTHREAD_COND_INITIALIZER; -int callback_executed = 0; -static libp2p_stream_t *mix_conn = NULL; - -typedef struct { - char peerId[256]; - const char **addrs; - size_t addrCount; -} PeerInfo; - -static void waitForCallback(void); -static void free_peerinfo(PeerInfo *pi); -static void event_handler(int callerRet, const char *msg, size_t len, - void *userData); -static void peerinfo_handler(int callerRet, const Libp2pPeerInfo *info, - const char *msg, size_t len, void *userData); -static void pubkey_handler(int callerRet, const uint8_t *data, size_t dataLen, - const char *msg, size_t len, void *userData); -static void private_key_handler(int callerRet, const uint8_t *keyData, - size_t keyDataLen, const char *msg, size_t len, - void *userData); -static void connection_handler(int callerRet, libp2p_stream_t *conn, - const char *msg, size_t len, void *userData); -static void read_handler(int callerRet, const uint8_t *data, size_t dataLen, - const char *msg, size_t len, void *userData); -static void signal_callback_executed(void); -static void fill_random(uint8_t *buf, size_t len); - -int main(int argc, char **argv) { - int status = 1; - libp2p_ctx_t *nodes[NUM_NODES] = {0}; - PeerInfo infos[NUM_NODES] = {0}; - libp2p_curve25519_key_t mix_priv_keys[NUM_NODES] = {0}; - libp2p_curve25519_key_t mix_pub_keys[NUM_NODES] = {0}; - // Needed for mix node pool entries (mix pool stores mix pubkey + libp2p - // pubkey). - libp2p_secp256k1_pubkey_t libp2p_pub_keys[NUM_NODES] = {0}; - libp2p_private_key_t libp2p_priv_keys[NUM_NODES] = {0}; - - for (int i = 0; i < NUM_NODES; i++) { - libp2p_config_t cfg = libp2p_new_default_config(); - cfg.mount_mix = 1; - cfg.mount_gossipsub = 0; - - libp2p_new_private_key(LIBP2P_PK_SECP256K1, private_key_handler, - &libp2p_priv_keys[i]); - waitForCallback(); - cfg.priv_key = libp2p_priv_keys[i]; - - nodes[i] = libp2p_new(&cfg, event_handler, NULL); - waitForCallback(); - - libp2p_start(nodes[i], event_handler, NULL); - waitForCallback(); - - libp2p_peerinfo(nodes[i], peerinfo_handler, &infos[i]); - waitForCallback(); - - libp2p_mix_generate_priv_key(&mix_priv_keys[i]); - libp2p_mix_public_key(mix_priv_keys[i], &mix_pub_keys[i]); - - libp2p_public_key(nodes[i], pubkey_handler, &libp2p_pub_keys[i]); - waitForCallback(); - - if (infos[i].addrCount == 0 || infos[i].addrs == NULL || - infos[i].addrs[0] == NULL) { - printf("Error: node %d has no listening address\n", i); - goto cleanup; - } - - // Mix node identity is separate from libp2p identity; this binds the node's - // listening multiaddr to the mix keypair so other mix nodes can route - // through it. - libp2p_mix_set_node_info(nodes[i], infos[i].addrs[0], mix_priv_keys[i], - event_handler, NULL); - waitForCallback(); - - // Exit-layer needs to know how to read application protocol payloads. - libp2p_mix_register_dest_read_behavior(nodes[i], "/ipfs/ping/1.0.0", - LIBP2P_MIX_READ_EXACTLY, 32, - event_handler, NULL); - waitForCallback(); - - printf("Node %d started: %s\n", i, infos[i].peerId); - for (size_t j = 0; j < infos[i].addrCount; j++) { - printf(" %s\n", infos[i].addrs[j]); - } - } - - printf("Started %d nodes with mix enabled.\n", NUM_NODES); - - // Each mix node needs the public info of the others to build random paths. - printf("Populating mix node pools...\n"); - for (int i = 0; i < NUM_NODES; i++) { - for (int j = 0; j < NUM_NODES; j++) { - if (i == j) - continue; - libp2p_mix_nodepool_add(nodes[i], infos[j].peerId, infos[j].addrs[0], - mix_pub_keys[j], libp2p_pub_keys[j], - event_handler, NULL); - waitForCallback(); - } - } - - // Mix dial from node 1 to node 5 (1-based indexing). - if (infos[4].addrCount == 0 || infos[4].addrs == NULL || - infos[4].addrs[0] == NULL) { - printf("Error: node 5 has no listening address\n"); - goto cleanup; - } - libp2p_mix_dial_with_reply(nodes[0], infos[4].peerId, infos[4].addrs[0], - "/ipfs/ping/1.0.0", 1, 0, connection_handler, - NULL); - waitForCallback(); - if (mix_conn == NULL) { - printf("Error: mix dial did not return a connection\n"); - goto cleanup; - } - - uint8_t payload[32]; - fill_random(payload, sizeof(payload)); - libp2p_stream_write(nodes[0], mix_conn, payload, sizeof(payload), - event_handler, NULL); - waitForCallback(); - - libp2p_stream_readExactly(nodes[0], mix_conn, sizeof(payload), read_handler, - NULL); - waitForCallback(); - - libp2p_stream_close(nodes[0], mix_conn, event_handler, NULL); - waitForCallback(); - - libp2p_stream_release(nodes[0], mix_conn, event_handler, NULL); - waitForCallback(); - mix_conn = NULL; - - status = 0; - - sleep(5); - -cleanup: - for (int i = 0; i < NUM_NODES; i++) { - free_peerinfo(&infos[i]); - } - - for (int i = 0; i < NUM_NODES; i++) { - free(libp2p_priv_keys[i].data); - libp2p_priv_keys[i].data = NULL; - libp2p_priv_keys[i].dataLen = 0; - } - - for (int i = 0; i < NUM_NODES; i++) { - if (nodes[i] != NULL) { - libp2p_stop(nodes[i], event_handler, NULL); - waitForCallback(); - libp2p_destroy(nodes[i], event_handler, NULL); - waitForCallback(); - nodes[i] = NULL; - } - } - - return status; -} - -static void event_handler(int callerRet, const char *msg, size_t len, - void *userData) { - if (callerRet == RET_OK) { - if (msg != NULL && len != 0) - printf("Event: %s\n", msg); - } else { - printf("Error(%d): %s\n", callerRet, msg != NULL ? msg : ""); - exit(1); - } - - signal_callback_executed(); -} - -static void peerinfo_handler(int callerRet, const Libp2pPeerInfo *info, - const char *msg, size_t len, void *userData) { - PeerInfo *pi = (PeerInfo *)userData; - - if (callerRet != RET_OK || info == NULL) { - if (msg != NULL && len > 0) { - printf("Error(%d): %.*s\n", callerRet, (int)len, msg); - } else { - printf("Error(%d): peerinfo callback failed\n", callerRet); - } - exit(1); - } - - free_peerinfo(pi); - - if (info->peerId != NULL) { - strncpy(pi->peerId, info->peerId, sizeof(pi->peerId) - 1); - pi->peerId[sizeof(pi->peerId) - 1] = '\0'; - } - - pi->addrCount = info->addrsLen; - if (info->addrsLen > 0 && info->addrs != NULL) { - pi->addrs = (const char **)calloc(info->addrsLen, sizeof(char *)); - if (pi->addrs == NULL) { - printf("Error: out of memory copying peerinfo addrs\n"); - exit(1); - } - for (size_t i = 0; i < info->addrsLen; i++) { - const char *addr = info->addrs[i]; - if (addr != NULL) { - size_t len = strlen(addr); - char *buf = (char *)malloc(len + 1); - if (buf == NULL) { - printf("Error: out of memory copying peerinfo addr\n"); - exit(1); - } - memcpy(buf, addr, len + 1); - pi->addrs[i] = buf; - } - } - } - - signal_callback_executed(); -} - -static void pubkey_handler(int callerRet, const uint8_t *data, size_t dataLen, - const char *msg, size_t len, void *userData) { - libp2p_secp256k1_pubkey_t *out = (libp2p_secp256k1_pubkey_t *)userData; - - if (callerRet != RET_OK) { - printf("Error(%d): %.*s\n", callerRet, (int)len, msg != NULL ? msg : ""); - exit(1); - } - - if (data == NULL || dataLen != sizeof(out->bytes)) { - printf("Error: invalid public key bytes (len=%zu)\n", dataLen); - exit(1); - } - - memcpy(out->bytes, data, sizeof(out->bytes)); - - signal_callback_executed(); -} - -static void private_key_handler(int callerRet, const uint8_t *keyData, - size_t keyDataLen, const char *msg, size_t len, - void *userData) { - if (callerRet != RET_OK || keyDataLen == 0 || keyData == NULL) { - printf("Private key error(%d): %.*s\n", callerRet, (int)len, - msg != NULL ? msg : ""); - exit(1); - } - - libp2p_private_key_t *priv_key = (libp2p_private_key_t *)userData; - - uint8_t *buf = (uint8_t *)malloc(keyDataLen); - if (!buf) { - fprintf(stderr, "Out of memory while copying private key\n"); - exit(1); - } - - memcpy(buf, keyData, keyDataLen); - priv_key->data = buf; - priv_key->dataLen = keyDataLen; - - signal_callback_executed(); -} - -static void connection_handler(int callerRet, libp2p_stream_t *conn, - const char *msg, size_t len, void *userData) { - if (callerRet != RET_OK) { - printf("Error(%d): %.*s\n", callerRet, (int)len, msg != NULL ? msg : ""); - exit(1); - } - - (void)conn; - mix_conn = conn; - (void)userData; - - signal_callback_executed(); -} - -static void fill_random(uint8_t *buf, size_t len) { - for (size_t i = 0; i < len; i++) { - buf[i] = (uint8_t)(rand() & 0xFF); - } -} - -static void read_handler(int callerRet, const uint8_t *data, size_t dataLen, - const char *msg, size_t len, void *userData) { - if (callerRet != RET_OK) { - printf("Read error(%d): %.*s\n", callerRet, (int)len, - msg != NULL ? msg : ""); - exit(1); - } - printf("========================================\n"); - printf("========================================\n"); - printf("========================================\n"); - printf("Read %zu bytes\n", dataLen); - (void)data; - (void)userData; - - signal_callback_executed(); -} - -static void signal_callback_executed(void) { - pthread_mutex_lock(&mutex); - callback_executed = 1; - pthread_cond_signal(&cond); - pthread_mutex_unlock(&mutex); -} - -static void waitForCallback(void) { - pthread_mutex_lock(&mutex); - while (!callback_executed) { - pthread_cond_wait(&cond, &mutex); - } - callback_executed = 0; - pthread_mutex_unlock(&mutex); -} - -static void free_peerinfo(PeerInfo *pi) { - if (pi == NULL) - return; - - for (size_t i = 0; i < pi->addrCount; i++) - free((void *)pi->addrs[i]); - free(pi->addrs); - pi->addrs = NULL; - pi->addrCount = 0; - pi->peerId[0] = '\0'; -} diff --git a/cbind/ffi_types.nim b/cbind/ffi_types.nim index 9d9b702a38..37a8884b53 100644 --- a/cbind/ffi_types.nim +++ b/cbind/ffi_types.nim @@ -133,7 +133,6 @@ type Libp2pConfig* {.bycopy.} = object mountGossipsub*: cint gossipsubTriggerSelf*: cint mountKad*: cint - mountMix*: cint mountServiceDiscovery*: cint dnsResolver*: cstring addrs*: ptr cstring @@ -176,16 +175,6 @@ type RetCode* {.size: sizeof(cint).} = enum RET_ERR = 1 RET_MISSING_CALLBACK = 2 -type MixReadBehaviorKind* {.size: sizeof(cint).} = enum - MIX_READ_EXACTLY = 0 - MIX_READ_LP = 1 - -type MixCurve25519Key* {.bycopy.} = object - bytes*: array[32, byte] - -type MixSecp256k1PubKey* {.bycopy.} = object - bytes*: array[33, byte] - type Libp2pPeerStoreEntry* {.bycopy.} = object peerId*: cstring addrs*: ptr cstring diff --git a/cbind/libp2p.h b/cbind/libp2p.h index 1fc679f58a..367bd580b1 100644 --- a/cbind/libp2p.h +++ b/cbind/libp2p.h @@ -48,14 +48,9 @@ typedef void (*Libp2pBufferCallback)(int callerRet, const uint8_t *data, // libp2p instance (created by libp2p_new). typedef struct libp2p_ctx libp2p_ctx_t; -// stream handle returned by dial/mix_dial callbacks. +// stream handle returned by dial callbacks. typedef struct libp2p_stream libp2p_stream_t; -// Curve25519 private/public key bytes used by the mix protocol. -typedef struct { - uint8_t bytes[32]; -} libp2p_curve25519_key_t; - // Compressed secp256k1 public key bytes (33 bytes, including prefix). typedef struct { uint8_t bytes[33]; @@ -77,13 +72,6 @@ enum { Direction_Out = 1, }; -typedef uint32_t Libp2pMixReadBehavior; - -enum { - LIBP2P_MIX_READ_EXACTLY = 0, - LIBP2P_MIX_READ_LP = 1, -}; - typedef uint32_t Libp2pMuxer; enum { @@ -141,9 +129,6 @@ typedef struct { // Enable/disable Kademlia DHT (default on). int mount_kad; - // Enable/disable mix protocol support (default off). - int mount_mix; - // Enable Service Discovery (default off). int mount_service_discovery; @@ -520,42 +505,6 @@ int libp2p_service_disco_random_lookup(libp2p_ctx_t *ctx, RandomRecordsCallback callback, void *userData); -// === Mix APIs === - -void libp2p_mix_generate_priv_key(libp2p_curve25519_key_t *outKey); - -void libp2p_mix_public_key(libp2p_curve25519_key_t inKey, - libp2p_curve25519_key_t *outKey); - -int libp2p_mix_dial(libp2p_ctx_t *ctx, const char *peerId, - const char *multiaddr, const char *proto, - ConnectionCallback callback, void *userData); - -int libp2p_mix_dial_with_reply(libp2p_ctx_t *ctx, const char *peerId, - const char *multiaddr, const char *proto, - int expect_reply, uint8_t num_surbs, - ConnectionCallback callback, void *userData); - -// Registers how the exit-layer reads payloads for the given proto. -// behavior + size_param: -// - LIBP2P_MIX_READ_EXACTLY: read exactly size_param bytes -// - LIBP2P_MIX_READ_LP: read length-prefixed frames up to size_param bytes -int libp2p_mix_register_dest_read_behavior(libp2p_ctx_t *ctx, const char *proto, - Libp2pMixReadBehavior behavior, - uint32_t size_param, - Libp2pCallback callback, - void *userData); - -int libp2p_mix_set_node_info(libp2p_ctx_t *ctx, const char *multiaddr, - libp2p_curve25519_key_t mix_priv_key, - Libp2pCallback callback, void *userData); - -int libp2p_mix_nodepool_add(libp2p_ctx_t *ctx, const char *peerId, - const char *multiaddr, - libp2p_curve25519_key_t mix_pub_key, - libp2p_secp256k1_pubkey_t libp2p_pub_key, - Libp2pCallback callback, void *userData); - // callback receives a buffer valid only during its execution int libp2p_public_key(libp2p_ctx_t *ctx, Libp2pBufferCallback callback, void *userData); diff --git a/cbind/libp2p.nim b/cbind/libp2p.nim index df8cd31fbf..e6446fa1e6 100644 --- a/cbind/libp2p.nim +++ b/cbind/libp2p.nim @@ -20,8 +20,7 @@ import libp2p_peerstore_requests, ], ../libp2p, - ../libp2p/crypto/crypto, - ../libp2p/crypto/curve25519 + ../libp2p/crypto/crypto ################################################################################ ### Not-exported components ################################################################################ @@ -169,34 +168,6 @@ proc initializeLibrary() {.exported.} = ################################################################################ ### Exported procs -proc libp2p_mix_generate_priv_key( - outKey: ptr MixCurve25519Key -) {.dynlib, exportc, cdecl.} = - initializeLibrary() - - doAssert(not outKey.isNil(), "outKey is nil") - - var rng = newRng() - let priv = Curve25519Key.random(rng[]) - - for i in 0 ..< Curve25519KeySize: - outKey[].bytes[i] = priv[i] - -proc libp2p_mix_public_key( - inKey: MixCurve25519Key, outKey: ptr MixCurve25519Key -) {.dynlib, exportc, cdecl.} = - initializeLibrary() - - doAssert(not outKey.isNil(), "outKey is nil") - - var priv: Curve25519Key - for i in 0 ..< Curve25519KeySize: - priv[i] = inKey.bytes[i] - - let pub = public(priv) - for i in 0 ..< Curve25519KeySize: - outKey[].bytes[i] = pub[i] - proc libp2p_create_cid( version: cuint, multicodec: cstring, @@ -522,175 +493,6 @@ proc libp2p_mount_protocol( return RET_OK.cint -proc libp2p_mix_dial( - ctx: ptr LibP2PContext, - peerId: cstring, - multiaddr: cstring, - proto: cstring, - callback: ConnectionCallback, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibParams(ctx, callback, userData) - - if peerId.isNil() or multiaddr.isNil() or proto.isNil(): - let msg = "peerId, multiaddr, or proto is nil" - callback(RET_ERR.cint, nil, msg[0].addr, cast[csize_t](len(msg)), userData) - return RET_ERR.cint - - libp2p_thread.sendRequestToLibP2PThread( - ctx, - RequestType.STREAM, - StreamRequest.createShared( - StreamMsgType.MIX_DIAL, peerId = peerId, multiaddr = multiaddr, proto = proto - ), - callback, - userData, - ).isOkOr: - let msg = "libp2p error: " & $error - callback(RET_ERR.cint, nil, msg[0].addr, cast[csize_t](len(msg)), userData) - return RET_ERR.cint - - return RET_OK.cint - -proc libp2p_mix_dial_with_reply( - ctx: ptr LibP2PContext, - peerId: cstring, - multiaddr: cstring, - proto: cstring, - expectReply: cint, - numSurbs: cuint, - callback: ConnectionCallback, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibParams(ctx, callback, userData) - - if peerId.isNil() or multiaddr.isNil() or proto.isNil(): - let msg = "peerId, multiaddr, or proto is nil" - callback(RET_ERR.cint, nil, msg[0].addr, cast[csize_t](len(msg)), userData) - return RET_ERR.cint - - let expect = expectReply != 0 - let surbs = (if numSurbs > high(uint8).cuint: high(uint8).cuint - else: numSurbs) - - libp2p_thread.sendRequestToLibP2PThread( - ctx, - RequestType.STREAM, - StreamRequest.createShared( - StreamMsgType.MIX_DIAL, - peerId = peerId, - multiaddr = multiaddr, - proto = proto, - mixExpectReply = expect, - mixNumSurbs = surbs.uint8, - ), - callback, - userData, - ).isOkOr: - let msg = "libp2p error: " & $error - callback(RET_ERR.cint, nil, msg[0].addr, cast[csize_t](len(msg)), userData) - return RET_ERR.cint - - return RET_OK.cint - -proc libp2p_mix_register_dest_read_behavior( - ctx: ptr LibP2PContext, - proto: cstring, - behavior: MixReadBehaviorKind, - sizeParam: cuint, - callback: Libp2pCallback, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibParams(ctx, callback, userData) - - if proto.isNil(): - let msg = "proto is nil" - callback(RET_ERR.cint, msg[0].addr, cast[csize_t](len(msg)), userData) - return RET_ERR.cint - - libp2p_thread.sendRequestToLibP2PThread( - ctx, - RequestType.STREAM, - StreamRequest.createShared( - StreamMsgType.MIX_REGISTER_DEST_READ, - proto = proto, - mixReadBehaviorKind = behavior.cint, - mixReadBehaviorParam = sizeParam.cint, - ), - callback, - userData, - ).isOkOr: - let msg = "libp2p error: " & $error - callback(RET_ERR.cint, msg[0].addr, cast[csize_t](len(msg)), userData) - return RET_ERR.cint - - return RET_OK.cint - -proc libp2p_mix_set_node_info( - ctx: ptr LibP2PContext, - multiaddr: cstring, - mixPrivKey: MixCurve25519Key, - callback: Libp2pCallback, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibParams(ctx, callback, userData) - if multiaddr.isNil(): - failWithMsg(callback, userData, "multiaddr is nil") - - libp2p_thread.sendRequestToLibP2PThread( - ctx, - RequestType.STREAM, - StreamRequest.createShared( - StreamMsgType.MIX_SET_NODE_INFO, - multiaddr = multiaddr, - data = cast[ptr byte](unsafeAddr mixPrivKey.bytes[0]), - dataLen = Curve25519KeySize.csize_t, - ), - callback, - userData, - ).isOkOr: - failWithMsg(callback, userData, "libp2p error: " & $error) - - RET_OK.cint - -proc libp2p_mix_nodepool_add( - ctx: ptr LibP2PContext, - peerId: cstring, - multiaddr: cstring, - mixPubKey: MixCurve25519Key, - libp2pPubKey: MixSecp256k1PubKey, - callback: Libp2pCallback, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibParams(ctx, callback, userData) - - if peerId.isNil(): - failWithMsg(callback, userData, "peerId is nil") - if multiaddr.isNil(): - failWithMsg(callback, userData, "multiaddr is nil") - - libp2p_thread.sendRequestToLibP2PThread( - ctx, - RequestType.STREAM, - StreamRequest.createShared( - StreamMsgType.MIX_NODEPOOL_ADD, - peerId = peerId, - multiaddr = multiaddr, - mixPubKey = mixPubKey, - libp2pPubKey = libp2pPubKey, - ), - callback, - userData, - ).isOkOr: - failWithMsg(callback, userData, "libp2p error: " & $error) - - RET_OK.cint - proc libp2p_public_key( ctx: ptr LibP2PContext, callback: Libp2pBufferCallback, userData: pointer ): cint {.dynlib, exportc, cdecl.} = diff --git a/cbind/libp2p_thread/inter_thread_communication/libp2p_thread_request.nim b/cbind/libp2p_thread/inter_thread_communication/libp2p_thread_request.nim index 8e28f20e56..cd06977525 100644 --- a/cbind/libp2p_thread/inter_thread_communication/libp2p_thread_request.nim +++ b/cbind/libp2p_thread/inter_thread_communication/libp2p_thread_request.nim @@ -440,14 +440,6 @@ proc processStream( handleConnectionRes(await req.processDial(libp2p), request) of StreamMsgType.DIAL_CIRCUIT_RELAY: handleConnectionRes(await req.processDialCircuitRelay(libp2p), request) - of StreamMsgType.MIX_DIAL: - handleConnectionRes(await req.processMixDial(libp2p), request) - of StreamMsgType.MIX_REGISTER_DEST_READ: - handleRes(await req.processMixRegisterDestRead(libp2p), request) - of StreamMsgType.MIX_SET_NODE_INFO: - handleRes(await req.processMixSetNodeInfo(libp2p), request) - of StreamMsgType.MIX_NODEPOOL_ADD: - handleRes(await req.processMixNodePoolAdd(libp2p), request) of StreamMsgType.CLOSE, StreamMsgType.CLOSE_WITH_EOF: handleRes(await req.processClose(libp2p), request) of StreamMsgType.RELEASE: diff --git a/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_lifecycle_requests.nim b/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_lifecycle_requests.nim index 71ec53a83a..39c85d4936 100644 --- a/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_lifecycle_requests.nim +++ b/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_lifecycle_requests.nim @@ -20,9 +20,6 @@ import ../../../../libp2p/protocols/kademlia import ../../../../libp2p/protocols/protocol import ../../../../libp2p/protocols/service_discovery import ../../../../libp2p/protocols/ping -import ../../../../libp2p/protocols/mix -import ../../../../libp2p/protocols/mix/mix_protocol -import ../../../../libp2p/protocols/mix/mix_node import ../../../../libp2p/protocols/connectivity/relay/client const DefaultDnsResolver = "1.1.1.1:53" @@ -199,14 +196,6 @@ proc mountKad(libp2p: var LibP2P, config: Libp2pConfig) = kad = Opt.some(k) libp2p.kad = kad -proc mountMix(libp2p: var LibP2P, config: Libp2pConfig) = - var mix = Opt.none(MixProtocol) - if config.mountMix != 0 and libp2p.mixNodeInfo.isSome: - var mixProto = MixProtocol.new(libp2p.mixNodeInfo.get(), libp2p.switch) - libp2p.switch.mount(mixProto) - mix = Opt.some(mixProto) - libp2p.mix = mix - proc mountProtocols(libp2p: var LibP2P, config: Libp2pConfig) = if config.mountGossipsub != 0: libp2p.mountGossipsub(config) @@ -215,8 +204,6 @@ proc mountProtocols(libp2p: var LibP2P, config: Libp2pConfig) = libp2p.switch.mount(Ping.new()) - libp2p.mountMix(config) - proc createLibp2p(appCallbacks: AppCallbacks, config: Libp2pConfig): LibP2P = let dnsResolver = Opt.some(cast[NameResolver](DnsResolver.new(@[initTAddress($config.dnsResolver)]))) @@ -284,8 +271,6 @@ proc createLibp2p(appCallbacks: AppCallbacks, config: Libp2pConfig): LibP2P = switch: switch, gossipSub: Opt.none(GossipSub), kad: Opt.none(KadDHT), - mix: Opt.none(MixProtocol), - mixNodeInfo: Opt.none(MixNodeInfo), relayClient: relayClientOpt, topicHandlers: initTable[PubsubTopicPair, TopicHandlerEntry](), connections: initTable[ptr Libp2pStream, Connection](), @@ -303,7 +288,6 @@ proc init*(T: typedesc[Libp2pConfig]): T = mountGossipsub: 1, gossipsubTriggerSelf: 1, mountKad: 1, - mountMix: 0, mountServiceDiscovery: 0, dnsResolver: DefaultDnsResolver.alloc(), addrs: nil, @@ -337,7 +321,6 @@ proc copyConfig(config: ptr Libp2pConfig): Libp2pConfig = resolved.mountGossipsub = config[].mountGossipsub resolved.gossipsubTriggerSelf = config[].gossipsubTriggerSelf resolved.mountKad = config[].mountKad - resolved.mountMix = config[].mountMix resolved.mountServiceDiscovery = config[].mountServiceDiscovery resolved.muxer = config[].muxer resolved.transport = config[].transport diff --git a/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_stream_requests.nim b/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_stream_requests.nim index 6bd29b3673..6add2cb6c3 100644 --- a/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_stream_requests.nim +++ b/cbind/libp2p_thread/inter_thread_communication/requests/libp2p_stream_requests.nim @@ -5,19 +5,10 @@ import std/tables import chronos, results import ../../../[alloc, ffi_types, types] import ../../../../libp2p -import ../../../../libp2p/crypto/curve25519 -import ../../../../libp2p/crypto/secp -import ../../../../libp2p/protocols/mix -import ../../../../libp2p/protocols/mix/curve25519 as mix_curve25519 -import ../../../../libp2p/protocols/mix/mix_node type StreamMsgType* = enum DIAL DIAL_CIRCUIT_RELAY - MIX_DIAL - MIX_REGISTER_DEST_READ - MIX_SET_NODE_INFO - MIX_NODEPOOL_ADD CLOSE CLOSE_WITH_EOF RELEASE @@ -31,13 +22,7 @@ type StreamRequest* = object peerId: cstring multiaddr: cstring proto: cstring - mixReadBehaviorKind: cint - mixReadBehaviorParam: cint - mixExpectReply: bool - mixNumSurbs: uint8 connHandle: ptr Libp2pStream - mixPubKey: MixCurve25519Key - libp2pPubKey: MixSecp256k1PubKey data: SharedSeq[byte] ## Only used for WRITE/WRITELP readLen: csize_t ## Only used for READEXACTLY maxSize: int64 ## Only used for READLP @@ -48,13 +33,7 @@ proc createShared*( peerId: cstring = "", multiaddr: cstring = "", proto: cstring = "", - mixReadBehaviorKind: cint = MIX_READ_EXACTLY.cint, - mixReadBehaviorParam: cint = 0, - mixExpectReply: bool = false, - mixNumSurbs: uint8 = 0, conn: ptr Libp2pStream = nil, - mixPubKey: MixCurve25519Key = default(MixCurve25519Key), - libp2pPubKey: MixSecp256k1PubKey = default(MixSecp256k1PubKey), data: ptr byte = nil, dataLen: csize_t = 0, readLen: csize_t = 0, @@ -65,13 +44,7 @@ proc createShared*( ret[].peerId = peerId.alloc() ret[].multiaddr = multiaddr.alloc() ret[].proto = proto.alloc() - ret[].mixReadBehaviorKind = mixReadBehaviorKind - ret[].mixReadBehaviorParam = mixReadBehaviorParam - ret[].mixExpectReply = mixExpectReply - ret[].mixNumSurbs = mixNumSurbs ret[].connHandle = conn - ret[].mixPubKey = mixPubKey - ret[].libp2pPubKey = libp2pPubKey ret[].data = allocSharedSeqFromCArray(data, dataLen.int) ret[].readLen = readLen ret[].maxSize = maxSize @@ -126,145 +99,6 @@ proc processDialCircuitRelay*( libp2p[].connections[handle] = conn return ok(handle) -proc processMixRegisterDestRead*( - self: ptr StreamRequest, libp2p: ptr LibP2P -): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - defer: - destroyShared(self) - - let mixProto = libp2p[].mix.valueOr: - return err("mix protocol is not mounted") - - if self[].proto.isNil() or self[].proto[0] == '\0': - return err("proto is empty") - - let sizeParam = self[].mixReadBehaviorParam.int - if sizeParam < 0: - return err("size must be >= 0") - case MixReadBehaviorKind(self[].mixReadBehaviorKind) - of MIX_READ_EXACTLY: - mixProto.registerDestReadBehavior($self[].proto, readExactly(sizeParam)) - of MIX_READ_LP: - mixProto.registerDestReadBehavior($self[].proto, readLp(sizeParam)) - - return ok() - -proc processMixDial*( - self: ptr StreamRequest, libp2p: ptr LibP2P -): Future[Result[ptr Libp2pStream, string]] {.async: (raises: [CancelledError]).} = - defer: - destroyShared(self) - - let mixProto = libp2p[].mix.valueOr: - return err("mix protocol is not mounted") - - let peerId = PeerId.init($self[].peerId).valueOr: - return err($error) - let maddr = MultiAddress.init($self[].multiaddr).valueOr: - return err($error) - - var params = MixParameters() - if self[].mixExpectReply: - params.expectReply = Opt.some(true) - if self[].mixNumSurbs > 0: - params.numSurbs = Opt.some(self[].mixNumSurbs) - - let conn = mixProto.toConnection( - MixDestination.init(peerId, maddr), $self[].proto, params - ).valueOr: - return err(error) - - let handle = cast[ptr Libp2pStream](createShared(Libp2pStream, 1)) - handle[].conn = cast[pointer](conn) - libp2p[].connections[handle] = conn - - return ok(handle) - -proc processMixSetNodeInfo*( - self: ptr StreamRequest, libp2p: ptr LibP2P -): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - defer: - destroyShared(self) - - if self[].multiaddr.isNil() or self[].multiaddr[0] == '\0': - return err("node multiaddr is empty") - - if self[].data.len != mix_curve25519.FieldElementSize: - return err("mix private key must be " & $mix_curve25519.FieldElementSize & " bytes") - - let mixPrivKeyBytes = self[].data.toSeq - let mixPrivKey = bytesToFieldElement(mixPrivKeyBytes).valueOr: - return err("mix private key is not a valid priv key") - - let mixPubKey = public(mixPrivKey) - - let nodeAddr = MultiAddress.init($self[].multiaddr).valueOr: - return err($error) - - let peerInfo = libp2p[].switch.peerInfo - if peerInfo.isNil(): - return err("switch peerInfo is nil") - - let libp2pPubKey = - case peerInfo.publicKey.scheme - of PKScheme.Secp256k1: - peerInfo.publicKey.skkey - else: - return err("peerInfo public key must be secp256k1") - - let libp2pPrivKey = - case peerInfo.privateKey.scheme - of PKScheme.Secp256k1: - peerInfo.privateKey.skkey - else: - return err("peerInfo private key must be secp256k1") - - libp2p[].mixNodeInfo = Opt.some( - initMixNodeInfo( - peerInfo.peerId, nodeAddr, mixPubKey, mixPrivKey, libp2pPubKey, libp2pPrivKey - ) - ) - - if libp2p[].mix.isNone: - var mixProto = MixProtocol.new(libp2p[].mixNodeInfo.get(), libp2p[].switch) - try: - await mixProto.start() - libp2p[].switch.mount(mixProto) - except LPError as exc: - return err("could not mount mix: " & exc.msg) - - libp2p[].mix = Opt.some(mixProto) - - return ok() - -proc processMixNodePoolAdd*( - self: ptr StreamRequest, libp2p: ptr LibP2P -): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - defer: - destroyShared(self) - - let mixProto = libp2p[].mix.valueOr: - return err("mix protocol is not mounted") - - if self[].peerId.isNil() or self[].peerId[0] == '\0': - return err("peerId is empty") - if self[].multiaddr.isNil() or self[].multiaddr[0] == '\0': - return err("multiaddr is empty") - - let peerId = PeerId.init($self[].peerId).valueOr: - return err($error) - let maddr = MultiAddress.init($self[].multiaddr).valueOr: - return err($error) - - let mixPubKey = mix_curve25519.bytesToFieldElement(self[].mixPubKey.bytes).valueOr: - return err("mix public key invalid: " & error) - - let libp2pPubKey = SkPublicKey.init(self[].libp2pPubKey.bytes).valueOr: - return err("libp2p public key invalid") - - mixProto.nodePool.add(MixPubInfo.init(peerId, maddr, mixPubKey, libp2pPubKey)) - return ok() - proc processClose*( self: ptr StreamRequest, libp2p: ptr LibP2P ): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = diff --git a/cbind/types.nim b/cbind/types.nim index 1b63a013ba..936ee72118 100644 --- a/cbind/types.nim +++ b/cbind/types.nim @@ -8,8 +8,6 @@ import ../libp2p import ../libp2p/protocols/protocol import ../libp2p/protocols/pubsub/gossipsub import ../libp2p/protocols/kademlia -import ../libp2p/protocols/mix -import ../libp2p/protocols/mix/mix_node import ../libp2p/protocols/connectivity/relay/client import ffi_types @@ -28,8 +26,6 @@ type LibP2P* = ref object switch*: Switch gossipSub*: Opt[GossipSub] kad*: Opt[KadDHT] - mix*: Opt[MixProtocol] - mixNodeInfo*: Opt[MixNodeInfo] relayClient*: Opt[RelayClient] topicHandlers*: Table[PubsubTopicPair, TopicHandlerEntry] connections*: Table[ptr Libp2pStream, Connection] diff --git a/docs/README.md b/docs/README.md index 3017c9ebcd..f44ddd9251 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,9 +8,6 @@ Welcome to the nim-libp2p documentation. Use the links below to navigate: - [Common Hurdles](common_hurdles.md) - [Contributing](contributing.md) - [Compile Time Flags](compile_time_flags.md) -- Protocols - - [Mix Protocol](protocols_mix.md) - - [Spam Protection](protocols_mix_spam_protection.md) - [Hole Punching Interop](interop_hole_punching.md) --- diff --git a/docs/development.md b/docs/development.md index d96c27069e..571d036698 100644 --- a/docs/development.md +++ b/docs/development.md @@ -63,7 +63,6 @@ nimble test # - Full path: "libp2p/transports/test_tcp" matches libp2p/transports/test_tcp.nim nimble testpath quic nimble testpath transports/test_ws -nimble testpath mix # etc ... # run specific test suites diff --git a/docs/protocols_mix.md b/docs/protocols_mix.md deleted file mode 100644 index 5f3c4b32c2..0000000000 --- a/docs/protocols_mix.md +++ /dev/null @@ -1,114 +0,0 @@ -[← Back to README](../README.md) - -# Mix - -A custom protocol designed to enable anonymous communication in peer-to-peer networks. - -## Overview - -The Mix protocol leverages the Sphinx packet format to ensure sender anonymity and message unlinkability. -It routes messages through a series of mix nodes, effectively concealing the origin of the message. -This implementation is part of a broader effort to integrate anonymity into the libp2p ecosystem. - -**Note:** This is a proof-of-concept, not production-ready code. -It serves as a foundation for further development and research into anonymous communication within libp2p networks. -It provides a basis for future development and invites community experimentation and contributions. - -## Key Features - -- **Sphinx Packet Format**: Guarantees anonymity through fixed-size packets and layered encryption. -- **Random Path Selection**: Routes messages through randomly selected mix nodes. -- **Spam Protection Interface**: Standardized interface for integrating spam protection mechanisms. See [Spam Protection](protocols_mix_spam_protection.md) for details. -- **Pluggable Components**: Allows for customizable peer discovery and incentivization mechanisms. (To be developed) - -## Usage - -```nim -let mixProto = MixProtocol.new(index, numberOfNodes, switch).valueOr: - error "Mix protocol initialization failed", err = error - return - -# Exit node will forward any message it receives to its destination, -# but if the protocol requires reading a response, we need to -# register how should the exit node will read them. -# Use either readExactly or readLp. -# In this example we assume we're gonna use Ping protocol -mixProto.registerDestReadBehavior(PingCodec, readExactly(32)) - -let pingProto = Ping.new() - -switch.mount(mixProto) -switch.mount(pingProto) - -await switch.start() - -let conn = mixProto.toConnection( - MixDestination.init(thePeerId, theMultiAddr[0]), - PingCodec, - MixParameters(expectReply: Opt.some(true)), - ).valueOr: - error "Could not obtain connection", err = error - return - -let response = await pingProto.ping(conn) -``` - -## Spam Protection - -The Mix protocol includes a flexible spam protection interface that allows custom mechanisms to be integrated. By default, spam protection is disabled (nil). - -```nim -# Create a custom spam protection instance -let spamProtection = MySpamProtection.new() - -# Initialize MixProtocol with spam protection -let mixProto = MixProtocol.new( - mixNodeInfo, - switch, - spamProtection = Opt.some(spamProtection) -) -``` - -For detailed information on implementing custom spam protection mechanisms, see [Spam Protection](protocols_mix_spam_protection.md). - -## Using experimental `exit == destination` - -1. Compile with: `-d:libp2p_mix_experimental_exit_is_dest` -2. In `toConnection` you can now specify the behavior the exit node will have: - -```nim -# Exit != destination (the default) -# The exit node will forward the request to the destination -# You can also use MixDestination.init instead -let theDestination = MixDestination.forwardToAddr(thePeerId, theMultiAddress) -let conn = mixProto.toConnection( - theDestination, - theCodec, - ).expect("should build connection") - - -# Exit == destination -# The protocol handler will be executed at the exit node -let theDestination = MixDestination.exitNode(thePeerId) -let conn = mixProto.toConnection( - theDestination, - theCodec, - ).expect("should build connection") -``` - -## Example - -A complete working example demonstrating the Mix protocol with Ping can be found at [examples/mix_ping.nim](../examples/mix_ping.nim). - -## RFC and Further Reading - -For a detailed technical specification and discussion, please refer to the [Mix Protocol RFC](https://lip.logos.co/ift-ts/raw/mix.html). - -## Acknowledgments - -Thanks to the libp2p community and all contributors for their feedback and insights throughout the development -of the Mix protocol. - ---- - -[← Back to README](../README.md) diff --git a/docs/protocols_mix_spam_protection.md b/docs/protocols_mix_spam_protection.md deleted file mode 100644 index 8b851d8771..0000000000 --- a/docs/protocols_mix_spam_protection.md +++ /dev/null @@ -1,44 +0,0 @@ -[← Mix Protocol](protocols_mix.md) - -# Spam Protection Interface - -A pluggable interface for integrating spam protection mechanisms into the Mix protocol, as specified in [section 9.6 of the MIX specification](https://lip.logos.co/ift-ts/raw/mix.html#96-spam-protection-interface). - -## Architecture - -Currently, only **Per-Hop Generation** is implemented, where each mix node generates fresh proofs for the next hop. - -## Packet Structure - -Proofs are appended after the Sphinx packet: - -``` -Wire Format: [Sphinx Packet: 4608 bytes][Proof: proofSize bytes] -``` - -## Disabling Spam Protection - -To disable spam protection, simply pass `nil` as the `spamProtection` parameter when initializing MixProtocol. - -When spam protection is disabled (nil): - -- No proofs are generated or verified -- Wire packet size equals Sphinx packet size (4608 bytes) - -## Example Implementations - -See [test_spam_protection_interface.nim](../tests/libp2p/mix/test_spam_protection_interface.nim) for example implementations: - -- **Proof-of-Work**: Requires computational work to generate proofs -- **Rate Limiting**: Tracks and limits packet rates per node - -## Further Reading - -For detailed specification and security considerations, see: - -- [MIX Protocol Specification](https://lip.logos.co/ift-ts/raw/mix.html) -- [Spam Protection Interface (Section 9.6)](https://lip.logos.co/ift-ts/raw/mix.html#96-spam-protection-interface) - ---- - -[← Back to README](../README.md) diff --git a/examples/examples_run.nim b/examples/examples_run.nim index 6a06332749..313d8b98fc 100644 --- a/examples/examples_run.nim +++ b/examples/examples_run.nim @@ -3,4 +3,4 @@ import helloworld, circuitrelay, tutorial_1_connect, tutorial_2_customproto, tutorial_3_protobuf, tutorial_4_gossipsub, tutorial_5_connmanager, - tutorial_6_peerscoring, mix_ping + tutorial_6_peerscoring diff --git a/examples/mix_ping.nim b/examples/mix_ping.nim deleted file mode 100644 index 1de13f9fc1..0000000000 --- a/examples/mix_ping.nim +++ /dev/null @@ -1,97 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -## Mix Protocol Ping Example -## -## This example demonstrates using the Mix protocol with the Ping protocol. -## It creates a set of mix nodes that form an anonymous overlay network, -## then sends a ping through the mix network to a destination node and -## receives the response via Single Use Reply Blocks (SURBs). - -{.used.} - -import chronicles, chronos, results -import std/[strformat, sequtils] -import - ../libp2p/[ - protocols/mix, - protocols/mix/mix_protocol, - protocols/mix/curve25519, - protocols/ping, - peerid, - multiaddress, - switch, - builders, - crypto/crypto, - crypto/secp, - ] - -const NumMixNodes = 10 - -proc createSwitch( - multiAddr: MultiAddress, libp2pPrivKey: Opt[SkPrivateKey] = Opt.none(SkPrivateKey) -): Switch = - var rng = newRng() - let skkey = libp2pPrivKey.valueOr(SkKeyPair.random(rng[]).seckey) - let privKey = PrivateKey(scheme: Secp256k1, skkey: skkey) - newStandardSwitchBuilder(privKey = Opt.some(privKey), addrs = multiAddr).build() - -proc mixPingSimulation() {.async: (raises: [Exception]).} = - let mixNodeInfos = MixNodeInfo.generateRandomMany(NumMixNodes) - var switches: seq[Switch] = @[] - var mixProtos: seq[MixProtocol] = @[] - - # Set up mix protocols on each mix node - for nodeInfo in mixNodeInfos: - var switch = createSwitch(nodeInfo.multiAddr, Opt.some(nodeInfo.libp2pPrivKey)) - let proto = MixProtocol.new(nodeInfo, switch) - - # Populate nodePool with all other nodes' public info - proto.nodePool.add(mixNodeInfos.includeAllExcept(nodeInfo)) - - # Register how to read ping responses (32 bytes exactly) - proto.registerDestReadBehavior(PingCodec, readExactly(32)) - switch.mount(proto) - - switches.add(switch) - mixProtos.add(proto) - - defer: - await switches.mapIt(it.stop()).allFutures() - - # Create a destination node (not part of the mix network) - let destNode = createSwitch(MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet()) - defer: - await destNode.stop() - - let pingProto = Ping.new() - destNode.mount(pingProto) - - # Start all switches - await switches.mapIt(it.start()).allFutures() - await destNode.start() - - # Pick sender (first mix node) and send ping through the mix network - let senderIndex = 0 - - info "Sending ping through mix network", - sender = switches[senderIndex].peerInfo.peerId, - destination = destNode.peerInfo.peerId - - # Create a connection through the mix network - let conn = mixProtos[senderIndex] - .toConnection( - MixDestination.init(destNode.peerInfo.peerId, destNode.peerInfo.addrs[0]), - PingCodec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - .expect("could not build connection") - - # Send ping and wait for response through the mix network - let response = await pingProto.ping(conn) - await conn.close() - - info "Ping response received through mix network", rtt = response - -when isMainModule: - waitFor(mixPingSimulation()) diff --git a/libp2p.nimble b/libp2p.nimble index 4ac3616dfe..ab3fac6c9d 100644 --- a/libp2p.nimble +++ b/libp2p.nimble @@ -32,7 +32,6 @@ proc runTest(filename: string, moreoptions: string = "") = if getEnv("CICOV").len > 0: compileCmd &= " --nimcache:nimcache/" & filename & "-" & $compileCmd.hash compileCmd &= " -d:libp2p_autotls_support" - compileCmd &= " -d:libp2p_mix_experimental_exit_is_dest" compileCmd &= " " & moreoptions & " " var runnerArgs = " --output-level=VERBOSE" diff --git a/libp2p/peerstore.nim b/libp2p/peerstore.nim index db7e3e80ac..cf21f1ce9b 100644 --- a/libp2p/peerstore.nim +++ b/libp2p/peerstore.nim @@ -21,7 +21,6 @@ import std/[tables, sets, macros, sequtils], chronos, ./crypto/crypto, - ./crypto/curve25519, ./protocols/identify, ./protocols/protocol, ./peerid, @@ -64,9 +63,6 @@ type ProtoVersionBook* = ref object of PeerBook[string] SPRBook* = ref object of PeerBook[Envelope] - MixPubKeyBook* = ref object of PeerBook[Curve25519Key] - ## Keeps track of Mix protocol public keys of peers - #################### # Peer store types # #################### diff --git a/libp2p/protocols/mix.nim b/libp2p/protocols/mix.nim deleted file mode 100644 index abad792092..0000000000 --- a/libp2p/protocols/mix.nim +++ /dev/null @@ -1,67 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import - ./mix/[ - mix_protocol, mix_node, entry_connection, exit_layer, spam_protection, - delay_strategy, pool, - ] -import ../stream/connection -import chronos -import ../utils/sequninit - -export toConnection -export MixProtocolID -export MixProtocol - -export get -export `new` -export init -export getMaxMessageSizeForCodec -export MixDestination -export MixParameters -export destReadBehaviorCb -export DestReadBehavior -export registerDestReadBehavior - -# Spam protection exports -export SpamProtection -export generateProof -export verifyProof - -export NoSamplingDelayStrategy -export ExponentialDelayStrategy -export SpamProtectionDelayStrategy - -export mix_node - -export MixNodePool -export add -export remove -export peerIds - -proc readLp*(maxSize: int): DestReadBehavior = - ## Create a read behavior that reads length-prefixed messages (varint-encoded length). - ## The exit layer will automatically restore the length prefix for the reply. - let callback = proc( - conn: Connection - ): Future[seq[byte]] {.async: (raises: [CancelledError, LPStreamError]).} = - await conn.readLp(maxSize) - - DestReadBehavior(callback: callback, usesLengthPrefix: true) - -proc readExactly*(nBytes: int): DestReadBehavior = - ## Create a read behavior that reads exactly nBytes without any length prefix. - ## The exit layer will not add a length prefix to the reply. - let callback = proc( - conn: Connection - ): Future[seq[byte]] {.async: (raises: [CancelledError, LPStreamError]).} = - let buf = newSeqUninit[byte](nBytes) - await conn.readExactly(addr buf[0], nBytes) - return buf - - DestReadBehavior(callback: callback, usesLengthPrefix: false) - -when defined(libp2p_mix_experimental_exit_is_dest): - export exitNode - export forwardToAddr diff --git a/libp2p/protocols/mix/benchmark.nim b/libp2p/protocols/mix/benchmark.nim deleted file mode 100644 index 5b57de8e5a..0000000000 --- a/libp2p/protocols/mix/benchmark.nim +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import chronicles -import ../../peerid -import times -import stew/endians2 - -const MetadataSize* = 16 - -type Metadata* = object - orig: uint64 - msgId: uint64 - -proc benchmarkLog*( - eventName: static[string], - myPeerId: PeerId, - startTime: Time, - metadata: Metadata, - fromPeerId: Opt[PeerId], - toPeerId: Opt[PeerId], -) = - let endTime = getTime() - let procDelay = (endTime - startTime).inMilliseconds() - info eventName, - msgId = metadata.msgId, - fromPeerId, - toPeerId, - myPeerId, - orig = metadata.orig, - current = startTime, - procDelay - -proc deserialize*(T: typedesc[Metadata], data: seq[byte]): T = - doAssert data.len >= MetadataSize - T(orig: uint64.fromBytesLE(data[0 ..< 8]), msgId: uint64.fromBytesLE(data[8 ..< 16])) - -proc serialize*(meta: Metadata): seq[byte] = - @(meta.orig.toBytesLE()) & @(meta.msgId.uint64.toBytesLE()) diff --git a/libp2p/protocols/mix/cover_traffic.nim b/libp2p/protocols/mix/cover_traffic.nim deleted file mode 100644 index f45b629de0..0000000000 --- a/libp2p/protocols/mix/cover_traffic.nim +++ /dev/null @@ -1,436 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -## Pluggable cover traffic interface and strategies for the Mix Protocol. -## -## Cover traffic ensures sender unobservability by emitting dummy Sphinx packets -## at a constant rate, making it impossible for an observer to distinguish cover -## traffic from locally originated messages. -## -## See Mix Cover Traffic specification sections 3-7. - -import std/deques -import chronicles, chronos, results, metrics -import ../../[multiaddress, peerid] -import ../../utils/heartbeat -import ./mix_metrics, ./sphinx - -logScope: - topics = "libp2p mix covertraffic" - -# Callback types injected by MixProtocol to avoid circular dependency. -# CoverTraffic lives in a separate module and cannot import MixProtocol -# (that would create a circular import). The callbacks are the only way -# to break this cycle while letting CoverTraffic build and send packets -# through MixProtocol's infrastructure (nodePool, sphinx, spam protection). -type - CoverPacketBuild* = object - packet*: seq[byte] - firstHopPeerId*: PeerId - firstHopAddr*: MultiAddress - proofToken*: seq[byte] - - BuildCoverPacketProc* = - proc(): Result[CoverPacketBuild, string] {.gcsafe, raises: [].} - - SendCoverPacketProc* = proc( - peerId: PeerId, multiAddr: MultiAddress, packet: seq[byte] - ): Future[Result[void, string]] {.async: (raises: [CancelledError]).} - -type - CoverPacket* = object - packet*: seq[byte] - firstHopPeerId*: PeerId - firstHopAddr*: MultiAddress - proofToken*: seq[byte] ## Opaque token from spam protection proof generation - - ClaimResult* = object - success*: bool - reclaimedToken*: seq[byte] ## Proof token from discarded cover packet (empty if none) - -type SlotPool* = ref object - ## Manages the R-slot budget per epoch. All traffic types (cover, local - ## origination, forwarding) draw from the same pool. - epoch*: uint64 - totalSlots: int - coverQueue: Deque[CoverPacket] - coverClaimed*: int - nonCoverClaimed*: int - -proc new*(T: typedesc[SlotPool], totalSlots: int): T = - doAssert totalSlots > 0, "SlotPool totalSlots must be greater than zero" - T(epoch: 0, totalSlots: totalSlots, coverQueue: initDeque[CoverPacket]()) - -proc beginEpoch*(pool: SlotPool, epoch: uint64) = - ## Clear stale cover packets and refill slots for the new epoch. - ## Precomputed packets from the previous epoch are discarded because - ## their proofs belong to the old epoch. - pool.epoch = epoch - pool.coverQueue = initDeque[CoverPacket]() - pool.coverClaimed = 0 - pool.nonCoverClaimed = 0 - -func availableSlots*(pool: SlotPool): int {.inline.} = - pool.totalSlots - pool.coverClaimed - pool.nonCoverClaimed - -func hasAvailableSlots*(pool: SlotPool): bool {.inline.} = - pool.availableSlots > 0 - -proc claimSlotForCover*(pool: SlotPool): bool = - if not pool.hasAvailableSlots(): - return false - pool.coverClaimed += 1 - true - -proc claimSlot*(pool: SlotPool): ClaimResult = - ## Claim a slot for non-cover use. Discards one pre-built cover packet - ## and returns its proof token for potential reuse (Mix Cover Traffic spec §5.2). - if not pool.hasAvailableSlots(): - return ClaimResult(success: false) - - pool.nonCoverClaimed += 1 - var token: seq[byte] - if pool.coverQueue.len > 0: - let discarded = pool.coverQueue.popFirst() - token = discarded.proofToken - ClaimResult(success: true, reclaimedToken: token) - -func totalSlots*(pool: SlotPool): int {.inline.} = - pool.totalSlots - -proc addPacket*(pool: SlotPool, pkt: CoverPacket) = - pool.coverQueue.addLast(pkt) - -func queuedCount*(pool: SlotPool): int {.inline.} = - pool.coverQueue.len - -proc dequeue*(pool: SlotPool): Opt[CoverPacket] {.inline.} = - if pool.coverQueue.len == 0: - return Opt.none(CoverPacket) - Opt.some(pool.coverQueue.popFirst()) - -type - ValidateProofTokenProc* = proc(token: seq[byte]): bool {.gcsafe, raises: [].} - ## Callback to check if a prebuilt proof is still valid (e.g., Merkle root - ## still in the acceptable window). Returns true if valid, false if stale. - - ReclaimProofTokenProc* = proc(token: seq[byte]) {.gcsafe, raises: [].} - ## Callback to return a proof token for reuse when a prebuilt cover packet - ## is discarded (e.g., due to stale Merkle root). - - CoverTraffic* = ref object of RootObj - ## Abstract base to allow alternate emission strategies (e.g. Poisson-Rate). - ## MixProtocol injects packet building and sending via callback procs. - slotPool*: SlotPool - buildPacket: BuildCoverPacketProc - sendPacket: SendCoverPacketProc - validateProofToken: ValidateProofTokenProc - reclaimProofToken: ReclaimProofTokenProc - -method start*(ct: CoverTraffic) {.base, async: (raises: [CancelledError]).} = - raiseAssert "start must be implemented by concrete cover traffic types" - -method stop*(ct: CoverTraffic) {.base, async: (raises: []).} = - raiseAssert "stop must be implemented by concrete cover traffic types" - -method onEpochChange*(ct: CoverTraffic, epoch: uint64) {.base, gcsafe, raises: [].} = - ct.slotPool.beginEpoch(epoch) - -method onCoverReceived*(ct: CoverTraffic) {.base, gcsafe, raises: [].} = - ## Diagnostics hook when a cover packet loops back (Mix Cover Traffic spec §11.2). - discard - -proc setCoverPacketBuilder*(ct: CoverTraffic, builder: BuildCoverPacketProc) = - ct.buildPacket = builder - -proc setProofTokenValidator*(ct: CoverTraffic, validator: ValidateProofTokenProc) = - ct.validateProofToken = validator - -proc setProofTokenReclaimer*(ct: CoverTraffic, reclaimer: ReclaimProofTokenProc) = - ct.reclaimProofToken = reclaimer - -proc setCoverPacketSender*(ct: CoverTraffic, sender: SendCoverPacketProc) = - ct.sendPacket = sender - -type ConstantRateCoverTraffic* = ref object of CoverTraffic - ## Emits cover packets at a fixed interval derived from - ## `scaledSlots = max(1, floor(f * R))`, giving `((1 + L) * P) / scaledSlots` - ## seconds (with a 1 ms lower bound), where f is the `cover_rate_fraction`. - ## RECOMMENDED as the default strategy (Mix Cover Traffic spec §7.1). - emissionInterval: Duration - epochDuration: Duration - coverRateFraction: float - precomputeTarget: int - enablePrecomputation: bool - precomputeBatchSize: int - emissionLoop: Future[void] - precomputeLoop: Future[void] - epochTimerLoop: Future[void] - emissionEpochEvent: AsyncEvent - precomputeEpochEvent: AsyncEvent - useInternalEpochTimer: bool - running: bool - -proc new*( - T: typedesc[ConstantRateCoverTraffic], - totalSlots: int = 100, - epochDuration: Duration = 60.seconds, - coverRateFraction: float = 0.7, - enablePrecomputation: bool = false, - precomputeBatchSize: int = 0, - useInternalEpochTimer: bool = true, -): T = - ## Parameters: - ## totalSlots: R (rate limit budget per epoch) - ## epochDuration: P (epoch duration) - ## coverRateFraction: f ∈ (0.0, 1.0], scales the cover emission rate - ## relative to the maximum safe rate R / ((1+L) * P). - ## Default (0.7) reserves ~30% headroom for forwarding variance. - ## enablePrecomputation: whether to pre-build cover packets in batches - ## precomputeBatchSize: packets per batch (0 = auto: f * R / (1+L) / 10) - ## useInternalEpochTimer: false when SpamProtection provides OnEpochChange - doAssert totalSlots > 0, "totalSlots (R) must be positive" - doAssert epochDuration > Duration.default, "epochDuration (P) must be positive" - doAssert coverRateFraction > 0.0 and coverRateFraction <= 1.0, - "coverRateFraction (f) must be in (0.0, 1.0]" - - # Effective cover budget after `cover_rate_fraction` scaling: floor(f * R). - # Clamped to at least 1 so very small f values don't produce a zero divisor. - let scaledSlots = max(1, (totalSlots.float * coverRateFraction).int) - - # Time between consecutive cover emissions: ((1 + L) * P) / scaledSlots, - # clamped to at least 1 ms to guard against overly tight scheduling for large R. - # Approximates the continuous rate ((1 + L) * P) / (f * R); differs when - # floor(f * R) < 1 or when f * R is non-integer. - let emissionInterval = - max(1.milliseconds, epochDuration * (1 + PathLength) div scaledSlots) - - # Expected cover emissions per epoch at equilibrium: scaledSlots / (1 + L), - # clamped to at least 1 packet. The remaining slots in R are consumed by - # forwarding and local origination. Approximates (f * R) / (1 + L) with - # integer floor applied at each step. - let precomputeTarget = max(1, scaledSlots div (1 + PathLength)) - - # Default batch size = 10% of precomputeTarget (at least 1), so pre-computation - # spreads across ~10 batches per epoch. - let batchSize = - if precomputeBatchSize > 0: - precomputeBatchSize - else: - max(1, precomputeTarget div 10) - - T( - slotPool: SlotPool.new(totalSlots), - emissionInterval: emissionInterval, - epochDuration: epochDuration, - coverRateFraction: coverRateFraction, - precomputeTarget: precomputeTarget, - enablePrecomputation: enablePrecomputation, - precomputeBatchSize: batchSize, - emissionEpochEvent: newAsyncEvent(), - precomputeEpochEvent: newAsyncEvent(), - useInternalEpochTimer: useInternalEpochTimer, - running: false, - ) - -proc unclaimCoverSlot(ct: ConstantRateCoverTraffic) = - ## Return a cover slot on build failure so it can be retried within the - ## same epoch. Send failures do NOT unclaim — the proof/messageId was - ## already consumed and cannot be reused. - if ct.slotPool.coverClaimed > 0: - ct.slotPool.coverClaimed -= 1 - -proc buildAndSendOnDemand( - ct: ConstantRateCoverTraffic -) {.async: (raises: [CancelledError]).} = - ## Build and send a cover packet on-demand. Assumes slot is already claimed. - let buildRes = ct.buildPacket() - if buildRes.isErr: - trace "Failed to build cover packet", err = buildRes.error - mix_cover_error.inc(labelValues = ["BUILD_FAILED"]) - ct.unclaimCoverSlot() - return - - let built = buildRes.get() - let sendRes = - await ct.sendPacket(built.firstHopPeerId, built.firstHopAddr, built.packet) - if sendRes.isErr: - debug "Failed to send cover packet", err = sendRes.error - mix_cover_error.inc(labelValues = ["SEND_FAILED"]) - else: - mix_cover_emitted.inc(labelValues = ["on_demand"]) - -proc emitCoverPacket*( - ct: ConstantRateCoverTraffic -) {.async: (raises: [CancelledError]).} = - doAssert ct.buildPacket != nil, "buildPacket callback must be set before emitting" - doAssert ct.sendPacket != nil, "sendPacket callback must be set before emitting" - - if ct.enablePrecomputation and ct.slotPool.queuedCount > 0: - if not ct.slotPool.claimSlotForCover(): - mix_slot_claim_rejected.inc(labelValues = ["cover"]) - return - ct.slotPool.dequeue().withValue(pkt): - # Check if the prebuilt proof is still valid (e.g., Merkle root not stale) - if ct.validateProofToken != nil and pkt.proofToken.len > 0 and - not ct.validateProofToken(pkt.proofToken): - trace "Prebuilt cover packet has stale proof, rebuilding on-demand" - mix_cover_error.inc(labelValues = ["STALE_PROOF"]) - # Reclaim the stale proof's messageId so it can be reused - if ct.reclaimProofToken != nil: - ct.reclaimProofToken(pkt.proofToken) - await ct.buildAndSendOnDemand() - return - else: - let sendRes = - await ct.sendPacket(pkt.firstHopPeerId, pkt.firstHopAddr, pkt.packet) - if sendRes.isErr: - debug "Failed to send pre-built cover packet", err = sendRes.error - mix_cover_error.inc(labelValues = ["SEND_FAILED"]) - else: - mix_cover_emitted.inc(labelValues = ["prebuilt"]) - return - - if ct.slotPool.claimSlotForCover(): - await ct.buildAndSendOnDemand() - -proc runEmissionLoop( - ct: ConstantRateCoverTraffic -) {.async: (raises: [CancelledError]).} = - var nextTick = Moment.now() + ct.emissionInterval - while ct.running: - if not ct.slotPool.hasAvailableSlots(): - ct.emissionEpochEvent.clear() - await ct.emissionEpochEvent.wait() - nextTick = Moment.now() - - let now = Moment.now() - if now < nextTick: - await sleepAsync(nextTick - now) - - await ct.emitCoverPacket() - nextTick = nextTick + ct.emissionInterval - -proc runPrecomputeLoop( - ct: ConstantRateCoverTraffic -) {.async: (raises: [CancelledError]).} = - ## Builds cover packets in batches and adds them to the coverQueue for the - ## current epoch (same-epoch precomputation). - ## - ## Packets are built with proofs for the current epoch so their proof tokens - ## can be reclaimed if the packet is discarded before sending. - while ct.running: - let currentEpoch = ct.slotPool.epoch - let targetCount = ct.precomputeTarget - var built = 0 - - while built + ct.slotPool.queuedCount < targetCount and ct.running: - if ct.slotPool.epoch != currentEpoch: - trace "Epoch changed during pre-computation, aborting", - startedEpoch = currentEpoch, currentEpoch = ct.slotPool.epoch - break - - let batchEnd = - min(built + ct.precomputeBatchSize, targetCount - ct.slotPool.queuedCount) - var batchFailed = false - while built < batchEnd: - let buildRes = ct.buildPacket() - if buildRes.isErr: - debug "Pre-computation: failed to build cover packet", err = buildRes.error - mix_cover_error.inc(labelValues = ["BUILD_FAILED"]) - batchFailed = true - break - - let coverBuild = buildRes.get() - ct.slotPool.addPacket( - CoverPacket( - packet: coverBuild.packet, - firstHopPeerId: coverBuild.firstHopPeerId, - firstHopAddr: coverBuild.firstHopAddr, - proofToken: coverBuild.proofToken, - ) - ) - built += 1 - mix_cover_precomputed.inc() - # Yield per packet so other async tasks can proceed - await sleepAsync(1.milliseconds) - - if batchFailed: - break - - # Longer yield between batches - await sleepAsync(10.milliseconds) - - trace "Pre-computation complete", epoch = currentEpoch, totalBuilt = built - - # If epoch changed during precomputation, the event was already fired - # (via onEpochChange → fire). Don't clear+wait or we'd skip an epoch. - if ct.slotPool.epoch != currentEpoch: - continue - - ct.precomputeEpochEvent.clear() - await ct.precomputeEpochEvent.wait() - -proc runEpochTimer(ct: ConstantRateCoverTraffic) {.async: (raises: [CancelledError]).} = - ## Fallback epoch timer when no SpamProtection provides OnEpochChange. - var epochCounter: uint64 = 0 - heartbeat "Cover traffic epoch timer", ct.epochDuration, sleepFirst = true: - epochCounter += 1 - ct.onEpochChange(epochCounter) - -method onEpochChange*( - ct: ConstantRateCoverTraffic, epoch: uint64 -) {.gcsafe, raises: [].} = - procCall CoverTraffic(ct).onEpochChange(epoch) - ct.emissionEpochEvent.fire() - ct.precomputeEpochEvent.fire() - -method start*(ct: ConstantRateCoverTraffic) {.async: (raises: [CancelledError]).} = - if ct.running: - return - - doAssert ct.buildPacket != nil, "buildPacket callback must be set before start" - doAssert ct.sendPacket != nil, "sendPacket callback must be set before start" - - ct.running = true - - ct.emissionLoop = ct.runEmissionLoop() - - if ct.enablePrecomputation: - ct.precomputeLoop = ct.runPrecomputeLoop() - - if ct.useInternalEpochTimer: - ct.epochTimerLoop = ct.runEpochTimer() - - debug "Cover traffic started", - interval = ct.emissionInterval, - precomputation = ct.enablePrecomputation, - internalEpochTimer = ct.useInternalEpochTimer - -proc cancelIfNotNil(fut: Future[void]) {.async: (raises: []).} = - if not fut.isNil: - await fut.cancelAndWait() - -method stop*(ct: ConstantRateCoverTraffic) {.async: (raises: []).} = - if not ct.running: - return - ct.running = false - await cancelIfNotNil(ct.emissionLoop) - await cancelIfNotNil(ct.precomputeLoop) - await cancelIfNotNil(ct.epochTimerLoop) - ct.emissionLoop = nil - ct.precomputeLoop = nil - ct.epochTimerLoop = nil - trace "Cover traffic stopped" - -func emissionInterval*(ct: ConstantRateCoverTraffic): Duration = - ct.emissionInterval - -func precomputeBatchSize*(ct: ConstantRateCoverTraffic): int = - ct.precomputeBatchSize - -func coverRateFraction*(ct: ConstantRateCoverTraffic): float = - ct.coverRateFraction - -func isRunning*(ct: ConstantRateCoverTraffic): bool = - ct.running diff --git a/libp2p/protocols/mix/crypto.nim b/libp2p/protocols/mix/crypto.nim deleted file mode 100644 index 6cb8320b7f..0000000000 --- a/libp2p/protocols/mix/crypto.nim +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import endians, nimcrypto - -proc aes_ctr*(key, iv, data: openArray[byte]): seq[byte] = - ## Processes 'data' using AES in CTR mode. - ## For CTR mode, the same function handles both encryption and decryption. - doAssert key.len == 16, "Key must be 16 bytes for AES-128" - doAssert iv.len == 16, "IV must be 16 bytes for AES-128" - - var - ctx: CTR[aes128] - output = newSeq[byte](data.len) - - ctx.init(key, iv) - ctx.encrypt(data, output) - ctx.clear() - - output - -proc advance_ctr*(iv: var openArray[byte], blocks: uint64) = - ## Advances the counter in the AES-CTR IV by a specified number of blocks. - var counter: uint64 - bigEndian64(addr counter, addr iv[8]) - counter += blocks - bigEndian64(addr iv[8], addr counter) - -proc aes_ctr_start_index*(key, iv, data: openArray[byte], startIndex: int): seq[byte] = - ## Encrypts 'data' using AES in CTR mode from startIndex, without processing all preceding data. - ## For CTR mode, the same function handles both encryption and decryption. - doAssert key.len == 16, "Key must be 16 bytes for AES-128" - doAssert iv.len == 16, "IV must be 16 bytes for AES-128" - doAssert startIndex mod 16 == 0, "Start index must be a multiple of 16" - - var advIV = @iv - - # Advance the counter to the start index - let blocksToAdvance = startIndex div 16 - advance_ctr(advIV, blocksToAdvance.uint64) - - return aes_ctr(key, advIV, data) - -proc sha256_hash*(data: openArray[byte]): array[32, byte] = - ## hashes 'data' using SHA-256. - return sha256.digest(data).data - -proc kdf*(key: openArray[byte]): seq[byte] = - ## Returns the hash of 'key' truncated to 16 bytes. - let hash = sha256_hash(key) - return hash[0 .. 15] - -proc hmac*(key, data: openArray[byte]): seq[byte] = - ## Computes a HMAC for 'data' using given 'key'. - let hmac = sha256.hmac(key, data).data - return hmac[0 .. 15] diff --git a/libp2p/protocols/mix/curve25519.nim b/libp2p/protocols/mix/curve25519.nim deleted file mode 100644 index 370f935c89..0000000000 --- a/libp2p/protocols/mix/curve25519.nim +++ /dev/null @@ -1,55 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import results -import bearssl/rand -import ../../crypto/curve25519 - -const FieldElementSize* = Curve25519KeySize - -type FieldElement* = Curve25519Key - -proc bytesToFieldElement*(bytes: openArray[byte]): Result[FieldElement, string] = - ## Convert bytes to FieldElement - if bytes.len != FieldElementSize: - return err("Field element size must be 32 bytes") - ok(intoCurve25519Key(bytes)) - -proc fieldElementToBytes*(fe: FieldElement): seq[byte] = - ## Convert FieldElement to bytes - fe.getBytes() - -# Generate a random FieldElement -proc generateRandomFieldElement*(): Result[FieldElement, string] = - let rng = HmacDrbgContext.new() - if rng.isNil: - return err("Failed to create HmacDrbgContext with system randomness") - ok(Curve25519Key.random(rng[])) - -# Generate a key pair (private key and public key are both FieldElements) -proc generateKeyPair*(): Result[tuple[privateKey, publicKey: FieldElement], string] = - let privateKey = generateRandomFieldElement().valueOr: - return err("Error in private key generation: " & error) - - let publicKey = public(privateKey) - ok((privateKey, publicKey)) - -proc multiplyPointWithScalars*( - point: FieldElement, scalars: openArray[FieldElement] -): FieldElement = - ## Multiply a given Curve25519 point with a set of scalars - var res = point - for scalar in scalars: - Curve25519.mul(res, scalar) - res - -proc multiplyBasePointWithScalars*( - scalars: openArray[FieldElement] -): Result[FieldElement, string] = - ## Multiply the Curve25519 base point with a set of scalars - if scalars.len <= 0: - return err("Atleast one scalar must be provided") - var res: FieldElement = public(scalars[0]) # Use the predefined base point - for i in 1 ..< scalars.len: - Curve25519.mul(res, scalars[i]) # Multiply with each scalar - ok(res) diff --git a/libp2p/protocols/mix/delay.nim b/libp2p/protocols/mix/delay.nim deleted file mode 100644 index fa64201fee..0000000000 --- a/libp2p/protocols/mix/delay.nim +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import stew/endians2, chronos - -type Delay* = uint16 - ## A mix-protocol forwarding delay in milliseconds, encoded as a big-endian - ## uint16. - -const NoDelay* = Delay(0) -const DelaySize* = 2 # size of Delay type in bytes - -proc toBytes*(d: Delay): seq[byte] {.inline.} = - let bytes = d.toBytesBE() - @[bytes[0], bytes[1]] - -proc fromBytes*(T: typedesc[Delay], bytes: openArray[byte]): Delay {.inline.} = - doAssert bytes.len == DelaySize, "Delay.fromBytes expects exactly DelaySize bytes" - uint16.fromBytesBE(bytes) - -proc toDuration*(d: Delay): Duration = - d.milliseconds diff --git a/libp2p/protocols/mix/delay_strategy.nim b/libp2p/protocols/mix/delay_strategy.nim deleted file mode 100644 index 2a87afc452..0000000000 --- a/libp2p/protocols/mix/delay_strategy.nim +++ /dev/null @@ -1,144 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -## Pluggable delay strategy interface for the Mix Protocol. - -import std/math -import bearssl/rand -import ./delay -export delay - -type DelayStrategy* = ref object of RootObj ## Abstract interface for delay strategies. - rng: ref HmacDrbgContext - -method generateForEntry*(self: DelayStrategy): Delay {.base, gcsafe, raises: [].} = - ## Generate delay value to encode in packet (called by sender/entry node). - ## implementation should return some default value in case of errors - raiseAssert "generateForEntry must be implemented by concrete delay strategy types" - -method generateForIntermediate*( - self: DelayStrategy, encodedDelay: Delay -): Delay {.base, gcsafe, raises: [].} = - ## Generate actual delay from encoded value (called by intermediate node). - ## implementation should return some default value in case of errors - raiseAssert "generateForIntermediate must be implemented by concrete delay strategy types" - -type NoSamplingDelayStrategy* = ref object of DelayStrategy - ## Default strategy: generates random delays [0-2]ms, uses them directly. - -proc new*(T: typedesc[NoSamplingDelayStrategy], rng: ref HmacDrbgContext): T = - doAssert(rng != nil, "random is not set") - T(rng: rng) - -method generateForEntry*(self: NoSamplingDelayStrategy): Delay {.gcsafe, raises: [].} = - self.rng[].generate(uint16) mod 3 - -method generateForIntermediate*( - self: NoSamplingDelayStrategy, encodedDelay: Delay -): Delay {.gcsafe, raises: [].} = - encodedDelay - -const DefaultMeanDelay*: Delay = 100 -const DefaultNegligibleProb* = 1e-6 - ## Probability below which the tail of the exponential distribution is - ## excluded from the practical sampling window. - ## Yields a maximum delay of mean * -ln(negligibleProb) ≈ mean * 13.8. -const DefaultMinimumDelay*: Delay = 0 - ## Optional lower bound for sampled delays. This is useful when auxiliary work - ## such as proof generation runs in parallel with the delay timer and would - ## otherwise collapse the lower tail into a predictable floor. -const DefaultSpamProtectionDelayFloor*: Delay = 100 - ## Recommended default lower bound when per-hop spam protection is enabled. - ## Use `SpamProtectionDelayStrategy` to apply this floor explicitly. - -type ExponentialDelayStrategy* = ref object of DelayStrategy - ## Recommended strategy: encodes mean delay, samples from exponential distribution. - ## Samples are drawn directly from the exponential distribution conditioned on - ## the configured [minimumDelay, practicalMaxDelay] window. This preserves - ## a smooth bounded distribution without fixed spikes at either bound. - meanDelay: Delay - negligibleProb: float64 - minimumDelay: Delay - -type SpamProtectionDelayStrategy* = ref object of ExponentialDelayStrategy - ## Recommended strategy when `MixProtocol` is configured with per-hop spam - ## protection. Applies a non-zero minimum delay floor by default so proof - ## generation time does not collapse short delays into a predictable spike. - -proc new*( - T: typedesc[ExponentialDelayStrategy], - meanDelay: Delay = DefaultMeanDelay, - rng: ref HmacDrbgContext, - negligibleProb: float64 = DefaultNegligibleProb, - minimumDelay: Delay = DefaultMinimumDelay, -): T {.raises: [].} = - doAssert(rng != nil, "random is not set") - doAssert( - negligibleProb > 0.0 and negligibleProb < 1.0, "negligibleProb must be in (0, 1)" - ) - T( - meanDelay: meanDelay, - rng: rng, - negligibleProb: negligibleProb, - minimumDelay: minimumDelay, - ) - -proc new*( - T: typedesc[SpamProtectionDelayStrategy], - meanDelay: Delay = DefaultMeanDelay, - rng: ref HmacDrbgContext, - negligibleProb: float64 = DefaultNegligibleProb, - minimumDelay: Delay = DefaultSpamProtectionDelayFloor, -): T {.raises: [].} = - doAssert(rng != nil, "random is not set") - doAssert( - negligibleProb > 0.0 and negligibleProb < 1.0, "negligibleProb must be in (0, 1)" - ) - T( - meanDelay: meanDelay, - rng: rng, - negligibleProb: negligibleProb, - minimumDelay: minimumDelay, - ) - -proc sampleOpenUnitInterval(self: DelayStrategy): float64 {.inline, raises: [].} = - const Float64MantissaBits = 53 - let rand53 = self.rng[].generate(uint64) shr (64 - Float64MantissaBits) - (float64(rand53) + 0.5) / float64(1'u64 shl Float64MantissaBits) - -proc practicalMaxDelay(meanDelay: Delay, negligibleProb: float64): float64 {.inline.} = - min(-float64(meanDelay) * ln(negligibleProb), float64(high(Delay))) - -proc sampleTruncatedExponential( - self: DelayStrategy, meanDelay: Delay, minDelay, maxDelay: float64 -): Delay {.inline, raises: [].} = - let - meanDelayF = float64(meanDelay) - minBound = exp(-minDelay / meanDelayF) - maxBound = exp(-maxDelay / meanDelayF) - sample = self.sampleOpenUnitInterval() - delayVal = -meanDelayF * ln(minBound - sample * (minBound - maxBound)) - boundedDelay = clamp(delayVal, minDelay, min(maxDelay, float64(high(Delay)))) - boundedDelay.uint16 - -method generateForEntry*(self: ExponentialDelayStrategy): Delay {.gcsafe, raises: [].} = - self.meanDelay - -method generateForIntermediate*( - self: ExponentialDelayStrategy, encodedDelay: Delay -): Delay {.gcsafe, raises: [].} = - ## Samples directly from the exponential distribution conditioned on the - ## configured practical window. If the configured minimum delay already - ## exceeds the practical maximum for the encoded mean, the configured minimum - ## is returned as a deterministic fallback. - if encodedDelay == NoDelay: - return NoDelay - - let - minDelay = float64(self.minimumDelay) - maxDelay = practicalMaxDelay(encodedDelay, self.negligibleProb) - - if minDelay >= maxDelay: - return self.minimumDelay - - self.sampleTruncatedExponential(encodedDelay, minDelay, maxDelay) diff --git a/libp2p/protocols/mix/entry_connection.nim b/libp2p/protocols/mix/entry_connection.nim deleted file mode 100644 index da484f8b22..0000000000 --- a/libp2p/protocols/mix/entry_connection.nim +++ /dev/null @@ -1,139 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import hashes, chronos, results, chronicles -import ../../stream/connection -import ../../varint -import ../../utils/sequninit -import ./mix_protocol -from fragmentation import DataSize - -const DefaultSurbs = uint8(4) - -type MixDialer* = proc( - msg: seq[byte], codec: string, destination: MixDestination -): Future[void] {.async: (raises: [CancelledError, LPStreamError]).} - -type MixParameters* = object - expectReply*: Opt[bool] - numSurbs*: Opt[uint8] - -type MixEntryConnection* = ref object of Connection - destination: MixDestination - codec: string - mixDialer: MixDialer - params: MixParameters - incoming: AsyncQueue[seq[byte]] - incomingFut: Future[void] - replyReceivedFut: Future[void] - cached: seq[byte] - -func shortLog*(conn: MixEntryConnection): string = - if conn == nil: - "MixEntryConnection(nil)" - else: - "MixEntryConnection(" & $conn.destination & ")" - -chronicles.formatIt(MixEntryConnection): - shortLog(it) - -method readOnce*( - s: MixEntryConnection, pbytes: pointer, nbytes: int -): Future[int] {.async: (raises: [CancelledError, LPStreamError]).} = - if s.isEof: - raise newLPStreamEOFError() - - # Only wait for reply if cache is empty - if s.cached.len == 0: - if s.replyReceivedFut.isNil: - raise newException(LPStreamError, "mix connection does not expect replies") - try: - await s.replyReceivedFut - if s.cached.len == 0: - # No data received - this is EOF - s.isEof = true - raise newLPStreamEOFError() - except CancelledError as exc: - raise exc - except LPStreamEOFError as exc: - raise exc - except CatchableError as exc: - raise (ref LPStreamError)(msg: "error in readOnce: " & exc.msg, parent: exc) - - # We have data in cache, return what we can - let toRead = min(nbytes, s.cached.len) - copyMem(pbytes, addr s.cached[0], toRead) - s.cached = s.cached[toRead ..^ 1] - - return toRead - -method write*( - self: MixEntryConnection, msg: seq[byte] -): Future[void] {.async: (raises: [CancelledError, LPStreamError]).} = - if msg.len() > DataSize: - raise newException(LPStreamError, "exceeds max msg size of " & $DataSize & " bytes") - await self.mixDialer(msg, self.codec, self.destination) - -proc shortLog*(self: MixEntryConnection): string {.raises: [].} = - "[MixEntryConnection] Destination: " & $self.destination - -method closeImpl*(self: MixEntryConnection): Future[void] {.async: (raises: []).} = - if not self.incomingFut.isNil: - self.incomingFut.cancelSoon() - -func hash*(self: MixEntryConnection): Hash = - hash($self.destination) - -proc new*( - T: typedesc[MixEntryConnection], - srcMix: MixProtocol, - destination: MixDestination, - codec: string, - params: MixParameters, -): T {.raises: [].} = - let expectReply = params.expectReply.get(false) - let numSurbs = - if expectReply: - params.numSurbs.get(DefaultSurbs) - else: - 0 - - var instance = T() - instance.destination = destination - instance.codec = codec - - if expectReply: - instance.incoming = newAsyncQueue[seq[byte]]() - instance.replyReceivedFut = newFuture[void]() - let checkForIncoming = proc(): Future[void] {.async: (raises: [CancelledError]).} = - instance.cached = await instance.incoming.get() - instance.replyReceivedFut.complete() - instance.incomingFut = checkForIncoming() - - instance.mixDialer = proc( - msg: seq[byte], codec: string, dest: MixDestination - ): Future[void] {.async: (raises: [CancelledError, LPStreamError]).} = - let sendRes = await srcMix.anonymizeLocalProtocolSend( - instance.incoming, msg, codec, dest, numSurbs - ) - if sendRes.isErr: - raise newException(LPStreamError, sendRes.error) - - instance - -proc toConnection*( - srcMix: MixProtocol, - destination: MixDestination, - codec: string, - params: MixParameters = MixParameters(), -): Result[Connection, string] {.gcsafe, raises: [].} = - ## Create a stream to send and optionally receive responses. - ## Under the hood it will wrap the message in a sphinx packet - ## and send it via a random mix path. - if not srcMix.hasDestReadBehavior(codec): - if params.expectReply.get(false): - return err("no destination read behavior for codec") - else: - warn "no destination read behavior for codec", codec - - ok(MixEntryConnection.new(srcMix, destination, codec, params)) diff --git a/libp2p/protocols/mix/exit_connection.nim b/libp2p/protocols/mix/exit_connection.nim deleted file mode 100644 index 2ca38838f5..0000000000 --- a/libp2p/protocols/mix/exit_connection.nim +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import hashes, chronos, chronicles -import ../../stream/connection -from fragmentation import DataSize - -type MixExitConnection* = ref object of Connection - message: seq[byte] - response: seq[byte] - -method readOnce*( - self: MixExitConnection, pbytes: pointer, nbytes: int -): Future[int] {.async: (raises: [CancelledError, LPStreamError]).} = - if self.message.len == 0: - return 0 # Nothing else to read. - if self.message.len < nbytes: - raise newException( - LPStreamError, "Not enough data in to read exactly " & $nbytes & " bytes." - ) - copyMem(pbytes, addr self.message[0], nbytes) - self.message = self.message[nbytes ..^ 1] - nbytes - -method write*( - self: MixExitConnection, msg: seq[byte] -): Future[void] {.async: (raises: [CancelledError, LPStreamError]).} = - if msg.len() > DataSize: - raise newException(LPStreamError, "exceeds max msg size of " & $DataSize & " bytes") - self.response.add(msg) - -func shortLog*(self: MixExitConnection): string {.raises: [].} = - "MixExitConnection" - -chronicles.formatIt(MixExitConnection): - shortLog(it) - -method initStream*(self: MixExitConnection) = - discard - -method closeImpl*(self: MixExitConnection): Future[void] {.async: (raises: []).} = - discard - -func hash*(self: MixExitConnection): Hash = - discard - -proc getResponse*(self: MixExitConnection): seq[byte] = - let r = self.response - self.response = @[] - return r - -proc new*(T: typedesc[MixExitConnection], message: seq[byte]): T = - T(message: message) diff --git a/libp2p/protocols/mix/exit_layer.nim b/libp2p/protocols/mix/exit_layer.nim deleted file mode 100644 index 4c7402d8b1..0000000000 --- a/libp2p/protocols/mix/exit_layer.nim +++ /dev/null @@ -1,171 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import chronicles, chronos, metrics, std/sequtils, std/tables -import ../../builders -import ../../stream/connection -import ../../varint -import ../../utils/sequninit -import ./[mix_metrics, reply_connection, serialization, multiaddr] - -when defined(libp2p_mix_experimental_exit_is_dest): - import std/enumerate - import ./exit_connection - -type OnReplyDialer* = - proc(surb: SURB, message: seq[byte]) {.async: (raises: [CancelledError]).} - -## Callback type for reading responses from a destination connection -type destReadBehaviorCb* = proc(conn: Connection): Future[seq[byte]] {. - async: (raises: [CancelledError, LPStreamError]) -.} - -## Wrapper for destination read behavior with metadata. -## When a callback uses length-prefixed reads (e.g., readLp), it strips the length prefix. -## Setting usesLengthPrefix=true tells the exit layer to restore the prefix in the reply, -## ensuring that application code can correctly call readLp() on the MixEntryConnection. -type DestReadBehavior* = object - callback*: destReadBehaviorCb - usesLengthPrefix*: bool - -type ExitLayer* = object - switch: Switch - onReplyDialer: OnReplyDialer - destReadBehavior: TableRef[string, DestReadBehavior] - -proc init*( - T: typedesc[ExitLayer], - switch: Switch, - onReplyDialer: OnReplyDialer, - destReadBehavior: TableRef[string, DestReadBehavior], -): T = - ExitLayer( - switch: switch, onReplyDialer: onReplyDialer, destReadBehavior: destReadBehavior - ) - -proc replyDialerCbFactory(self: ExitLayer): MixReplyDialer = - return proc( - surbs: seq[SURB], msg: seq[byte] - ): Future[void] {.async: (raises: [CancelledError, LPStreamError]).} = - let respFuts = surbs.mapIt(self.onReplyDialer(it, msg)) - await allFutures(respFuts) - -proc reply( - self: ExitLayer, surbs: seq[SURB], response: seq[byte] -) {.async: (raises: [CancelledError]).} = - if surbs.len == 0: - return - - let replyConn = MixReplyConnection.new(surbs, self.replyDialerCbFactory()) - defer: - await replyConn.close() - try: - await replyConn.write(response) - except LPStreamError as exc: - error "could not reply", description = exc.msg - mix_messages_error.inc(labelValues = ["ExitLayer", "REPLY_FAILED"]) - -when defined(libp2p_mix_experimental_exit_is_dest): - proc runHandler( - self: ExitLayer, codec: string, message: seq[byte], surbs: seq[SURB] - ) {.async: (raises: [CancelledError]).} = - let exitConn = MixExitConnection.new(message) - defer: - await exitConn.close() - - var hasHandler: bool = false - for index, handler in enumerate(self.switch.ms.handlers): - if codec in handler.protos: - try: - hasHandler = true - await handler.protocol.handler(exitConn, codec) - except CatchableError as e: - error "Error during execution of MixProtocol handler: ", err = e.msg - - if not hasHandler: - error "Handler doesn't exist", codec = codec - return - - if surbs.len != 0: - let response = exitConn.getResponse() - await self.reply(surbs, response) - -proc fwdRequest( - self: ExitLayer, - codec: string, - message: seq[byte], - destination: Hop, - surbs: seq[SURB], -) {.async: (raises: [CancelledError]).} = - # If dialing destination fails, no response is returned to - # the sender, so, flow can just end here. Only log errors - # for now - # https://github.com/vacp2p/mix/issues/86 - - if destination == Hop(): - error "no destination available" - mix_messages_error.inc(labelValues = ["Exit", "NO_DESTINATION"]) - return - - let (destPeerId, destAddr) = destination.get().bytesToMultiAddr().valueOr: - error "Failed to convert bytes to multiaddress", err = error - mix_messages_error.inc(labelValues = ["Exit", "INVALID_DEST"]) - return - - var response: seq[byte] - try: - let destConn = await self.switch.dial(destPeerId, @[destAddr], codec) - defer: - await destConn.close() - await destConn.write(message) - - if surbs.len != 0: - if not self.destReadBehavior.hasKey(codec): - error "No destReadBehavior for codec", codec - return - - var behavior: DestReadBehavior - try: - behavior = self.destReadBehavior[codec] - except KeyError: - raiseAssert "checked with HasKey" - - let rawResponse = await behavior.callback(destConn) - - # Add length prefix back only if the behavior callback uses length-prefixed reads - # (e.g., readLp). This ensures that when application code calls readLp() on the - # MixEntryConnection, it can correctly read the response. - if behavior.usesLengthPrefix: - let vbytes = PB.toBytes(rawResponse.len.uint64) - response = newSeqUninit[byte](rawResponse.len + vbytes.len) - response[0 ..< vbytes.len] = vbytes.toOpenArray() - response[vbytes.len ..< response.len] = rawResponse - else: - response = rawResponse - except LPStreamError as exc: - error "Stream error while writing to next hop: ", err = exc.msg - mix_messages_error.inc(labelValues = ["ExitLayer", "LPSTREAM_ERR"]) - except DialFailedError as exc: - error "Failed to dial next hop: ", err = exc.msg - mix_messages_error.inc(labelValues = ["ExitLayer", "DIAL_FAILED"]) - except CancelledError as exc: - raise exc - - await self.reply(surbs, response) - -proc onMessage*( - self: ExitLayer, - codec: string, - message: seq[byte], - destination: Hop, - surbs: seq[SURB], -) {.async: (raises: [CancelledError]).} = - when defined(libp2p_mix_experimental_exit_is_dest): - if destination == Hop(): - trace "onMessage - exit is destination", codec, message - await self.runHandler(codec, message, surbs) - else: - trace "onMessage - exist is not destination", codec, message - await self.fwdRequest(codec, message, destination, surbs) - else: - await self.fwdRequest(codec, message, destination, surbs) diff --git a/libp2p/protocols/mix/fragmentation.nim b/libp2p/protocols/mix/fragmentation.nim deleted file mode 100644 index 74676e91ed..0000000000 --- a/libp2p/protocols/mix/fragmentation.nim +++ /dev/null @@ -1,98 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import ./[serialization, seqno_generator] -import results, stew/endians2 -import ../../peerid - -const PaddingLengthSize* = 2 -const SeqNoSize* = 4 -const DataSize* = MessageSize - PaddingLengthSize - SeqNoSize - -# Unpadding and reassembling messages will be handled by the top-level applications. -# Although padding and splitting messages could also be managed at that level, we -# implement it here to clarify the sender's logic. -# This is crucial as the sender is responsible for wrapping messages in Sphinx packets. - -type MessageChunk* = object - paddingLength: uint16 - data: seq[byte] - seqNo: uint32 - -proc init*( - T: typedesc[MessageChunk], paddingLength: uint16, data: seq[byte], seqNo: uint32 -): T = - T(paddingLength: paddingLength, data: data, seqNo: seqNo) - -proc get*(msgChunk: MessageChunk): (uint16, seq[byte], uint32) = - (msgChunk.paddingLength, msgChunk.data, msgChunk.seqNo) - -proc serialize*(msgChunk: MessageChunk): seq[byte] = - let - paddingBytes = msgChunk.paddingLength.toBytesBE() - seqNoBytes = msgChunk.seqNo.toBytesBE() - - doAssert msgChunk.data.len == DataSize, - "Padded data must be exactly " & $DataSize & " bytes" - - return @paddingBytes & msgChunk.data & @seqNoBytes - -proc deserialize*(T: typedesc[MessageChunk], data: openArray[byte]): Result[T, string] = - if data.len != MessageSize: - return err("Data must be exactly " & $MessageSize & " bytes") - - let - paddingLength = uint16.fromBytesBE(data[0 .. PaddingLengthSize - 1]) - chunk = data[PaddingLengthSize .. (PaddingLengthSize + DataSize - 1)] - seqNo = uint32.fromBytesBE(data[PaddingLengthSize + DataSize ..^ 1]) - - ok(T(paddingLength: paddingLength, data: chunk, seqNo: seqNo)) - -proc ceilDiv*(a, b: int): int = - (a + b - 1) div b - -proc addPadding*(messageBytes: seq[byte], seqNo: SeqNo): MessageChunk = - ## Pads messages smaller than DataSize - let paddingLength = uint16(DataSize - messageBytes.len) - let paddedData = - if paddingLength > 0: - let paddingBytes = newSeq[byte](paddingLength) - paddingBytes & messageBytes - else: - messageBytes - MessageChunk(paddingLength: paddingLength, data: paddedData, seqNo: seqNo) - -proc addPadding*(messageBytes: seq[byte], peerId: PeerId): MessageChunk = - ## Pads messages smaller than DataSize - var seqNoGen = SeqNo.init(peerId) - seqNoGen.generate(messageBytes) - messageBytes.addPadding(seqNoGen) - -proc removePadding*(msgChunk: MessageChunk): Result[seq[byte], string] = - let msgLength = len(msgChunk.data) - int(msgChunk.paddingLength) - if msgLength < 0: - return err("Invalid padding length") - - ok(msgChunk.data[msgChunk.paddingLength ..^ 1]) - -proc padAndChunkMessage*(messageBytes: seq[byte], peerId: PeerId): seq[MessageChunk] = - var seqNoGen = SeqNo.init(peerId) - seqNoGen.generate(messageBytes) - - var chunks: seq[MessageChunk] = @[] - - # Split to chunks - let totalChunks = max(1, ceilDiv(messageBytes.len, DataSize)) - # Ensure at least one chunk is generated - for i in 0 ..< totalChunks: - let - startIdx = i * DataSize - endIdx = min(startIdx + DataSize, messageBytes.len) - chunkData = messageBytes[startIdx .. endIdx - 1] - msgChunk = chunkData.addPadding(seqNoGen) - - chunks.add(msgChunk) - - seqNoGen.inc() - - return chunks diff --git a/libp2p/protocols/mix/mix_message.nim b/libp2p/protocols/mix/mix_message.nim deleted file mode 100644 index 9be9a5e12b..0000000000 --- a/libp2p/protocols/mix/mix_message.nim +++ /dev/null @@ -1,50 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import chronicles, results -import stew/[byteutils, leb128] -import ../../protobuf/minprotobuf -import ../../utils/sequninit - -type MixMessage* = object - message*: seq[byte] - codec*: string - -proc init*(T: typedesc[MixMessage], message: openArray[byte], codec: string): T = - return T(message: @message, codec: codec) - -proc serialize*(mixMsg: MixMessage): seq[byte] = - let vbytes = toBytes(mixMsg.codec.len.uint64, Leb128) - doAssert vbytes.len <= 2, "serialization failed: codec length exceeds 2 bytes" - - var buf = newSeqUninit[byte](vbytes.len + mixMsg.codec.len + mixMsg.message.len) - buf[0 ..< vbytes.len] = vbytes.toOpenArray() - buf[vbytes.len ..< mixMsg.codec.len] = mixMsg.codec.toBytes() - buf[vbytes.len + mixMsg.codec.len ..< buf.len] = mixMsg.message - buf - -proc deserialize*( - T: typedesc[MixMessage], data: openArray[byte] -): Result[MixMessage, string] = - if data.len == 0: - return err("deserialization failed: data is empty") - - var codecLen: int - var varintLen: int - for i in 0 ..< min(data.len, 2): - let parsed = uint16.fromBytes(data[0 ..< i], Leb128) - if parsed.len < 0 or (i == 1 and parsed.len == 0): - return err("deserialization failed: invalid codec length") - - varintLen = parsed.len - codecLen = parsed.val.int - - if data.len < varintLen + codecLen: - return err("deserialization failed: not enough data") - - ok( - T( - codec: string.fromBytes(data[varintLen ..< varintLen + codecLen]), - message: data[varintLen + codecLen ..< data.len], - ) - ) diff --git a/libp2p/protocols/mix/mix_metrics.nim b/libp2p/protocols/mix/mix_metrics.nim deleted file mode 100644 index 886645665b..0000000000 --- a/libp2p/protocols/mix/mix_metrics.nim +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.push raises: [].} - -import metrics - -declarePublicCounter mix_messages_recvd, "number of mix messages received", ["type"] - -declarePublicCounter mix_messages_forwarded, - "number of mix messages forwarded", ["type"] - -declarePublicCounter mix_messages_error, - "number of mix messages failed processing", ["type", "error"] - -declarePublicGauge mix_pool_size, "number of nodes in the pool" - -declarePublicCounter mix_cover_emitted, "number of cover packets emitted", ["type"] - -declarePublicCounter mix_cover_received, - "number of cover packets received at exit (loop return)" - -declarePublicCounter mix_slot_claim_rejected, - "number of slot claim rejections", ["type"] - -declarePublicCounter mix_cover_error, "number of cover traffic errors", ["error"] - -declarePublicCounter mix_cover_precomputed, - "number of cover packets pre-computed per epoch" diff --git a/libp2p/protocols/mix/mix_node.nim b/libp2p/protocols/mix/mix_node.nim deleted file mode 100644 index fc250e31c7..0000000000 --- a/libp2p/protocols/mix/mix_node.nim +++ /dev/null @@ -1,85 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import strformat, sequtils -import ../../crypto/[secp, crypto] -import ../../[multiaddress, peerid] -import ./curve25519 - -type MixNodeInfo* = object - peerId*: PeerId - multiAddr*: MultiAddress - mixPubKey*: FieldElement - mixPrivKey*: FieldElement - libp2pPubKey*: SkPublicKey - libp2pPrivKey*: SkPrivateKey - -proc initMixNodeInfo*( - peerId: PeerId, - multiAddr: MultiAddress, - mixPubKey, mixPrivKey: FieldElement, - libp2pPubKey: SkPublicKey, - libp2pPrivKey: SkPrivateKey, -): MixNodeInfo = - MixNodeInfo( - peerId: peerId, - multiAddr: multiAddr, - mixPubKey: mixPubKey, - mixPrivKey: mixPrivKey, - libp2pPubKey: libp2pPubKey, - libp2pPrivKey: libp2pPrivKey, - ) - -proc generateRandom*(T: typedesc[MixNodeInfo], port: int): MixNodeInfo = - let - (mixPrivKey, mixPubKey) = generateKeyPair().expect("Generate key pair error") - keyPair = SkKeyPair.random(newRng()[]) - pubKeyProto = PublicKey(scheme: Secp256k1, skkey: keyPair.pubkey) - - MixNodeInfo( - peerId: PeerId.init(pubKeyProto).expect("PeerId init error"), - multiAddr: MultiAddress.init(fmt"/ip4/0.0.0.0/tcp/{port}").tryGet(), - mixPubKey: mixPubKey, - mixPrivKey: mixPrivKey, - libp2pPubKey: keyPair.pubkey, - libp2pPrivKey: keyPair.seckey, - ) - -proc generateRandomMany*( - T: typedesc[MixNodeInfo], count: int, basePort: int = 4242 -): seq[MixNodeInfo] = - var nodeInfos = newSeq[MixNodeInfo](count) - for i in 0 ..< count: - nodeInfos[i] = MixNodeInfo.generateRandom(basePort + i) - nodeInfos - -type MixPubInfo* = object - peerId*: PeerId - multiAddr*: MultiAddress - mixPubKey*: FieldElement - libp2pPubKey*: SkPublicKey - -proc init*( - T: typedesc[MixPubInfo], - peerId: PeerId, - multiAddr: MultiAddress, - mixPubKey: FieldElement, - libp2pPubKey: SkPublicKey, -): T = - T( - peerId: peerId, - multiAddr: multiAddr, - mixPubKey: mixPubKey, - libp2pPubKey: libp2pPubKey, - ) - -proc get*(info: MixPubInfo): (PeerId, MultiAddress, FieldElement, SkPublicKey) = - (info.peerId, info.multiAddr, info.mixPubKey, info.libp2pPubKey) - -proc toMixPubInfo*(info: MixNodeInfo): MixPubInfo = - MixPubInfo.init(info.peerId, info.multiAddr, info.mixPubKey, info.libp2pPubKey) - -proc includeAllExcept*( - allNodes: seq[MixNodeInfo], exceptNode: MixNodeInfo -): seq[MixPubInfo] {.inline.} = - allNodes.mapIt(it.toMixPubInfo()).filterIt(it.peerId != exceptNode.peerId) diff --git a/libp2p/protocols/mix/mix_protocol.nim b/libp2p/protocols/mix/mix_protocol.nim deleted file mode 100644 index 010c76a5b4..0000000000 --- a/libp2p/protocols/mix/mix_protocol.nim +++ /dev/null @@ -1,1108 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import chronicles, chronos, sequtils, results, sets -import std/[strformat, tables], metrics -import - ./[ - curve25519, delay, fragmentation, mix_message, mix_node, sphinx, serialization, - tag_manager, mix_metrics, exit_layer, multiaddr, exit_connection, spam_protection, - delay_strategy, pool, cover_traffic, - ] -import ../protocol -import ../../utils/[sequninit] -import ../../stream/[connection, lpstream] -import ../../[switch, multicodec, peerinfo, varint] -import ../../crypto/crypto - -export pool - -when defined(enable_mix_benchmarks): - import ./benchmark - from times import getTime, toUnixFloat, `-`, initTime, `$`, inMilliseconds, Time - -when defined(libp2p_mix_experimental_exit_is_dest): - {.warning: "experimental support for mix exit == destination is enabled!".} - -const MixProtocolID* = "/mix/1.0.0" - -const CoverTrafficCodec* = "/mix/cover/1.0.0" - ## Reserved codec for cover traffic packets. Cover packets use this codec - ## so the exit node can identify and silently discard returning cover traffic. - -func isCoverTraffic*(msg: MixMessage): bool = - ## Returns true if the message is a cover traffic packet. - msg.codec == CoverTrafficCodec - -type - SURBIdentifierGroup = ref object - members: HashSet[SURBIdentifier] - - ConnCreds = object - igroup: SURBIdentifierGroup - incoming: AsyncQueue[seq[byte]] - surbSecret: serialization.Secret - surbKey: serialization.Key - -## Mix Protocol defines a decentralized anonymous message routing layer for libp2p networks. -## It enables sender anonymity by routing each message through a decentralized mix overlay -## network composed of participating libp2p nodes, known as mix nodes. Each message is -## routed independently in a stateless manner, allowing other libp2p protocols to selectively -## anonymize messages without modifying their core protocol behavior. -type MixProtocol* = ref object of LPProtocol - mixNodeInfo: MixNodeInfo - switch*: Switch - nodePool*: MixNodePool - tagManager: TagManager - exitLayer: ExitLayer - rng: ref HmacDrbgContext - # TODO: verify if this requires cleanup for cases in which response never arrives (and connection is closed) - connCreds: Table[SURBIdentifier, ConnCreds] - destReadBehavior: TableRef[string, DestReadBehavior] - connPool: Table[PeerId, Connection] - spamProtection: Opt[SpamProtection] - delayStrategy: DelayStrategy - coverTraffic*: Opt[CoverTraffic] - ongoingMixMessages: seq[Future[void]] - ## Tracks all in-flight handleMixMessages futures so they can be - ## cancelled on stop and waited for during teardown. - -proc hasDestReadBehavior*(mixProto: MixProtocol, codec: string): bool = - return mixProto.destReadBehavior.hasKey(codec) - -proc registerDestReadBehavior*( - mixProto: MixProtocol, codec: string, behavior: DestReadBehavior -) = - mixProto.destReadBehavior[codec] = behavior - -proc cryptoRandomInt(rng: ref HmacDrbgContext, max: int): Result[int, string] = - if max == 0: - return err("Max cannot be zero.") - let res = rng[].generate(uint64) mod uint64(max) - ok(res.int) - -proc removeClosedConnections( - mixProto: MixProtocol, pid: PeerId -) {.async: (raises: []).} = - var peersToGC = mixProto.connPool - .keys() - .toSeq() - .filterIt(not mixProto.switch.isConnected(it)) - .toHashSet() - peersToGC.incl(pid) - - for p in peersToGC: - var conn: Connection - if mixProto.connPool.pop(p, conn): - await conn.close() - -proc getConn( - mixProto: MixProtocol, - pid: PeerId, - addrs: seq[MultiAddress], - codecs: seq[string], - forceNewStream: bool = false, -): Future[Connection] {.async: (raises: [DialFailedError, CancelledError]).} = - if forceNewStream: - # GC all expired connections including the one used for `pid` - await mixProto.removeClosedConnections(pid) - try: - return mixProto.connPool[pid] - except KeyError: - let c = await mixProto.switch.dial(pid, addrs, codecs) - mixProto.connPool[pid] = c - return c - -proc writeLp( - mixProto: MixProtocol, - pid: PeerId, - addrs: seq[MultiAddress], - codecs: seq[string], - payload: seq[byte], -) {.async: (raises: [DialFailedError, LPStreamError, CancelledError]).} = - let c = await mixProto.getConn(pid, addrs, codecs) - try: - await c.writeLp(payload) - except LPStreamClosedError, LPStreamResetError, LPStreamRemoteClosedError, - LPStreamConnDownError: - let c = await mixProto.getConn(pid, addrs, codecs, forceNewStream = true) - await c.writeLp(payload) - -proc generateAndAppendProof( - mixProto: MixProtocol, packet: seq[byte], label: string -): Result[tuple[packet: seq[byte], proofToken: seq[byte]], string] = - ## Generate spam protection proof and append it to the packet. - ## Returns the packet with proof appended and an opaque proof token - ## for proof slot tracking. - let spamProtection = mixProto.spamProtection.valueOr: - return ok((packet, newSeq[byte]())) - - let bindingData = packet - let proofResult = spamProtection - .generateProof(bindingData) - .mapErr( - proc(e: string): string = - mix_messages_error.inc(labelValues = [label, "SPAM_PROOF_GEN_FAILED"]) - fmt"Failed to generate spam protection proof: {e}" - ).valueOr: - return err(error) - - let packetWithProof = appendProofToPacket(packet, proofResult.proof) - .mapErr( - proc(e: string): string = - mix_messages_error.inc(labelValues = [label, "SPAM_PROOF_EMBED_FAILED"]) - fmt"Failed to append spam protection proof: {e}" - ).valueOr: - return err(error) - - ok((packetWithProof, proofResult.token)) - -proc extractProof( - mixProto: MixProtocol, packetWithProof: var seq[byte], label: string -): Result[tuple[sphinxPacket: seq[byte], proof: seq[byte]], string] = - ## Extract spam protection proof from the packet without verifying. - ## Returns the Sphinx packet and the extracted proof. - - let spamProtection = mixProto.spamProtection.valueOr: - return ok((sphinxPacket: packetWithProof, proof: newSeq[byte](0))) - - let (packet, proofData) = extractProofFromPacket(packetWithProof, spamProtection) - .mapErr( - proc(e: string): string = - mix_messages_error.inc(labelValues = [label, "SPAM_PROOF_EXTRACTION_FAILED"]) - fmt"Failed to extract spam protection proof: {e}" - ).valueOr: - return err(error) - - ok((sphinxPacket: packet, proof: proofData)) - -proc verifyProof( - mixProto: MixProtocol, sphinxPacket: seq[byte], proof: seq[byte], label: string -): Result[void, string] = - ## Verify a previously extracted spam protection proof. - let spamProtection = mixProto.spamProtection.valueOr: - return ok() - - let bindingData = sphinxPacket - - let verifyResult = spamProtection.verifyProof(proof, bindingData).valueOr: - mix_messages_error.inc(labelValues = [label, "SPAM_PROOF_VERIFY_ERROR"]) - return err(fmt"Spam protection proof verification error: {error}") - - if not verifyResult: - mix_messages_error.inc(labelValues = [label, "SPAM_PROOF_INVALID"]) - return err("Spam protection proof verification failed") - - trace "Spam protection proof verified successfully" - ok() - -method handleMixMessages*( - mixProto: MixProtocol, - fromPeerId: PeerId, - receivedBytes: sink seq[byte], - metadataBytes: sink seq[byte], -) {.base, async: (raises: [LPStreamError, CancelledError]).} = - when defined(enable_mix_benchmarks): - let startTime = getTime() - - if metadataBytes.len == 0: - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "NO_DATA"]) - return # No data, end of stream - - if receivedBytes.len == 0: - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "NO_DATA"]) - return # No data, end of stream - - # Step 1: Extract spam proof - # Note: extractProof takes var to enable zero-copy truncation - let (sphinxBytes, spamProof) = mixProto.extractProof( - receivedBytes, "Intermediate/Exit" - ).valueOr: - error "Spam proof extraction failed", err = error - return - - # Step 2: Deserialize and check replay - let sphinxPacket = SphinxPacket.deserialize(sphinxBytes).valueOr: - error "Sphinx deserialization failed", err = error - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "INVALID_SPHINX"]) - return - - let (isReplay, sharedSecret) = checkReplay( - sphinxPacket, mixProto.mixNodeInfo.mixPrivKey, mixProto.tagManager - ).valueOr: - error "Replay check failed", err = error - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "INVALID_SPHINX"]) - return - - if isReplay: - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "DUPLICATE"]) - return - - # Step 3: Verify spam proof - # Only done after replay check passes to avoid wasting cycles on duplicates - mixProto.verifyProof(sphinxBytes, spamProof, "Intermediate/Exit").isOkOr: - error "Spam protection verification failed", err = error - return - - # Step 4: Full Sphinx processing - # Reuse the shared secret computed in step 2 to avoid duplicate EC multiplication - let processedSP = processSphinxPacket( - sphinxPacket, - mixProto.mixNodeInfo.mixPrivKey, - mixProto.tagManager, - Opt.some(sharedSecret), - ).valueOr: - error "Failed to process Sphinx packet", err = error - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "INVALID_SPHINX"]) - return - - when defined(enable_mix_benchmarks): - let metadata = Metadata.deserialize(metadataBytes) - - case processedSP.status - of Exit: - mix_messages_recvd.inc(labelValues = ["Exit"]) - - # This is the exit node, forward to destination - let msgChunk = MessageChunk.deserialize(processedSP.messageChunk).valueOr: - error "Deserialization failed", err = error - mix_messages_error.inc(labelValues = ["Exit", "INVALID_SPHINX"]) - return - - let unpaddedMsg = msgChunk.removePadding().valueOr: - error "Unpadding message failed", err = error - mix_messages_error.inc(labelValues = ["Exit", "INVALID_SPHINX"]) - return - - let deserialized = MixMessage.deserialize(unpaddedMsg).valueOr: - error "Deserialization failed", err = error - mix_messages_error.inc(labelValues = ["Exit", "INVALID_SPHINX"]) - return - - if isCoverTraffic(deserialized): - trace "Cover packet received (loop), discarding", - peerId = mixProto.mixNodeInfo.peerId - mix_cover_received.inc() - mixProto.coverTraffic.withValue(ct): - ct.onCoverReceived() - return - - let (surbs, message) = extractSURBs(deserialized.message).valueOr: - error "Extracting surbs from payload failed", err = error - mix_messages_error.inc(labelValues = ["Exit", "INVALID_MSG_SURBS"]) - return - - trace "Exit node - Received mix message", - peerId = mixProto.mixNodeInfo.peerId, - message = deserialized.message, - codec = deserialized.codec - - when defined(enable_mix_benchmarks): - benchmarkLog "Exit", - mixProto.switch.peerInfo.peerId, - startTime, - metadata, - Opt.some(fromPeerId), - Opt.none(PeerId) - - await mixProto.exitLayer.onMessage( - deserialized.codec, message, processedSP.destination, surbs - ) - - mix_messages_forwarded.inc(labelValues = ["Exit"]) - of Reply: - trace "# Reply", id = processedSP.id - - if not mixProto.connCreds.hasKey(processedSP.id): - mix_messages_error.inc(labelValues = ["Sender/Reply", "NO_CONN_FOUND"]) - return - - var connCred: ConnCreds - try: - connCred = mixProto.connCreds[processedSP.id] - except KeyError: - raiseAssert "checked with hasKey" - - let reply = processReply( - connCred.surbKey, connCred.surbSecret, processedSP.delta_prime - ).valueOr: - error "could not process reply", id = processedSP.id - mix_messages_error.inc(labelValues = ["Reply", "INVALID_CREDS"]) - return - - # Deleting all other SURBs associated to this - for id in connCred.igroup.members: - mixProto.connCreds.del(id) - - let msgChunk = MessageChunk.deserialize(reply).valueOr: - error "Deserialization failed", err = error - mix_messages_error.inc(labelValues = ["Reply", "INVALID_SPHINX"]) - return - - let unpaddedMsg = msgChunk.removePadding().valueOr: - error "Unpadding message failed", err = error - mix_messages_error.inc(labelValues = ["Reply", "INVALID_SPHINX"]) - return - - let deserialized = MixMessage.deserialize(unpaddedMsg).valueOr: - error "Deserialization failed", err = error - mix_messages_error.inc(labelValues = ["Reply", "INVALID_SPHINX"]) - return - - when defined(enable_mix_benchmarks): - benchmarkLog "Reply", - mixProto.switch.peerInfo.peerId, - startTime, - metadata, - Opt.some(fromPeerId), - Opt.none(PeerId) - - await connCred.incoming.put(deserialized.message) - of Intermediate: - trace "Intermediate node processing", - peerId = mixProto.mixNodeInfo.peerId, multiAddr = mixProto.mixNodeInfo.multiAddr - mix_messages_recvd.inc(labelValues = ["Intermediate"]) - - # Claim a slot for forwarding (Mix Cover Traffic spec §6.4) - mixProto.coverTraffic.withValue(ct): - let claim = ct.slotPool.claimSlot() - if not claim.success: - warn "Slot exhaustion, dropping forwarded packet" - mix_messages_error.inc(labelValues = ["Intermediate", "SLOT_EXHAUSTED"]) - mix_slot_claim_rejected.inc(labelValues = ["forward"]) - return - # Reclaim proof token from discarded cover packet for messageId reuse - if claim.reclaimedToken.len > 0: - mixProto.spamProtection.withValue(sp): - sp.reclaimProofToken(claim.reclaimedToken) - - let actualDelay = mixProto.delayStrategy.generateForIntermediate(processedSP.delay) - trace "Computed delay", encodedDelay = processedSP.delay, actualDelay - - # Forward to next hop - let nextHopBytes = processedSP.nextHop.get() - - let (nextPeerId, nextAddr) = bytesToMultiAddr(nextHopBytes).valueOr: - trace "Failed to convert bytes to multiaddress", err = error - mix_messages_error.inc(labelValues = ["Intermediate", "INVALID_DEST"]) - return - - when defined(enable_mix_benchmarks): - benchmarkLog "Intermediate", - mixProto.switch.peerInfo.peerId, - startTime, - metadata, - Opt.some(fromPeerId), - Opt.some(nodeInfo.peerId) - - # Per-hop spam protection: generate the fresh proof while the packet is - # being held. When using SpamProtectionDelayStrategy (or similar) with - # exponential delays, a lower sampling floor can be applied so this overlap - # does not collapse short samples into a fixed processing-time spike. - let proofGenStartTime = Moment.now() - let delayFut = sleepAsync(actualDelay.toDuration) - - let proofGenFut = ( - proc(): Future[Result[tuple[packet: seq[byte], proofToken: seq[byte]], string]] {. - async - .} = - return mixProto.generateAndAppendProof( - processedSP.serializedSphinxPacket, "Intermediate" - ) - )() - - await allFutures(proofGenFut, delayFut) - - mixProto.spamProtection.withValue(sp): - let proofGenTimeMs = (Moment.now() - proofGenStartTime).milliseconds - if proofGenTimeMs > actualDelay.int64: - warn "Proof generation time exceeds sampled delay", - proofGenTimeMs, - sampledDelay = actualDelay, - hint = "Increase the minimum delay floor or reduce proof generation time" - - let (outgoingPacket, _) = proofGenFut.value().valueOr: - error "Failed to generate spam protection proof for next hop", err = error - return - - try: - when defined(enable_mix_benchmarks): - await mixProto.writeLp(nextPeerId, @[nextAddr], @[MixProtocolID], metadataBytes) - await mixProto.writeLp(nextPeerId, @[nextAddr], @[MixProtocolID], outgoingPacket) - mix_messages_forwarded.inc(labelValues = ["Intermediate"]) - except CancelledError as exc: - raise exc - except DialFailedError as exc: - error "Failed to dial next hop: ", err = exc.msg - mix_messages_error.inc(labelValues = ["Intermediate", "DIAL_FAILED"]) - except LPStreamError as exc: - error "Failed to write to next hop: ", err = exc.msg - mix_messages_error.inc(labelValues = ["Intermediate", "DIAL_FAILED"]) - of Duplicate: - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "DUPLICATE"]) - of InvalidMAC: - mix_messages_error.inc(labelValues = ["Intermediate/Exit", "INVALID_MAC"]) - -proc proofSize(sp: Opt[SpamProtection]): int = - ## Helper to get proof size from optional spam protection. - ## Returns 0 if spam protection is None. - if sp.isNone: - return 0 - return sp.get().proofSize - -proc runMixMessage( - mixProto: MixProtocol, - fromPeerId: PeerId, - receivedBytes: sink seq[byte], - metadataBytes: sink seq[byte], -) {.async: (raises: []).} = - try: - await mixProto.handleMixMessages(fromPeerId, receivedBytes, metadataBytes) - except CancelledError: - trace "Handling mix message cancelled", fromPeerId - except LPStreamError as e: - error "Error handling mix message", fromPeerId, err = e.msg - -proc spawnMixMessage( - mixProto: MixProtocol, - fromPeerId: PeerId, - receivedBytes: sink seq[byte], - metadataBytes: sink seq[byte], -) = - ## Spawns a handleMixMessages task, tracks its future in `ongoingMixMessages`, - ## and removes it from the list when it finishes. - if not mixProto.started: - return - let fut = runMixMessage(mixProto, fromPeerId, receivedBytes, metadataBytes) - mixProto.ongoingMixMessages.add(fut) - fut.addCallback( - proc(_: pointer) {.gcsafe, raises: [].} = - mixProto.ongoingMixMessages.keepItIf(not it.finished) - ) - -proc handleMixNodeConnection( - mixProto: MixProtocol, conn: Connection -) {.async: (raises: [LPStreamError, CancelledError]).} = - defer: - await conn.close() - - while not conn.atEof: - var metadataBytes = newSeqUninit[byte](0) - when defined(enable_mix_benchmarks): - metadataBytes = newSeqUninit[byte](MetadataSize) - await conn.readExactly(addr metadataBytes[0], MetadataSize) - - # Calculate maximum wire packet size including spam protection proof - let maxWireSize = PacketSize + mixProto.spamProtection.proofSize() - let receivedBytes = await conn.readLp(maxWireSize) - mixProto.spawnMixMessage(conn.peerId, receivedBytes, metadataBytes) - -proc getMaxMessageSizeForCodec*( - codec: string, numberOfSurbs: uint8 = 0 -): Result[int, string] = - ## Computes the maximum payload size (in bytes) available for a message when encoded - ## with the given `codec`, optionally including space for the chosen number of surbs. - ## Returns an error if the codec + surb overhead exceeds the data capacity. - let serializedMsg = MixMessage.init(@[], codec).serialize() - let totalLen = serializedMsg.len + SurbLenSize + (int(numberOfSurbs) * SurbSize) - if totalLen > DataSize: - return err("cannot encode messages for this codec") - return ok(DataSize - totalLen) - -method buildSurb*( - mixProto: MixProtocol, id: SURBIdentifier, destPeerId: PeerId, exitPeerId: PeerId -): Result[SURB, string] {.base, gcsafe, raises: [].} = - var - publicKeys: seq[FieldElement] = @[] - hops: seq[Hop] = @[] - delays: seq[Delay] = @[] - - if mixProto.nodePool.len < PathLength: - return err("No. of public mix nodes less than path length") - - # Remove exit and dest node from nodes to consider for surbs - var poolPeerIds = - mixProto.nodePool.peerIds().filterIt(it != exitPeerId and it != destPeerId) - var availableIndices = toSeq(0 ..< poolPeerIds.len) - - # Select L mix nodes at random - for i in 0 ..< PathLength: - let (peerId, multiAddr, mixPubKey, hopDelay) = - if i < PathLength - 1: - let randomIndexPosition = cryptoRandomInt(mixProto.rng, availableIndices.len).valueOr: - return err("failed to generate random num: " & error) - let selectedIndex = availableIndices[randomIndexPosition] - let randPeerId = poolPeerIds[selectedIndex] - availableIndices.del(randomIndexPosition) - debug "Selected mix node for surbs: ", indexInPath = i, peerId = randPeerId - let mixPubInfo = mixProto.nodePool.get(randPeerId).valueOr: - return err("could not get mix pub info for peer: " & $randPeerId) - ( - mixPubInfo.peerId, - mixPubInfo.multiAddr, - mixPubInfo.mixPubKey, - mixProto.delayStrategy.generateForEntry(), - ) - else: - ( - mixProto.mixNodeInfo.peerId, mixProto.mixNodeInfo.multiAddr, - mixProto.mixNodeInfo.mixPubKey, NoDelay, - ) # No delay for last hop - - publicKeys.add(mixPubKey) - - let multiAddrBytes = multiAddrToBytes(peerId, multiAddr).valueOr: - mix_messages_error.inc(labelValues = ["Entry/SURB", "INVALID_MIX_INFO"]) - return err("failed to convert multiaddress to bytes: " & error) - - hops.add(Hop.init(multiAddrBytes)) - - delays.add(hopDelay) - - return createSURB(publicKeys, delays, hops, id, mixProto.rng) - -proc buildSurbs( - mixProto: MixProtocol, - incoming: AsyncQueue[seq[byte]], - numSurbs: uint8, - destPeerId: PeerId, - exitPeerId: PeerId, -): Result[seq[SURB], string] = - var response: seq[SURB] - var igroup = SURBIdentifierGroup(members: initHashSet[SURBIdentifier]()) - - for _ in 0.uint8 ..< numSurbs: - var id: SURBIdentifier - hmacDrbgGenerate(mixProto.rng[], id) - let surb = ?mixProto.buildSurb(id, destPeerId, exitPeerId) - igroup.members.incl(id) - mixProto.connCreds[id] = ConnCreds( - igroup: igroup, - surbSecret: surb.secret.get(), - surbKey: surb.key, - incoming: incoming, - ) - response.add(surb) - - return ok(response) - -proc prepareMsgWithSurbs( - mixProto: MixProtocol, - incoming: AsyncQueue[seq[byte]], - msg: seq[byte], - numSurbs: uint8 = 0, - destPeerId: PeerId, - exitPeerId: PeerId, -): Result[seq[byte], string] = - let surbs = mixProto.buildSurbs(incoming, numSurbs, destPeerId, exitPeerId).valueOr: - return err(error) - serializeMessageWithSURBs(msg, surbs) - -type SendPacketLogType* = enum - Entry - Reply - -type SendPacketLogConfig = object - logType: SendPacketLogType - when defined(enable_mix_benchmarks): - startTime: Time - metadata: Metadata - -proc sendPacket( - mixProto: MixProtocol, - peerId: PeerId, - multiAddress: MultiAddress, - sphinxPacket: SphinxPacket, - logConfig: SendPacketLogConfig, -): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - ## Send the wrapped message to the first mix node in the selected path - - let label = $logConfig.logType - - # Per-hop spam protection: Generate initial proof and append to packet - let (packetToSend, _) = mixProto.generateAndAppendProof( - sphinxPacket.serialize(), label - ).valueOr: - return err(error) - - when defined(enable_mix_benchmarks): - if logConfig.logType == Entry: - benchmarkLog "Sender", - mixProto.switch.peerInfo.peerId, - logConfig.startTime, - logConfig.metadata, - Opt.none(PeerId), - Opt.some(peerId) - - try: - when defined(enable_mix_benchmarks): - await mixProto.writeLp( - peerId, @[multiAddress], @[MixProtocolID], logConfig.metadata.serialize() - ) - await mixProto.writeLp(peerId, @[multiAddress], @[MixProtocolID], packetToSend) - except DialFailedError as exc: - mix_messages_error.inc(labelValues = [label, "SEND_FAILED"]) - return err(fmt"Failed to dial to next hop ({peerId}, {multiAddress}): {exc.msg}") - except LPStreamError as exc: - mix_messages_error.inc(labelValues = [label, "SEND_FAILED"]) - return err(fmt"Failed to write to next hop ({peerId}, {multiAddress}): {exc.msg}") - except CancelledError as exc: - raise exc - - mix_messages_forwarded.inc(labelValues = [label]) - return ok() - -proc buildMessage( - msg: seq[byte], codec: string, peerId: PeerId -): Result[Message, (string, string)] = - let - mixMsg = MixMessage.init(msg, codec) - serialized = mixMsg.serialize() - - if serialized.len > DataSize: - return err(("message size exceeds maximum payload size", "INVALID_SIZE")) - - let - paddedMsg = addPadding(serialized, peerId) - serializedMsgChunk = paddedMsg.serialize() - - ok(serializedMsgChunk) - -type DestinationType* = enum - ForwardAddr - MixNode - -## Represents the final target of a mixnet message. -## contains the peer id and multiaddress of the destination node -## if the exit != destination -type MixDestination* = object - peerId: PeerId - case kind: DestinationType - of ForwardAddr: - address: MultiAddress - else: - discard - -proc `$`*(d: MixDestination): string = - case d.kind - of ForwardAddr: - return "MixDestination[ForwardAddr](" & $d.address & "/p2p/" & $d.peerId & ")" - of MixNode: - return "MixDestination[MixNode](" & $d.peerId & ")" - -when defined(libp2p_mix_experimental_exit_is_dest): - proc exitNode*(T: typedesc[MixDestination], p: PeerId): T = - T(kind: DestinationType.MixNode, peerId: p) - -proc forwardToAddr*(T: typedesc[MixDestination], p: PeerId, address: MultiAddress): T = - T(kind: DestinationType.ForwardAddr, peerId: p, address: address) - -proc init*(T: typedesc[MixDestination], p: PeerId, address: MultiAddress): T = - MixDestination.forwardToAddr(p, address) - -proc anonymizeLocalProtocolSend*( - mixProto: MixProtocol, - incoming: AsyncQueue[seq[byte]], - msg: seq[byte], - codec: string, - destination: MixDestination, - numSurbs: uint8, -): Future[Result[void, string]] {.async: (raises: [CancelledError, LPStreamError]).} = - when not defined(libp2p_mix_experimental_exit_is_dest): - doAssert destination.kind == ForwardAddr, "Only exit != destination is allowed" - - mix_messages_recvd.inc(labelValues = ["Entry"]) - - # Claim a slot for local origination (Mix Cover Traffic spec §6.3) - mixProto.coverTraffic.withValue(ct): - let claim = ct.slotPool.claimSlot() - if not claim.success: - mix_slot_claim_rejected.inc(labelValues = ["send"]) - return err("No slots available in current epoch") - # Reclaim proof token from discarded cover packet for messageId reuse - if claim.reclaimedToken.len > 0: - mixProto.spamProtection.withValue(sp): - sp.reclaimProofToken(claim.reclaimedToken) - - var logConfig = SendPacketLogConfig(logType: Entry) - when defined(enable_mix_benchmarks): - # Assumes a fixed message layout whose first 16 bytes are the time at - # origin and msgId - logConfig.startTime = getTime() - logConfig.metadata = Metadata.deserialize(msg) - - var - publicKeys: seq[FieldElement] = @[] - hop: seq[Hop] = @[] - delays: seq[Delay] = @[] - exitPeerId: PeerId - - # Select L mix nodes at random - let numMixNodes = mixProto.nodePool.len - var numAvailableNodes = numMixNodes - - debug "Destination data", destination - - if mixProto.nodePool.get(destination.peerId).isSome: - numAvailableNodes = numMixNodes - 1 - - if numAvailableNodes < PathLength: - mix_messages_error.inc(labelValues = ["Entry", "LOW_MIX_POOL"]) - return err( - fmt"No. of public mix nodes ({numAvailableNodes}) less than path length ({PathLength})." - ) - - # Skip the destination peer - var poolPeerIds = mixProto.nodePool.peerIds() - var availableIndices = toSeq(0 ..< poolPeerIds.len) - - let index = poolPeerIds.find(destination.peerId) - if index != -1: - availableIndices.del(index) - elif destination.kind == MixNode: - return err("Destination does not support mix") - - var nextHopAddr: MultiAddress - var nextHopPeerId: PeerId - while hop.len < PathLength: - if availableIndices.len == 0: - mix_messages_error.inc(labelValues = ["Entry", "LOW_MIX_POOL"]) - return err("Ran out of available mix nodes while constructing path") - - let randomIndexPosition = cryptoRandomInt(mixProto.rng, availableIndices.len).valueOr: - mix_messages_error.inc(labelValues = ["Entry", "NON_RECOVERABLE"]) - return err(fmt"Failed to generate random number: {error}") - let selectedIndex = availableIndices[randomIndexPosition] - var randPeerId = poolPeerIds[selectedIndex] - availableIndices.del(randomIndexPosition) - - if destination.kind == ForwardAddr and randPeerId == destination.peerId: - # Skip the destination peer - continue - - # Last hop will be the exit node that will forward the request - if hop.len == PathLength - 1: - case destination.kind - of ForwardAddr: - # Last hop will be the exit node that will fwd the request - exitPeerId = randPeerId - of MixNode: - # Exist node will be the destination - exitPeerId = destination.peerId - randPeerId = destination.peerId - - debug "Selected mix node: ", indexInPath = hop.len, peerId = randPeerId - - # Extract multiaddress, mix public key, and hop - let mixPubInfoOpt = mixProto.nodePool.get(randPeerId) - if mixPubInfoOpt.isNone: - mix_messages_error.inc(labelValues = ["Entry", "INVALID_MIX_INFO"]) - trace "Failed to get mix pub info for peer, skipping and removing node from pool", - peerId = randPeerId - # Remove this node from the pool to prevent future selection - discard mixProto.nodePool.remove(randPeerId) - # Skip this node and try another - continue - let (peerId, multiAddr, mixPubKey, _) = mixPubInfoOpt.get().get() - - # Validate multiaddr before committing this node to the path - let multiAddrBytes = multiAddrToBytes(peerId, multiAddr).valueOr: - mix_messages_error.inc(labelValues = ["Entry", "INVALID_MIX_INFO"]) - trace "Failed to convert multiaddress to bytes, skipping and removing node from pool", - error = error, peerId = peerId, multiAddr = multiAddr - # Remove this node from the pool to prevent future selection - discard mixProto.nodePool.remove(randPeerId) - # Skip this node with invalid multiaddr and try another - # in future lookup in peerStore to see if there is any other valid multiaddr for this peer and use that. - continue - - # Only add to path after validation succeeds - publicKeys.add(mixPubKey) - - if hop.len == 0: - nextHopAddr = multiAddr - nextHopPeerId = peerId - - let hopDelay = - if hop.len != PathLength - 1: - mixProto.delayStrategy.generateForEntry() - else: - NoDelay # No delay for exit node - - delays.add(hopDelay) - - hop.add(Hop.init(multiAddrBytes)) - - # Encode destination - let destHop = - if destination.kind == ForwardAddr: - let destAddrBytes = multiAddrToBytes(destination.peerId, destination.address).valueOr: - mix_messages_error.inc(labelValues = ["Entry", "INVALID_DEST"]) - return err(fmt"Failed to convert multiaddress to bytes: {error}") - Hop.init(destAddrBytes) - else: - Hop() - - let msgWithSurbs = mixProto.prepareMsgWithSurbs( - incoming, msg, numSurbs, destination.peerId, exitPeerId - ).valueOr: - return err(fmt"Could not prepend SURBs: {error}") - - let message = buildMessage(msgWithSurbs, codec, mixProto.mixNodeInfo.peerId).valueOr: - mix_messages_error.inc(labelValues = ["Entry", error[1]]) - return err(fmt"Error building message: {error[0]}") - - # Wrap in Sphinx packet - let sphinxPacket = wrapInSphinxPacket(message, publicKeys, delays, hop, destHop).valueOr: - mix_messages_error.inc(labelValues = ["Entry", "NON_RECOVERABLE"]) - return err(fmt"Failed to wrap in sphinx packet: {error}") - - # Send the wrapped message to the first mix node in the selected path - return await mixProto.sendPacket(nextHopPeerId, nextHopAddr, sphinxPacket, logConfig) - -proc reply( - mixProto: MixProtocol, surb: SURB, msg: seq[byte] -) {.async: (raises: [CancelledError]).} = - let (peerId, multiAddr) = surb.hop.get().bytesToMultiAddr().valueOr: - error "could not obtain multiaddress from hop", err = error - return - - # Message does not require a codec, as it is already associated to a specific I - let message = buildMessage(msg, "", peerId).valueOr: - error "could not build reply message", err = error - return - - let sphinxPacket = useSURB(surb, message) - - let sendRes = await mixProto.sendPacket( - peerId, multiAddr, sphinxPacket, SendPacketLogConfig(logType: Reply) - ) - if sendRes.isErr: - error "could not send reply", peerId, multiAddr, err = sendRes.error - -type PathNode = object - peerId: PeerId - multiAddr: MultiAddress - mixPubKey: FieldElement - -proc selectRandomNodes( - mixProto: MixProtocol, count: int, excludePeerIds: HashSet[PeerId] -): Result[seq[PathNode], string] {.raises: [].} = - ## Select `count` random mix nodes from the pool, excluding specified peers. - let available = mixProto.nodePool.peerIds().filterIt(it notin excludePeerIds) - - if available.len < count: - return err( - "Not enough mix nodes in pool (available=" & $available.len & ", needed=" & $count & - ")" - ) - - let selectedPeerIds = mixProto.rng.pick(available, count).valueOr: - return err("No mix nodes available in pool") - - var selected: seq[PathNode] = @[] - for peerId in selectedPeerIds: - let mixPubInfo = mixProto.nodePool.get(peerId).valueOr: - return err("Could not get mix pub info for peer: " & $peerId) - selected.add( - PathNode( - peerId: mixPubInfo.peerId, - multiAddr: mixPubInfo.multiAddr, - mixPubKey: mixPubInfo.mixPubKey, - ) - ) - - ok(selected) - -proc buildCoverPacket*( - mixProto: MixProtocol -): Result[CoverPacketBuild, string] {.raises: [].} = - ## Build a cover Sphinx packet with a loop path (self = exit node), - ## random payload . - let nodes = mixProto.selectRandomNodes( - PathLength - 1, [mixProto.mixNodeInfo.peerId].toHashSet - ).valueOr: - return err(error) - - var - publicKeys: seq[FieldElement] = @[] - hops: seq[Hop] = @[] - delays: seq[Delay] = @[] - - for node in nodes: - let addrBytes = multiAddrToBytes(node.peerId, node.multiAddr).valueOr: - return err("Failed to convert multiaddress to bytes: " & error) - publicKeys.add(node.mixPubKey) - hops.add(Hop.init(addrBytes)) - delays.add(mixProto.delayStrategy.generateForEntry()) - - let selfAddrBytes = multiAddrToBytes( - mixProto.mixNodeInfo.peerId, mixProto.mixNodeInfo.multiAddr - ).valueOr: - return err("Failed to convert self multiaddress to bytes: " & error) - publicKeys.add(mixProto.mixNodeInfo.mixPubKey) - hops.add(Hop.init(selfAddrBytes)) - delays.add(NoDelay) - - # Identify cover packets at exit processing - let maxMsgSize = getMaxMessageSizeForCodec(CoverTrafficCodec).valueOr: - return err("Failed to get max message size for cover codec: " & error) - var randomPayload = newSeq[byte](maxMsgSize) - mixProto.rng[].generate(randomPayload) - - let message = buildMessage( - randomPayload, CoverTrafficCodec, mixProto.mixNodeInfo.peerId - ).valueOr: - return err("Error building cover message: " & error[0]) - - let sphinxPacket = wrapInSphinxPacket(message, publicKeys, delays, hops, Hop()).valueOr: - return err("Failed to wrap cover sphinx packet: " & error) - - let (packetToSend, proofToken) = mixProto.generateAndAppendProof( - sphinxPacket.serialize(), "Cover" - ).valueOr: - return err("Failed to generate proof for cover packet: " & error) - - let firstNode = nodes[0] - ok( - CoverPacketBuild( - packet: packetToSend, - firstHopPeerId: firstNode.peerId, - firstHopAddr: firstNode.multiAddr, - proofToken: proofToken, - ) - ) - -proc sendCoverPacket*( - mixProto: MixProtocol, peerId: PeerId, multiAddr: MultiAddress, packet: seq[byte] -): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - try: - await mixProto.writeLp(peerId, @[multiAddr], @[MixProtocolID], packet) - mix_messages_forwarded.inc(labelValues = ["Cover"]) - return ok() - except DialFailedError as exc: - mix_messages_error.inc(labelValues = ["Cover", "SEND_FAILED"]) - return err("Failed to dial for cover packet: " & exc.msg) - except LPStreamError as exc: - mix_messages_error.inc(labelValues = ["Cover", "SEND_FAILED"]) - return err("Failed to write cover packet: " & exc.msg) - -method start*(mixProto: MixProtocol) {.async: (raises: [CancelledError]).} = - await procCall LPProtocol(mixProto).start() - mixProto.coverTraffic.withValue(ct): - await ct.start() - -method stop*(mixProto: MixProtocol) {.async: (raises: []).} = - ## Stop the MixProtocol background tasks and cancel all in-flight - ## handleMixMessages futures. - mixProto.started = false - - await mixProto.tagManager.stop() - - mixProto.coverTraffic.withValue(ct): - await ct.stop() - - # Snapshot the list and clear it before cancelling. - let pending = mixProto.ongoingMixMessages - mixProto.ongoingMixMessages = @[] - if pending.len > 0: - await noCancel allFutures(pending.mapIt(it.cancelAndWait())) - -proc init*( - mixProto: MixProtocol, - mixNodeInfo: MixNodeInfo, - switch: Switch, - tagManager: TagManager = TagManager.new(), - spamProtection: Opt[SpamProtection] = default(Opt[SpamProtection]), - delayStrategy: Opt[DelayStrategy] = Opt.none(DelayStrategy), - coverTraffic: Opt[CoverTraffic] = Opt.none(CoverTraffic), -) {.raises: [].} = - ## Initialize a MixProtocol instance. - ## - ## Mix node public keys should be populated via the nodePool after - ## initialization using `mixProto.nodePool.add(mixPubInfo)`. - ## - ## When `spamProtection` is enabled, callers should prefer - ## `SpamProtectionDelayStrategy` to avoid timing correlation between proof - ## generation and short exponential delays. - - doAssert not switch.rng.isNil, "Switch must have RNG initialized" - - mixProto.mixNodeInfo = mixNodeInfo - mixProto.switch = switch - mixProto.rng = switch.rng - mixProto.nodePool = MixNodePool.new(switch.peerStore) - mixProto.tagManager = tagManager - mixProto.destReadBehavior = newTable[string, DestReadBehavior]() - - mixProto.spamProtection = spamProtection - mixProto.delayStrategy = delayStrategy.valueOr: - NoSamplingDelayStrategy.new(switch.rng) - - mixProto.coverTraffic = coverTraffic - coverTraffic.withValue(ct): - ct.setCoverPacketBuilder( - proc(): Result[CoverPacketBuild, string] {.gcsafe, raises: [].} = - mixProto.buildCoverPacket() - ) - ct.setCoverPacketSender( - proc( - peerId: PeerId, multiAddr: MultiAddress, packet: seq[byte] - ): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - await mixProto.sendCoverPacket(peerId, multiAddr, packet) - ) - # Note: useInternalEpochTimer must be set to false when SpamProtection is - # present, as SpamProtection provides epoch change notifications via - # notifyEpochChange. Having both active would cause double epoch advances. - spamProtection.withValue(sp): - sp.registerOnEpochChange( - proc(epoch: uint64) {.gcsafe, raises: [].} = - ct.onEpochChange(epoch) - ) - ct.setProofTokenValidator( - proc(token: seq[byte]): bool {.gcsafe, raises: [].} = - sp.isProofTokenValid(token) - ) - ct.setProofTokenReclaimer( - proc(token: seq[byte]) {.gcsafe, raises: [].} = - sp.reclaimProofToken(token) - ) - - let onReplyDialer = proc( - surb: SURB, message: seq[byte] - ) {.async: (raises: [CancelledError]).} = - await mixProto.reply(surb, message) - - mixProto.exitLayer = ExitLayer.init(switch, onReplyDialer, mixProto.destReadBehavior) - - mixProto.codecs = @[MixProtocolID] - mixProto.handler = proc( - conn: Connection, proto: string - ) {.async: (raises: [CancelledError]).} = - try: - await mixProto.handleMixNodeConnection(conn) - except LPStreamError as e: - debug "Stream error", conn = conn, err = e.msg - -proc new*( - T: typedesc[MixProtocol], - mixNodeInfo: MixNodeInfo, - switch: Switch, - tagManager: TagManager = TagManager.new(), - spamProtection: Opt[SpamProtection] = default(Opt[SpamProtection]), - delayStrategy: Opt[DelayStrategy] = Opt.none(DelayStrategy), - coverTraffic: Opt[CoverTraffic] = Opt.none(CoverTraffic), -): T {.raises: [].} = - ## Create a new MixProtocol instance. - ## - ## Mix node public keys should be populated via the nodePool after - ## creation using `mixProto.nodePool.add(mixPubInfo)`. - ## - ## When `spamProtection` is enabled, callers should prefer - ## `SpamProtectionDelayStrategy` to avoid timing correlation between proof - ## generation and short exponential delays. - let mixProto = new(T) - mixProto.init( - mixNodeInfo, switch, tagManager, spamProtection, delayStrategy, coverTraffic - ) - mixProto diff --git a/libp2p/protocols/mix/multiaddr.nim b/libp2p/protocols/mix/multiaddr.nim deleted file mode 100644 index 88db2ee8fc..0000000000 --- a/libp2p/protocols/mix/multiaddr.nim +++ /dev/null @@ -1,111 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import results, sugar, sequtils, strutils -import ./serialization -import stew/endians2 -import ../../[multicodec, multiaddress, peerid] - -const - PeerIdByteLen = 39 # ed25519 and secp256k1 multihash length - MinMultiAddrComponentLen = 2 - MaxMultiAddrComponentLen = 5 # quic + circuit relay - -# TODO: Add support for ipv6, dns, dns4, ws/wss/sni support - -proc getBaseTransport*( - multiAddr: MultiAddress -): Result[MultiAddress, string] {.raises: [].} = - ## Extracts the base transport portion of a multiaddress, stripping any - ## circuit-relay suffix (/p2p//p2p-circuit). - ## Returns the base address (e.g., /ip4/.../tcp/... or /ip4/.../udp/.../quic-v1). - let sma = multiAddr.items().toSeq() - - if not (sma.len >= MinMultiAddrComponentLen and sma.len <= MaxMultiAddrComponentLen): - return err("Invalid multiaddress format") - - let isCircuitRelay = ?multiAddr.contains(multiCodec("p2p-circuit")) - let baseP2PEndIdx = if isCircuitRelay: 3 else: 1 - - try: - if sma.len - baseP2PEndIdx < 0: - return err("Invalid multiaddress format") - ok(sma[0 .. sma.len - baseP2PEndIdx].mapIt(it.tryGet()).foldl(a & b)) - except LPError as exc: - err("Could not obtain base address: " & exc.msg) - -proc multiAddrToBytes*( - peerId: PeerId, multiAddr: MultiAddress -): Result[seq[byte], string] {.raises: [].} = - var ma = multiAddr - var res: seq[byte] = @[] - - let baseAddr = ?getBaseTransport(multiAddr) - - # Only IPV4 is supported - let isCircuitRelay = ?ma.contains(multiCodec("p2p-circuit")) - - let isQuic = QUIC_V1_IP4.match(baseAddr) - let isTCP = TCP_IP4.match(baseAddr) - - if not (isTCP or isQuic): - return err("Unsupported protocol") - - # 4 bytes for the IP - let ip = ?ma.getPart(multiCodec("ip4")).value().protoArgument() - res.add(ip) - - var port: string - if isQuic: - res.add(1.byte) # Protocol byte - let udpPortPart = ma.getPart(multiCodec("udp")).value() - port = $udpPortPart - elif isTCP: - res.add(0.byte) # Protocol byte - let tcpPortPart = ma.getPart(multiCodec("tcp")).value() - port = $tcpPortPart - let portNum = ?catch(port.split('/')[2].parseInt()).mapErr(x => x.msg) - res.add(portNum.uint16.toBytesBE()) - - if isCircuitRelay: - let relayIdPart = ?ma.getPart(multiCodec("p2p")) - let relayId = ?PeerId.init(?relayIdPart.protoArgument()).mapErr(x => $x) - if relayId.data.len != PeerIdByteLen: - return err("unsupported PeerId key type, only Secp256k1 keys are supported") - res.add(relayId.data) - - # PeerID (39 bytes) - if peerId.data.len != PeerIdByteLen: - return err("Unsupported PeerId key type, only Secp256k1 keys are supported") - res.add(peerId.data) - - if res.len > AddrSize: - return err("Address must be <= " & $AddrSize & " bytes") - - return ok(res & newSeq[byte](AddrSize - res.len)) - -proc bytesToMultiAddr*(bytes: openArray[byte]): MaResult[(PeerId, MultiAddress)] = - if bytes.len != AddrSize: - return err("Address must be exactly " & $AddrSize & " bytes") - - let - ip = bytes[0 .. 3].mapIt($it).join(".") - protocol = if bytes[4] == 0: "tcp" else: "udp" - quic = if bytes[4] == 1: "/quic-v1" else: "" - port = uint16.fromBytesBE(bytes[5 .. 6]) - # peerId1 represents the circuit relay server addr if p2p-circuit addr, otherwise it's the node's actual peerId - peerId1Bytes = bytes[7 ..< 46] - peerId2Bytes = bytes[7 + PeerIdByteLen ..< 7 + (PeerIdByteLen * 2)] - - let ma = ?MultiAddress.init("/ip4/" & ip & "/" & protocol & "/" & $port & quic) - - return - if peerId2Bytes != newSeq[byte](PeerIdByteLen): - # Has circuit relay address - let relayIdMa = ?MultiAddress.init(multiCodec("p2p"), peerId1Bytes) - let p2pCircuitMa = ?MultiAddress.init(multiCodec("p2p-circuit")) - let peerId = ?PeerId.init(peerId2Bytes).mapErr(x => $x) - ok((peerId, ?(ma & relayIdMa & p2pCircuitMa).catch().mapErr(x => x.msg))) - else: - let peerId = ?PeerId.init(peerId1Bytes).mapErr(x => $x) - ok((peerId, ma)) diff --git a/libp2p/protocols/mix/pool.nim b/libp2p/protocols/mix/pool.nim deleted file mode 100644 index a356b210fe..0000000000 --- a/libp2p/protocols/mix/pool.nim +++ /dev/null @@ -1,110 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -## Mix Node Pool Management -## -## This module provides an abstraction layer for managing mix node information. -## It encapsulates access to the peerStore's MixPubKeyBook, AddressBook, and KeyBook, -## providing a clean interface for the mix protocol to interact with mix node data. - -import std/[sequtils, tables] -import results -import ../../peerstore -import ../../peerid -import ../../multiaddress -import ../../crypto/crypto -import ../../crypto/curve25519 -import ./mix_node -import ./multiaddr as mix_multiaddr - -export mix_node.MixPubInfo - -func isSupportedMultiaddr(maddr: MultiAddress): bool = - ## Returns true if the multiaddress is supported by the mix protocol. - ## Mix protocol supports IPv4 addresses with TCP or QUIC-v1 transports, - ## including circuit-relay addresses that use these transports. - let baseAddr = mix_multiaddr.getBaseTransport(maddr).valueOr: - return false - TCP_IP4.match(baseAddr) or QUIC_V1_IP4.match(baseAddr) - -func findSupportedMultiaddr(maddrs: seq[MultiAddress]): Opt[MultiAddress] = - ## Returns the first multiaddress that is supported by the mix protocol. - for maddr in maddrs: - if isSupportedMultiaddr(maddr): - return Opt.some(maddr) - Opt.none(MultiAddress) - -type MixNodePool* = ref object - ## Manages mix node public information through the peerStore. - ## This abstraction allows the mix protocol to interact with mix node data - ## without directly coupling to peerStore implementation details. - ## - ## Future enhancements: - ## - Peer scoring: Track node reliability, latency, and performance metrics - ## - Pool maintenance: Automatic pruning of stale/unresponsive nodes - peerStore: PeerStore - -proc new*(T: typedesc[MixNodePool], peerStore: PeerStore): T = - ## Create a new MixNodePool instance backed by the given peerStore. - T(peerStore: peerStore) - -proc add*(pool: MixNodePool, info: MixPubInfo) = - ## Add a mix node to the pool. - ## MixPubKeyBook entry is always updated. - ## Address is added to AddressBook if not already present. - ## KeyBook is only set if not already present (to avoid overwriting - ## keys set by the Identify protocol). - pool.peerStore[MixPubKeyBook][info.peerId] = info.mixPubKey - - # Add address if not already present - let existingAddrs = pool.peerStore[AddressBook][info.peerId] - if info.multiAddr notin existingAddrs: - pool.peerStore[AddressBook][info.peerId] = existingAddrs & @[info.multiAddr] - - # Only set key if peer has no key yet - let existingKey = pool.peerStore[KeyBook][info.peerId] - if existingKey.scheme != Secp256k1: - pool.peerStore[KeyBook][info.peerId] = - PublicKey(scheme: Secp256k1, skkey: info.libp2pPubKey) - -proc add*(pool: MixNodePool, infos: seq[MixPubInfo]) = - ## Add multiple mix nodes to the pool. - for info in infos: - pool.add(info) - -proc remove*(pool: MixNodePool, peerId: PeerId): bool = - ## Remove a mix node from the pool. Returns true if the node was present. - pool.peerStore[MixPubKeyBook].del(peerId) - # Note: We only delete from MixPubKeyBook. The peer may still have - # entries in AddressBook/KeyBook for other protocols. - -proc get*(pool: MixNodePool, peerId: PeerId): Opt[MixPubInfo] = - ## Get MixPubInfo for a peer. Returns none if peer is not in the pool - ## or if required information (address, key) is missing. - let mixPubKey = pool.peerStore[MixPubKeyBook][peerId] - if mixPubKey == default(Curve25519Key): - return Opt.none(MixPubInfo) - - # Get the address - prefer LastSeenOutboundBook, fall back to AddressBook - # Mix protocol only supports IPv4 addresses with TCP or QUIC-v1 transports - var supportedAddr: MultiAddress - let lastSeenAddr = pool.peerStore[LastSeenOutboundBook][peerId] - if lastSeenAddr.isSome and isSupportedMultiaddr(lastSeenAddr.get): - supportedAddr = lastSeenAddr.get - else: - supportedAddr = findSupportedMultiaddr(pool.peerStore[AddressBook][peerId]).valueOr: - return Opt.none(MixPubInfo) - - let pubKey = pool.peerStore[KeyBook][peerId] - if pubKey.scheme != Secp256k1: - return Opt.none(MixPubInfo) - - Opt.some(MixPubInfo.init(peerId, supportedAddr, mixPubKey, pubKey.skkey)) - -proc peerIds*(pool: MixNodePool): seq[PeerId] = - ## Get all peer IDs in the mix node pool. - pool.peerStore[MixPubKeyBook].book.keys.toSeq() - -proc len*(pool: MixNodePool): int = - ## Get the number of mix nodes in the pool. - pool.peerStore[MixPubKeyBook].len diff --git a/libp2p/protocols/mix/reply_connection.nim b/libp2p/protocols/mix/reply_connection.nim deleted file mode 100644 index f4cc13fb8a..0000000000 --- a/libp2p/protocols/mix/reply_connection.nim +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import hashes, chronos, chronicles -import ../../stream/connection -import ./[serialization] -from fragmentation import DataSize - -type MixReplyDialer* = proc(surbs: seq[SURB], msg: seq[byte]): Future[void] {. - async: (raises: [CancelledError, LPStreamError]) -.} - -type MixReplyConnection* = ref object of Connection - surbs: seq[SURB] - mixReplyDialer: MixReplyDialer - -method readExactly*( - self: MixReplyConnection, pbytes: pointer, nbytes: int -): Future[void] {.async: (raises: [CancelledError, LPStreamError]).} = - raise newException(LPStreamError, "MixReplyConnection does not allow reading") - -method write*( - self: MixReplyConnection, msg: seq[byte] -): Future[void] {.async: (raises: [CancelledError, LPStreamError]).} = - if msg.len() > DataSize: - raise newException(LPStreamError, "exceeds max msg size of " & $DataSize & " bytes") - await self.mixReplyDialer(self.surbs, msg) - -proc shortLog*(self: MixReplyConnection): string {.raises: [].} = - "[MixReplyConnection]" - -chronicles.formatIt(MixReplyConnection): - shortLog(it) - -method initStream*(self: MixReplyConnection) = - discard - -method closeImpl*(self: MixReplyConnection): Future[void] {.async: (raises: []).} = - discard - -func hash*(self: MixReplyConnection): Hash = - hash($self.surbs) - -proc new*( - T: typedesc[MixReplyConnection], surbs: seq[SURB], mixReplyDialer: MixReplyDialer -): T = - T(surbs: surbs, mixReplyDialer: mixReplyDialer) diff --git a/libp2p/protocols/mix/seqno_generator.nim b/libp2p/protocols/mix/seqno_generator.nim deleted file mode 100644 index 6930f62d51..0000000000 --- a/libp2p/protocols/mix/seqno_generator.nim +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import std/endians, times -import ../../peerid -import ./crypto -import ../../utils/sequninit - -type SeqNo* = uint32 - -proc init*(T: typedesc[SeqNo], data: seq[byte]): T = - var seqNo: SeqNo = 0 - let hash = sha256_hash(data) - for i in 0 .. 3: - seqNo = seqNo or (uint32(hash[i]) shl (8 * (3 - i))) - return seqNo - -proc init*(T: typedesc[SeqNo], peerId: PeerId): T = - SeqNo.init(peerId.data) - -proc generate*(seqNo: var SeqNo, messageBytes: seq[byte]) = - let - currentTime = getTime().toUnix() * 1000 - currentTimeBytes = newSeqUninit[byte](8) - bigEndian64(unsafeAddr currentTimeBytes[0], unsafeAddr currentTime) - - let s = SeqNo.init(messageBytes & currentTimeBytes) - seqNo = (seqNo + s) mod high(uint32) - -proc inc*(seqNo: var SeqNo) = - seqNo = (seqNo + 1) mod high(uint32) - # TODO: Manage sequence no. overflow in a way that it does not affect re-assembly diff --git a/libp2p/protocols/mix/serialization.nim b/libp2p/protocols/mix/serialization.nim deleted file mode 100644 index 50a1978996..0000000000 --- a/libp2p/protocols/mix/serialization.nim +++ /dev/null @@ -1,239 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import results -import std/sequtils -import ../../utility -import ./delay - -const - k* = 16 # Security parameter - r* = 5 # Maximum path length - t* = 6 # t.k - combined length of next hop address and delay - AlphaSize* = 32 # Group element - BetaSize* = ((r * (t + 1)) + 1) * k # bytes - GammaSize* = 16 # Output of HMAC-SHA-256, truncated to 16 bytes - HeaderSize* = AlphaSize + BetaSize + GammaSize # Total header size - AddrSize* = (t * k) - DelaySize # Address size - PacketSize* = 4608 # Total packet size (from spec) - MessageSize* = PacketSize - HeaderSize - k # Size of the message itself - PayloadSize* = MessageSize + k # Total payload size - SurbSize* = HeaderSize + AddrSize + k - # Size of a surb packet inside the message payload - SurbLenSize* = 1 # Size of the field storing the number of surbs - SurbIdLen* = k # Size of the identifier used when sending a message with surb - -type Header* = object - Alpha*: seq[byte] - Beta*: seq[byte] - Gamma*: seq[byte] - -proc init*( - T: typedesc[Header], alpha: seq[byte], beta: seq[byte], gamma: seq[byte] -): T = - return T(Alpha: alpha, Beta: beta, Gamma: gamma) - -proc get*(header: Header): (seq[byte], seq[byte], seq[byte]) = - (header.Alpha, header.Beta, header.Gamma) - -proc serialize*(header: Header): seq[byte] = - doAssert header.Alpha.len == AlphaSize, - "Alpha must be exactly " & $AlphaSize & " bytes" - doAssert header.Beta.len == BetaSize, "Beta must be exactly " & $BetaSize & " bytes" - doAssert header.Gamma.len == GammaSize, - "Gamma must be exactly " & $GammaSize & " bytes" - return header.Alpha & header.Beta & header.Gamma - -proc deserialize*( - T: typedesc[Header], serializedHeader: openArray[byte] -): Result[T, string] = - if len(serializedHeader) < HeaderSize: - return err("Serialized header must be exactly " & $HeaderSize & " bytes") - - let header = Header( - Alpha: serializedHeader[0 .. (AlphaSize - 1)], - Beta: serializedHeader[AlphaSize .. (AlphaSize + BetaSize - 1)], - Gamma: serializedHeader[(AlphaSize + BetaSize) .. (HeaderSize - 1)], - ) - - ok(header) - -type Message* = seq[byte] - -proc serialize*(message: Message): seq[byte] = - doAssert message.len() == MessageSize, - "Message must be exactly " & $(MessageSize) & " bytes" - - var res = newSeq[byte](k) # Prepend k bytes of zero padding - res.add(message) - return res - -proc deserialize*( - T: typedesc[Message], serializedMessage: openArray[byte] -): Result[T, string] = - if len(serializedMessage) != PayloadSize: - return err("Serialized message must be exactly " & $PayloadSize & " bytes") - return ok(serializedMessage[k ..^ 1]) - -type Hop* = object - MultiAddress: seq[byte] - -proc init*(T: typedesc[Hop], multiAddress: seq[byte]): T = - T( - MultiAddress: - if multiAddress == newSeq[byte](AddrSize): - @[] - else: - multiAddress - ) - -proc get*(hop: Hop): seq[byte] = - return hop.MultiAddress - -proc serialize*(hop: Hop): seq[byte] = - if hop.MultiAddress.len == 0: - return newSeq[byte](AddrSize) - - doAssert len(hop.MultiAddress) == AddrSize, - "MultiAddress must be exactly " & $AddrSize & " bytes" - - return hop.MultiAddress - -proc deserialize*(T: typedesc[Hop], data: openArray[byte]): Result[T, string] = - if len(data) != AddrSize: - return err("MultiAddress must be exactly " & $AddrSize & " bytes") - ok( - T( - MultiAddress: - if data == newSeq[byte](AddrSize): - @[] - else: - @data - ) - ) - -type RoutingInfo* = object - Addr: Hop - Delay: Delay - Gamma: seq[byte] - Beta: seq[byte] - -proc init*( - T: typedesc[RoutingInfo], - address: Hop, - delay: Delay, - gamma: seq[byte], - beta: seq[byte], -): T = - return T(Addr: address, Delay: delay, Gamma: gamma, Beta: beta) - -proc getRoutingInfo*(info: RoutingInfo): (Hop, Delay, seq[byte], seq[byte]) = - (info.Addr, info.Delay, info.Gamma, info.Beta) - -proc serialize*(info: RoutingInfo): seq[byte] = - doAssert info.Gamma.len() == GammaSize, - "Gamma must be exactly " & $GammaSize & " bytes" - let expectedBetaLen = ((r * (t + 1)) - t) * k - doAssert info.Beta.len() == expectedBetaLen, - "Beta must be exactly " & $expectedBetaLen & " bytes" - - let addrBytes = info.Addr.serialize() - - return addrBytes & info.Delay.toBytes() & info.Gamma & info.Beta - -proc readBytes( - data: openArray[byte], offset: var int, readSize: Opt[int] = Opt.none(int) -): Result[seq[byte], string] = - if data.len < offset: - return err("not enough data") - - readSize.withValue(size): - if data.len < offset + size: - return err("not enough data") - let slice = data[offset ..< offset + size] - offset += size - return ok(slice) - - let slice = data[offset .. ^1] - offset = data.len - return ok(slice) - -proc deserialize*(T: typedesc[RoutingInfo], data: openArray[byte]): Result[T, string] = - if len(data) != BetaSize + ((t + 1) * k): - return err("Data must be exactly " & $(BetaSize + ((t + 1) * k)) & " bytes") - - let hop = Hop.deserialize(data[0 .. AddrSize - 1]).valueOr: - return err("Deserialize hop error: " & error) - - var offset: int = AddrSize - return ok( - RoutingInfo( - Addr: hop, - Delay: Delay.fromBytes(?data.readBytes(offset, Opt.some(DelaySize))), - Gamma: ?data.readBytes(offset, Opt.some(GammaSize)), - Beta: ?data.readBytes(offset, Opt.some(BetaSize)), - ) - ) - -type SphinxPacket* = object - Hdr*: Header - Payload*: seq[byte] - -proc init*(T: typedesc[SphinxPacket], header: Header, payload: seq[byte]): T = - T(Hdr: header, Payload: payload) - -proc get*(packet: SphinxPacket): (Header, seq[byte]) = - (packet.Hdr, packet.Payload) - -proc serialize*(packet: SphinxPacket): seq[byte] = - let headerBytes = packet.Hdr.serialize() - return headerBytes & packet.Payload - -proc deserialize*(T: typedesc[SphinxPacket], data: openArray[byte]): Result[T, string] = - if len(data) != PacketSize: - return err("Sphinx packet size must be exactly " & $PacketSize & " bytes") - - let header = ?Header.deserialize(data) - - return ok(SphinxPacket(Hdr: header, Payload: data[HeaderSize ..^ 1])) - -type - Secret* = seq[seq[byte]] - - Key* = seq[byte] - - SURBIdentifier* = array[SurbIdLen, byte] - - SURB* = object - hop*: Hop - header*: Header - key*: Key - secret*: Opt[Secret] - -proc serializeMessageWithSURBs*( - msg: seq[byte], surbs: seq[SURB] -): Result[seq[byte], string] = - if surbs.len > (MessageSize - SurbLenSize - 1) div SurbSize: - return err("too many SURBs") - - let surbBytes = - surbs.mapIt(it.hop.serialize() & it.header.serialize() & it.key).concat() - ok(byte(surbs.len) & surbBytes & msg) - -proc extractSURBs*(msg: seq[byte]): Result[(seq[SURB], seq[byte]), string] = - var offset = 0 - let surbsLenBytes = ?readBytes(msg, offset, Opt.some(1)) - let surbsLen = int(surbsLenBytes[0]) - - if surbsLen > (MessageSize - SurbLenSize - 1) div SurbSize: - return err("too many SURBs") - - var surbs: seq[SURB] = newSeq[SURB](surbsLen) - for i in 0 ..< surbsLen: - let hopBytes = ?readBytes(msg, offset, Opt.some(AddrSize)) - let headerBytes = ?readBytes(msg, offset, Opt.some(HeaderSize)) - surbs[i].hop = ?Hop.deserialize(hopBytes) - surbs[i].header = ?Header.deserialize(headerBytes) - surbs[i].key = ?readBytes(msg, offset, Opt.some(k)) - let msg = ?readBytes(msg, offset) - return ok((surbs, msg)) diff --git a/libp2p/protocols/mix/spam_protection.nim b/libp2p/protocols/mix/spam_protection.nim deleted file mode 100644 index b1e62fbc84..0000000000 --- a/libp2p/protocols/mix/spam_protection.nim +++ /dev/null @@ -1,152 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -## This module defines the spam protection interface for the Mix Protocol -## as specified in section 9.6 of the MIX protocol specification. -## -## Uses per-hop proof generation where each node generates fresh proofs for the next hop. - -import results - -type - EpochChangeCallback* = proc(epoch: uint64) {.gcsafe, raises: [].} - ## Callback invoked when the DoS protection mechanism detects an epoch transition. - ## See Mix DoS Protection spec §8.2.3. - - ProofResult* = object ## Result of spam protection proof generation. - proof*: seq[byte] ## Serialized proof bytes - token*: seq[byte] - ## Opaque token for proof slot tracking. - ## Only meaningful to the concrete implementation. - ## Used to reclaim proof slots when precomputed cover packets are discarded. - - SpamProtection* = ref object of RootObj - ## Abstract interface that spam protection mechanisms must implement - ## to integrate with the Mix Protocol. - ## Uses per-hop proof generation architecture. - proofSize*: int - epochChangeCallbacks: seq[EpochChangeCallback] - -method generateProof*( - self: SpamProtection, bindingData: seq[byte] -): Result[ProofResult, string] {.base, gcsafe, raises: [].} = - ## Generate a spam protection proof bound to specific packet data. - ## - ## Parameters: - ## bindingData: For sender-generated proofs, this is the decrypted payload - ## the hop will see; for per-hop generation, the complete - ## outgoing Sphinx packet state. - ## - ## Returns: - ## ProofResult containing serialized proof bytes and an opaque token - ## for proof slot tracking. - ## - ## Requirements: - ## - Must produce proof with length == self.proofSize - ## - Mechanism manages its own runtime state independently - ## - ## Note: This base implementation should be overridden by concrete types. - raiseAssert "generateProof must be implemented by concrete spam protection types" - -method reclaimProofToken*( - self: SpamProtection, token: seq[byte] -) {.base, gcsafe, raises: [].} = - ## Return an opaque proof token for potential reuse. - ## Called when a precomputed cover packet is discarded without being sent. - ## The concrete implementation may reclaim internal resources (e.g., rate limit slots). - ## Default: no-op. - discard - -method isProofTokenValid*( - self: SpamProtection, token: seq[byte] -): bool {.base, gcsafe, raises: [].} = - ## Check if a precomputed proof is still valid (e.g., its Merkle root - ## is still in the acceptable window). Called before sending a prebuilt - ## cover packet. If false, the packet should be discarded and rebuilt. - ## Default: always valid. - true - -method verifyProof*( - self: SpamProtection, encodedProofData: seq[byte], bindingData: seq[byte] -): Result[bool, string] {.base, gcsafe, raises: [].} = - ## Validate that a proof is correct and properly bound to packet data. - ## - ## Parameters: - ## encodedProofData: Extracted from routing block (sender approach) - ## or header field (per-hop approach) - ## bindingData: The packet-specific data against which proof is verified - ## - ## Returns: - ## Boolean indicating validity wrapped in Result. - ## - ## Requirements: - ## - Must handle malformed inputs gracefully, returning false - ## - Must atomically update internal state on successful verification - ## - Must manage state cleanup independently - ## - ## Note: This base implementation should be overridden by concrete types. - raiseAssert "verifyProof must be implemented by concrete spam protection types" - -method registerOnEpochChange*( - self: SpamProtection, cb: EpochChangeCallback -) {.base, gcsafe, raises: [].} = - self.epochChangeCallbacks.add(cb) - -proc notifyEpochChange*(self: SpamProtection, epoch: uint64) {.raises: [].} = - ## Fire all registered epoch change callbacks. - for cb in self.epochChangeCallbacks: - cb(epoch) - -# Note: To disable spam protection, pass nil as the spamProtection parameter -# when initializing MixProtocol. No no-op implementation is needed. - -# Integration helpers for per-hop spam protection -# -# Packet Structure (on wire): [Sphinx Packet: 4608 bytes][Sigma: proofSize bytes] -# -# The sigma (σ) field is appended after the Sphinx packet and contains the -# spam protection proof. This approach: -# - Keeps Sphinx packet structure unchanged (α|β|γ|δ) -# - Allows simple append/strip operations at each hop -# - Each hop: strips old proof, processes Sphinx, appends fresh proof -# - Total wire size: 4608 + proofSize bytes - -proc extractProofFromPacket*( - packetWithProof: var seq[byte], spamProtection: SpamProtection -): Result[(seq[byte], seq[byte]), string] = - ## Extract spam protection proof from end of packet and return both - ## the Sphinx packet (without proof) and the extracted proof. - ## This is called by intermediary nodes before Sphinx decryption. - ## - ## Zero-copy optimization: Takes ownership of the input packet via `var`, - ## truncates it to remove the proof, and only copies the small proof data. - ## This avoids copying the large (4608 byte) Sphinx packet. - ## - ## Returns: (sphinxPacket, proof) - let proofSize = spamProtection.proofSize - if proofSize == 0: - return ok((packetWithProof, newSeq[byte](0))) - - if packetWithProof.len < proofSize: - return err("Packet too small to contain proof") - - # Copy only the small proof data from the end using slice - let proofStartIdx = packetWithProof.len - proofSize - let proofBytes = packetWithProof[proofStartIdx ..< packetWithProof.len] - - # Truncate the packet in-place to remove proof (zero-copy for Sphinx packet) - packetWithProof.setLen(proofStartIdx) - - ok((packetWithProof, proofBytes)) - -proc appendProofToPacket*( - sphinxPacket: seq[byte], proof: seq[byte] -): Result[seq[byte], string] = - ## Append spam protection proof to end of Sphinx packet. - ## This is called when generating fresh proof for next hop. - ## - ## Returns: Complete packet with proof appended - if proof.len == 0: - return ok(sphinxPacket) - - ok(sphinxPacket & proof) diff --git a/libp2p/protocols/mix/sphinx.nim b/libp2p/protocols/mix/sphinx.nim deleted file mode 100644 index e28d204170..0000000000 --- a/libp2p/protocols/mix/sphinx.nim +++ /dev/null @@ -1,412 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import results, sequtils -import ./[crypto, curve25519, delay, serialization, tag_manager] -import ../../crypto/crypto -import ../../utils/sequninit - -const PathLength* = 3 # Path length (L) -const PaddingLength = (((t + 1) * (r - PathLength)) + 1) * k - -type ProcessingStatus* = enum - Exit - Intermediate - Reply - Duplicate - InvalidMAC - -proc computeAlpha( - publicKeys: openArray[FieldElement] -): Result[(seq[byte], seq[seq[byte]]), string] = - ## Compute alpha, an ephemeral public value. Each mix node uses its private key and - ## alpha to derive a shared session key for that hop. - ## This session key is used to decrypt and process one layer of the packet. - - if publicKeys.len == 0: - return err("No public keys provided") - - var - s: seq[seq[byte]] = newSeq[seq[byte]](publicKeys.len) - alpha_0: seq[byte] - alpha: FieldElement - secret: FieldElement - blinders: seq[FieldElement] = @[] - - let x = generateRandomFieldElement().valueOr: - return err("Generate field element error: " & error) - - blinders.add(x) - - for i in 0 ..< publicKeys.len: - if publicKeys[i].len != FieldElementSize: - return err("Invalid public key size: " & $i) - - # Compute alpha, shared secret, and blinder - if i == 0: - alpha = multiplyBasePointWithScalars([blinders[i]]).valueOr: - return err("Multiply base point with scalars error: " & error) - - alpha_0 = fieldElementToBytes(alpha) - else: - alpha = multiplyPointWithScalars(alpha, [blinders[i]]) - - # TODO: Optimize point multiplication by multiplying scalars first - secret = multiplyPointWithScalars(publicKeys[i], blinders) - - let blinder = bytesToFieldElement( - sha256_hash(fieldElementToBytes(alpha) & fieldElementToBytes(secret)) - ).valueOr: - return err("Error in bytes to field element conversion: " & error) - - blinders.add(blinder) - - s[i] = fieldElementToBytes(secret) - - return ok((alpha_0, s)) - -proc deriveKeyMaterial(keyName: string, s: seq[byte]): seq[byte] = - @(keyName.toOpenArrayByte(0, keyName.high)) & s - -proc computeFillerStrings(s: seq[seq[byte]]): Result[seq[byte], string] = - var filler: seq[byte] = @[] # Start with an empty filler string - - for i in 1 ..< s.len: - # Derive AES key and IV - let - aes_key = deriveKeyMaterial("aes_key", s[i - 1]).kdf() - iv = deriveKeyMaterial("iv", s[i - 1]).kdf() - - # Compute filler string - let - fillerLength = (t + 1) * k - zeroPadding = newSeq[byte](fillerLength) - - filler = aes_ctr_start_index( - aes_key, iv, filler & zeroPadding, (((t + 1) * (r - i)) + t + 2) * k - ) - - return ok(filler) - -proc computeBetaGamma( - s: seq[seq[byte]], - hops: openArray[Hop], - delay: openArray[Delay], - destHop: Hop, - id: SURBIdentifier, -): Result[tuple[beta: seq[byte], gamma: seq[byte]], string] = - ## Calculates the following elements: - ## - Beta: The nested encrypted routing information. It encodes the next hop address, the forwarding delay, integrity check Gamma for the next hop, and the Beta for subsequent hops. - ## - Gamma: A message authentication code computed over Beta using the session key derived from Alpha. It ensures header integrity at each hop. - let sLen = s.len - var - beta: seq[byte] - gamma: seq[byte] - - # Compute filler strings - let filler = computeFillerStrings(s).valueOr: - return err("Error in filler generation: " & error) - - for i in countdown(sLen - 1, 0): - # Derive AES key, MAC key, and IV - let - beta_aes_key = deriveKeyMaterial("aes_key", s[i]).kdf() - mac_key = deriveKeyMaterial("mac_key", s[i]).kdf() - beta_iv = deriveKeyMaterial("iv", s[i]).kdf() - - # Compute Beta and Gamma - if i == sLen - 1: - let destBytes = destHop.serialize() - let delayBytes = delay[i].toBytes() - let destPadding = destBytes & delayBytes & @id & newSeq[byte](PaddingLength) - - let aes = aes_ctr(beta_aes_key, beta_iv, destPadding) - - beta = aes & filler - else: - let routingInfo = RoutingInfo.init( - hops[i + 1], delay[i], gamma, beta[0 .. (((r * (t + 1)) - t) * k) - 1] - ) - - let serializedRoutingInfo = routingInfo.serialize() - - beta = aes_ctr(beta_aes_key, beta_iv, serializedRoutingInfo) - - gamma = hmac(mac_key, beta).toSeq() - - return ok((beta: beta, gamma: gamma)) - -proc computeDelta(s: seq[seq[byte]], msg: Message): Result[seq[byte], string] = - let sLen = s.len - var delta: seq[byte] - - for i in countdown(sLen - 1, 0): - # Derive AES key and IV - let - delta_aes_key = deriveKeyMaterial("delta_aes_key", s[i]).kdf() - delta_iv = deriveKeyMaterial("delta_iv", s[i]).kdf() - - # Compute Delta - if i == sLen - 1: - let serializedMsg = msg.serialize() - delta = aes_ctr(delta_aes_key, delta_iv, serializedMsg) - else: - delta = aes_ctr(delta_aes_key, delta_iv, delta) - - return ok(delta) - -proc createSURB*( - publicKeys: openArray[FieldElement], - delay: openArray[Delay], - hops: openArray[Hop], - id: SURBIdentifier, - rng: ref HmacDrbgContext, -): Result[SURB, string] = - if rng.isNil: - return err("rng must not be nil") - - if id == default(SURBIdentifier): - return err("id should be initialized") - - # Compute alpha and shared secrets - let (alpha_0, s) = computeAlpha(publicKeys).valueOr: - return err("Error in alpha generation: " & error) - - # Compute beta and gamma - let (beta_0, gamma_0) = computeBetaGamma(s, hops, delay, Hop(), id).valueOr: - return err("Error in beta and gamma generation: " & error) - - # Generate key - var key = newSeqUninit[byte](k) - rng[].generate(key) - - return ok( - SURB( - hop: hops[0], - header: Header.init(alpha_0, beta_0, gamma_0), - secret: Opt.some(s), - key: key, - ) - ) - -proc useSURB*(surb: SURB, msg: Message): SphinxPacket = - # Derive AES key and IV - let - delta_aes_key = deriveKeyMaterial("delta_aes_key", surb.key).kdf() - delta_iv = deriveKeyMaterial("delta_iv", surb.key).kdf() - - # Compute Delta - let serializedMsg = msg.serialize() - let delta = aes_ctr(delta_aes_key, delta_iv, serializedMsg) - - return SphinxPacket.init(surb.header, delta) - -proc processReply*( - key: seq[byte], s: seq[seq[byte]], delta_prime: seq[byte] -): Result[seq[byte], string] = - var delta = delta_prime[0 ..^ 1] - - var key_prime = key - for i in 0 .. s.len: - if i != 0: - key_prime = s[i - 1] - - let - delta_aes_key = deriveKeyMaterial("delta_aes_key", key_prime).kdf() - delta_iv = deriveKeyMaterial("delta_iv", key_prime).kdf() - - delta = aes_ctr(delta_aes_key, delta_iv, delta) - - let deserializeMsg = Message.deserialize(delta).valueOr: - return err("Message deserialization error: " & error) - - return ok(deserializeMsg) - -proc wrapInSphinxPacket*( - msg: Message, - publicKeys: openArray[FieldElement], - delay: openArray[Delay], - hop: openArray[Hop], - destHop: Hop, -): Result[SphinxPacket, string] = - # Compute alpha and shared secrets - let (alpha_0, s) = computeAlpha(publicKeys).valueOr: - return err("Error in alpha generation: " & error) - - # Compute beta and gamma - let (beta_0, gamma_0) = computeBetaGamma( - s, hop, delay, destHop, default(SURBIdentifier) - ).valueOr: - return err("Error in beta and gamma generation: " & error) - - # Compute delta - let delta_0 = computeDelta(s, msg).valueOr: - return err("Error in delta generation: " & error) - - # Serialize sphinx packet - let sphinxPacket = SphinxPacket.init(Header.init(alpha_0, beta_0, gamma_0), delta_0) - - return ok(sphinxPacket) - -type ProcessedSphinxPacket* = object - case status*: ProcessingStatus - of ProcessingStatus.Exit: - destination*: Hop - messageChunk*: seq[byte] - of ProcessingStatus.Intermediate: - nextHop*: Hop - delay*: Delay - serializedSphinxPacket*: seq[byte] - of ProcessingStatus.Reply: - id*: SURBIdentifier - delta_prime*: seq[byte] - else: - discard - -proc isZeros(data: seq[byte], startIdx: int, endIdx: int): bool = - doAssert 0 <= startIdx and endIdx < data.len and startIdx <= endIdx - for i in startIdx .. endIdx: - if data[i] != 0: - return false - return true - -template extractSurbId(data: seq[byte]): SURBIdentifier = - const startIndex = t * k - const endIndex = startIndex + SurbIdLen - 1 - doAssert data.len > startIndex and endIndex < data.len - var id: SURBIdentifier - copyMem(addr id[0], addr data[startIndex], SurbIdLen) - id - -proc computeTag(alpha: seq[byte], s: FieldElement): Tag = - ## Compute replay detection tag as H(α || s) per spec Section 8.6.1 Step 2 - sha256_hash(alpha & fieldElementToBytes(s)) - -proc checkReplay*( - sphinxPacket: SphinxPacket, privateKey: FieldElement, tm: var TagManager -): Result[tuple[isReplay: bool, sharedSecret: FieldElement], string] = - ## Check if a Sphinx packet is a replay without doing full processing. - ## Returns (isReplay, sharedSecret) to enable reuse of expensive EC multiplication. - ## If not a replay, the tag is immediately added to prevent race conditions. - let - (header, _) = sphinxPacket.get() - (alpha, _, _) = header.get() - - # Compute shared secret - let alphaFE = bytesToFieldElement(alpha).valueOr: - return err("Error in bytes to field element conversion: " & error) - - let s = multiplyPointWithScalars(alphaFE, [privateKey]) - - # Compute tag as H(α || s) per spec - let tag = computeTag(alpha, s) - - # Atomically check and add the tag to prevent race conditions - # (checkAndAddTag returns true if already present, false if newly added) - let isDuplicate = checkAndAddTag(tm, tag) - - ok((isReplay: isDuplicate, sharedSecret: s)) - -proc processSphinxPacket*( - sphinxPacket: SphinxPacket, - privateKey: FieldElement, - tm: var TagManager, - sharedSecret: Opt[FieldElement] = Opt.none(FieldElement), -): Result[ProcessedSphinxPacket, string] = - let - (header, payload) = sphinxPacket.get() - (alpha, beta, gamma) = header.get() - - # Compute shared secret (or reuse if provided) - let s = sharedSecret.valueOr: - let alphaFE = bytesToFieldElement(alpha).valueOr: - return err("Error in bytes to field element conversion: " & error) - multiplyPointWithScalars(alphaFE, [privateKey]) - - let sBytes = fieldElementToBytes(s) - - # Compute tag as H(α || s) per spec Section 8.6.1 Step 2 - let tag = computeTag(alpha, s) - - # Check if the tag has been seen (only if we didn't get pre-validated sharedSecret) - if sharedSecret.isNone and isTagSeen(tm, tag): - return ok(ProcessedSphinxPacket(status: Duplicate)) - - # Compute MAC - let mac_key = deriveKeyMaterial("mac_key", sBytes).kdf() - - if not (hmac(mac_key, beta).toSeq() == gamma): - # If MAC not verified - return ok(ProcessedSphinxPacket(status: InvalidMAC)) - - # Add tag if it wasn't already added by checkReplay - if sharedSecret.isNone: - tm.addTag(tag) - - # Derive AES key and IV - let - beta_aes_key = deriveKeyMaterial("aes_key", sBytes).kdf() - beta_iv = deriveKeyMaterial("iv", sBytes).kdf() - - delta_aes_key = deriveKeyMaterial("delta_aes_key", sBytes).kdf() - delta_iv = deriveKeyMaterial("delta_iv", sBytes).kdf() - - # Compute delta - let delta_prime = aes_ctr(delta_aes_key, delta_iv, payload) - - # Compute B - let zeroPadding = newSeq[byte]((t + 1) * k) - let B = aes_ctr(beta_aes_key, beta_iv, beta & zeroPadding) - - # Check if B has the required prefix for the original message - if B.isZeros((t + 1) * k, ((t + 1) * k) + PaddingLength - 1): - let hop = Hop.deserialize(B[0 .. AddrSize - 1]).valueOr: - return err(error) - - if B.isZeros(AddrSize, ((t + 1) * k) - 1): - if delta_prime.isZeros(0, k - 1): - let msg = Message.deserialize(delta_prime).valueOr: - return err("Message deserialization error: " & error) - return ok( - ProcessedSphinxPacket( - status: Exit, destination: hop, messageChunk: msg[0 .. MessageSize - 1] - ) - ) - else: - return err("delta_prime should be all zeros") - elif B.isZeros(0, (t * k) - 1): - return ok( - ProcessedSphinxPacket( - status: Reply, id: B.extractSurbId(), delta_prime: delta_prime - ) - ) - else: - # Extract routing information from B - let routingInfo = RoutingInfo.deserialize(B).valueOr: - return err("Routing info deserialization error: " & error) - - let (address, delay, gamma_prime, beta_prime) = routingInfo.getRoutingInfo() - - # Compute alpha - let blinder = bytesToFieldElement(sha256_hash(alpha & sBytes)).valueOr: - return err("Error in bytes to field element conversion: " & error) - - let alphaFE = bytesToFieldElement(alpha).valueOr: - return err("Error in bytes to field element conversion: " & error) - - let alpha_prime = multiplyPointWithScalars(alphaFE, [blinder]) - - # Serialize sphinx packet - let sphinxPkt = SphinxPacket.init( - Header.init(fieldElementToBytes(alpha_prime), beta_prime, gamma_prime), - delta_prime, - ) - - return ok( - ProcessedSphinxPacket( - status: Intermediate, - nextHop: address, - delay: delay, - serializedSphinxPacket: sphinxPkt.serialize(), - ) - ) diff --git a/libp2p/protocols/mix/tag_manager.nim b/libp2p/protocols/mix/tag_manager.nim deleted file mode 100644 index e9c054036c..0000000000 --- a/libp2p/protocols/mix/tag_manager.nim +++ /dev/null @@ -1,97 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -## Tag Manager for Mix Protocol Replay Protection -## -## Uses TimedCache with refreshOnPut=false to ensure first-seen time is preserved -## (important for replay protection where extending expiry on re-add would be a vulnerability). - -import chronicles, chronos -import ../pubsub/timedcache -import ../../utils/heartbeat - -const - DefaultTagTTL* = chronos.hours(1) - DefaultPurgeInterval* = chronos.minutes(5) - -type - ## Tag is H(α || s) as per spec Section 8.6.1 Step 2 - Tag* = array[32, byte] - - TagManager* = ref object - cache: TimedCache[Tag] - tagTTL: Duration - purgeInterval: Duration - purgeLoop: Future[void] - -proc len*(tm: TagManager): int {.inline.} = - ## Returns the number of tags currently stored. - tm.cache.len - -proc purgeExpiredTags*(tm: TagManager, now: Moment = Moment.now()): int = - ## Remove tags that have expired. - ## Returns the number of tags purged. - let before = tm.cache.len - tm.cache.expire(now) - before - tm.cache.len - -proc purgeLoopProc(tm: TagManager) {.async: (raises: [CancelledError]).} = - ## Periodically purges expired tags using the heartbeat pattern. - heartbeat "Tag purge", tm.purgeInterval, sleepFirst = true: - let purged = tm.purgeExpiredTags() - if purged > 0: - trace "Purged expired replay tags", count = purged, remaining = tm.len - -proc start*(tm: TagManager) = - ## Start the background purge loop. - if tm.purgeLoop.isNil or tm.purgeLoop.finished: - tm.purgeLoop = tm.purgeLoopProc() - -proc stop*(tm: TagManager) {.async: (raises: []).} = - ## Stop the background purge loop and wait for it to finish. - if not tm.purgeLoop.isNil: - await tm.purgeLoop.cancelAndWait() - -proc stopSoon*(tm: TagManager) = - ## Stop the background purge loop without waiting. - ## Use this in non-async contexts or when immediate return is needed. - if not tm.purgeLoop.isNil: - tm.purgeLoop.cancelSoon() - -proc new*( - T: typedesc[TagManager], - tagTTL: Duration = DefaultTagTTL, - purgeInterval: Duration = DefaultPurgeInterval, - autoStart: bool = true, -): T = - let tm = T( - cache: TimedCache[Tag].init(timeout = tagTTL, refreshOnPut = false), - tagTTL: tagTTL, - purgeInterval: purgeInterval, - ) - if autoStart: - tm.start() - tm - -proc addTag*(tm: TagManager, tag: Tag, now: Moment = Moment.now()) = - ## Add a tag to the manager. If already present, this is a no-op - ## (does not refresh expiry - first seen time is what matters for replay protection). - discard tm.cache.put(tag, now) - -proc isTagSeen*(tm: TagManager, tag: Tag): bool {.inline.} = - ## Check if a tag has been seen (and hasn't expired). - tag in tm.cache - -proc checkAndAddTag*(tm: TagManager, tag: Tag, now: Moment = Moment.now()): bool = - ## Atomically check if a tag exists and add it if not. - ## Returns true if the tag was already present (duplicate), false if newly added. - ## This prevents race conditions in concurrent replay detection. - tm.cache.put(tag, now) - -proc removeTag*(tm: TagManager, tag: Tag) = - ## Remove a specific tag. - discard tm.cache.del(tag) - -proc clearTags*(tm: TagManager) = - ## Remove all tags. - tm.cache = TimedCache[Tag].init(timeout = tm.tagTTL, refreshOnPut = false) diff --git a/tests/libp2p/mix/component/test_connection_api.nim b/tests/libp2p/mix/component/test_connection_api.nim deleted file mode 100644 index a728944dcb..0000000000 --- a/tests/libp2p/mix/component/test_connection_api.nim +++ /dev/null @@ -1,93 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos, results -import ../../../../libp2p/[protocols/mix, protocols/mix/mix_protocol, switch, builders] - -import ../../../tools/[lifecycle, unittest] -import ../utils - -suite "Mix Protocol - Connection API": - asyncTeardown: - checkTrackers() - - asyncTest "send fails when pool has not enough nodes": - let nodes = await setupMixNodes(3) # each node's pool = 2 peers (< PathLength) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let conn = nodes[0].toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - defer: - await conn.close() - - expect LPStreamError: - await conn.writeLp(@[1.byte, 2, 3]) - - asyncTest "toConnection rejects expectReply without destReadBehavior": - let nodes = await setupMixNodes(10) # no destReadBehavior registered - startAndDeferStop(nodes) - - # No destination protocol needed — toConnection should fail - let destNode = createSwitch() - await destNode.start() - defer: - await destNode.stop() - - let conn = nodes[0].toConnection( - destNode.toMixDestination(), - "/test/codec", - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - - check: - conn.isErr - conn.error == "no destination read behavior for codec" - - asyncTest "read from write-only connection raises error": - let nodes = await setupMixNodes(10) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let conn = nodes[0] - .toConnection( - destNode.toMixDestination(), - nrProto.codec, # no expectReply, connection is write-only - ) - .expect("could not build connection") - defer: - await conn.close() - - expect LPStreamError: - discard await conn.readLp(1024) - - asyncTest "write rejects oversized messages": - let nodes = await setupMixNodes(10) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let conn = nodes[0].toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - defer: - await conn.close() - - # Write a message at exactly the maximum allowed size — should succeed - let maxMessageSize = getMaxMessageSizeForCodec(nrProto.codec, 0).get() - await conn.write(newSeq[byte](maxMessageSize)) - - # Write a message one byte over the limit — should be rejected - expect LPStreamError: - await conn.write(newSeq[byte](maxMessageSize + 1)) diff --git a/tests/libp2p/mix/component/test_cover_traffic.nim b/tests/libp2p/mix/component/test_cover_traffic.nim deleted file mode 100644 index feee4840c8..0000000000 --- a/tests/libp2p/mix/component/test_cover_traffic.nim +++ /dev/null @@ -1,162 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos, results -import - ../../../../libp2p/[ - protocols/mix, - protocols/mix/mix_protocol, - protocols/mix/cover_traffic, - protocols/mix/delay_strategy, - protocols/mix/serialization, - protocols/mix/mix_node, - protocols/mix/spam_protection, - peerid, - multiaddress, - switch, - builders, - ] - -import metrics -import ../../../../libp2p/protocols/mix/mix_metrics -import ../../../tools/[lifecycle, unittest, crypto] -import ../[utils, spam_protection_impl] - -suite "Cover Traffic - Integration": - asyncTeardown: - checkTrackers() - - asyncTest "buildCoverPacket produces valid Sphinx that can be processed by each hop": - let nodes = await setupMixNodes(5) - startAndDeferStop(nodes) - - let node = nodes[0] - let buildRes = node.buildCoverPacket() - check buildRes.isOk - let built = buildRes.get() - - check built.packet.len == PacketSize - check built.firstHopPeerId != node.switch.peerInfo.peerId - - # Verify the packet can be deserialized as a valid Sphinx packet - let sphinxPacket = SphinxPacket.deserialize(built.packet) - check sphinxPacket.isOk - - asyncTest "cover packet traverses network and is silently discarded at exit": - let nodes = await setupMixNodes( - 5, delayStrategy = Opt.some(DelayStrategy(FixedDelayStrategy(delay: 0))) - ) - startAndDeferStop(nodes) - - let ct = ConstantRateCoverTraffic.new( - totalSlots = 10, epochDuration = 100.seconds, useInternalEpochTimer = false - ) - - ct.setCoverPacketBuilder( - proc(): Result[CoverPacketBuild, string] {.gcsafe, raises: [].} = - nodes[0].buildCoverPacket() - ) - ct.setCoverPacketSender( - proc( - peerId: PeerId, multiAddr: MultiAddress, packet: seq[byte] - ): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - return await nodes[0].sendCoverPacket(peerId, multiAddr, packet) - ) - ct.onEpochChange(1) - - let coverReceivedBefore = mix_cover_received.value() - - await ct.emitCoverPacket() - check ct.slotPool.coverClaimed == 1 - - # Verify cover packet was received at exit and silently discarded - checkUntilTimeout: - mix_cover_received.value() > coverReceivedBefore - - asyncTest "slot exhaustion blocks local send, epoch reset unblocks": - let nodes = await setupMixNodes(5) - startAndDeferStop(nodes) - - let ct = ConstantRateCoverTraffic.new(totalSlots = 2, useInternalEpochTimer = false) - ct.onEpochChange(1) - nodes[0].coverTraffic = Opt.some(CoverTraffic(ct)) - - let (destNode, _) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let incoming = newAsyncQueue[seq[byte]]() - - # First send succeeds (claims 1 of 2 slots) - let res1 = await nodes[0].anonymizeLocalProtocolSend( - incoming, @[1.byte], "/test/1.0.0", destNode.toMixDestination(), 0 - ) - check res1.isOk - - # Second send succeeds (claims 2 of 2 slots) - let res2 = await nodes[0].anonymizeLocalProtocolSend( - incoming, @[2.byte], "/test/1.0.0", destNode.toMixDestination(), 0 - ) - check res2.isOk - - # Third send fails — slots exhausted - let res3 = await nodes[0].anonymizeLocalProtocolSend( - incoming, @[3.byte], "/test/1.0.0", destNode.toMixDestination(), 0 - ) - check res3.isErr - check res3.error == "No slots available in current epoch" - - # Epoch reset unblocks - ct.onEpochChange(2) - let res4 = await nodes[0].anonymizeLocalProtocolSend( - incoming, @[4.byte], "/test/1.0.0", destNode.toMixDestination(), 0 - ) - check res4.isOk - - asyncTest "MixProtocol wires SpamProtection epoch changes to cover traffic": - let nodeInfos = MixNodeInfo.generateRandomMany(5) - let mixNodeInfo = nodeInfos[0] - let switch = - createSwitch(mixNodeInfo.multiAddr, Opt.some(mixNodeInfo.libp2pPrivKey)) - - let sp = newRateLimitSpamProtection(100) - let ct = ConstantRateCoverTraffic.new(totalSlots = 5) - - discard MixProtocol.new( - mixNodeInfo, - switch, - spamProtection = Opt.some(SpamProtection(sp)), - delayStrategy = Opt.some(DelayStrategy(NoSamplingDelayStrategy.new(rng()))), - coverTraffic = Opt.some(CoverTraffic(ct)), - ) - - # Exhaust slots - for _ in 0 ..< 5: - discard ct.slotPool.claimSlot() - check ct.slotPool.availableSlots == 0 - - # SpamProtection epoch change should propagate to cover traffic - sp.notifyEpochChange(42) - check ct.slotPool.epoch == 42 - check ct.slotPool.availableSlots == 5 - - asyncTest "no cover traffic configured — send works without slot claiming": - let nodes = await setupMixNodes(5) - startAndDeferStop(nodes) - - check nodes[0].coverTraffic.isNone - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let incoming = newAsyncQueue[seq[byte]]() - let sendRes = await nodes[0].anonymizeLocalProtocolSend( - incoming, @[1.byte, 2, 3], "/test/1.0.0", destNode.toMixDestination(), 0 - ) - check sendRes.isOk - - let receivedMsg = await nrProto.receivedMessages.get().wait(2.seconds) - check receivedMsg.data.len > 0 diff --git a/tests/libp2p/mix/component/test_message_delivery.nim b/tests/libp2p/mix/component/test_message_delivery.nim deleted file mode 100644 index 9e2c5a4e91..0000000000 --- a/tests/libp2p/mix/component/test_message_delivery.nim +++ /dev/null @@ -1,265 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import algorithm, chronos, results, stew/byteutils, sequtils, tables -import - ../../../../libp2p/[ - protocols/mix, - protocols/mix/mix_protocol, - protocols/mix/delay_strategy, - protocols/ping, - peerid, - switch, - builders, - ] - -import ../../../tools/[lifecycle, unittest] -import ../utils - -suite "Mix Protocol - Message Delivery": - asyncTeardown: - checkTrackers() - - asyncTest "expect reply, exit != destination": - let nodes = await setupMixNodes( - 10, destReadBehavior = Opt.some((codec: PingCodec, callback: readExactly(32))) - ) - startAndDeferStop(nodes) - - let (destNode, pingProto) = await setupDestNode(Ping.new()) - defer: - await stopDestNode(destNode) - - let conn = nodes[0] - .toConnection( - destNode.toMixDestination(), - pingProto.codec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - .expect("could not build connection") - - let response = await pingProto.ping(conn) - await conn.close() - - check response != 0.seconds - - asyncTest "expect no reply, exit != destination": - let nodes = await setupMixNodes(10) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let conn = nodes[0].toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - - let data = @[1.byte, 2, 3, 4, 5] - await conn.writeLp(data) - await conn.close() - - let receivedMsg = await nrProto.receivedMessages.get().wait(2.seconds) - check data == receivedMsg.data - - # assert anonymity of the sender - let sender = nodes[0].switch.peerInfo.peerId - let destination = destNode.peerInfo.peerId - check: - receivedMsg.connPeerId != sender - receivedMsg.connPeerId != destination - receivedMsg.connPeerId in nodes.mapIt(it.switch.peerInfo.peerId) - - asyncTest "multiple sequential messages on same connection": - let nodes = await setupMixNodes(10) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let conn = nodes[0].toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - defer: - await conn.close() - - let messages = (0 ..< 10).mapIt(newSeqWith(5, it.byte)) - - for msg in messages: - await conn.writeLp(msg) - - var received: seq[seq[byte]] - for _ in messages: - let msg = await nrProto.receivedMessages.get().wait(2.seconds) - received.add(msg.data) - - check received.sorted == messages - - asyncTest "path nodes are random - exit node varies across messages": - let nodes = await setupMixNodes(10) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - # Send multiple messages and track which mix node delivered each one - const numMessages = 20 - var exitNodes: Table[PeerId, int] - - for i in 0 ..< numMessages: - let conn = nodes[0] - .toConnection(destNode.toMixDestination(), nrProto.codec) - .expect("could not build connection") - - await conn.writeLp(@[byte(i)]) - await conn.close() - - let receivedMsg = await nrProto.receivedMessages.get().wait(2.seconds) - exitNodes.mgetOrPut(receivedMsg.connPeerId, 0).inc() - - # With 20 messages and 9 eligible nodes, - # random selection must produce at least 3 distinct exit nodes. - # Sender must never be exit and destination must never be exit. - let sender = nodes[0].switch.peerInfo.peerId - let destination = destNode.peerInfo.peerId - check: - exitNodes.len >= 3 - sender notin exitNodes - destination notin exitNodes - - when defined(libp2p_mix_experimental_exit_is_dest): - asyncTest "expect reply, exit == destination": - let nodes = await setupMixNodes( - 10, destReadBehavior = Opt.some((codec: PingCodec, callback: readExactly(32))) - ) - - let destNode = nodes[^1] - let pingProto = Ping.new() - destNode.switch.mount(pingProto) - - startAndDeferStop(nodes) - - let conn = nodes[0] - .toConnection( - MixDestination.exitNode(destNode.switch.peerInfo.peerId), - pingProto.codec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - .expect("could not build connection") - - let response = await pingProto.ping(conn) - await conn.close() - - check response != 0.seconds - - asyncTest "length-prefixed protocol - verify readLp fix": - ## This test verifies the fix for the length prefix bug where responses - ## from protocols using readLp() were losing their length prefix when - ## flowing back through the mix network. - let testPayload = "Privacy for everyone and transparency for people in power is one way to reduce corruption".toBytes() - let echoProto = EchoProtocol.new() - - let nodes = await setupMixNodes( - 10, - destReadBehavior = - Opt.some((codec: echoProto.codec, callback: readLp(EchoMaxReadLen))), - ) - - let destNode = nodes[^1] - destNode.switch.mount(echoProto) - - startAndDeferStop(nodes) - - let conn = nodes[0] - .toConnection( - destNode.toMixDestination(), - echoProto.codec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - .expect("could not build connection") - - await conn.writeLp(testPayload) - - # Read response - this should work correctly with the length prefix fix - let response = await conn.readLp(EchoMaxReadLen) - await conn.close() - - check response == testPayload - - asyncTest "intermediate nodes apply delay": - let delay: Delay = 300 - let delayStrategy: DelayStrategy = FixedDelayStrategy(delay: delay) - let nodes = await setupMixNodes(10, delayStrategy = Opt.some(delayStrategy)) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let conn = nodes[0].toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - defer: - await conn.close() - - let startTime = Moment.now() - let data = @[1.byte, 2, 3, 4, 5] - await conn.writeLp(data) - - let receivedMsg = await nrProto.receivedMessages.get().wait(2.seconds) - let elapsed = Moment.now() - startTime - - # Path == 3, 2 intermediate hops apply delay, exit node does not. - check: - receivedMsg.data == data - elapsed >= (delay * 2).toDuration - - asyncTest "concurrent messages with SURB replies": - let echoProto = EchoProtocol.new() - - let nodes = await setupMixNodes( - 10, - destReadBehavior = - Opt.some((codec: echoProto.codec, callback: readLp(EchoMaxReadLen))), - ) - startAndDeferStop(nodes) - - let (destNode, _) = await setupDestNode(echoProto) - defer: - await stopDestNode(destNode) - - proc sendAndReceive( - node: MixProtocol, dest: MixDestination, data: seq[byte] - ): Future[seq[byte]] {.async.} = - let conn = node - .toConnection( - dest, - echoProto.codec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - .expect("could not build connection") - await conn.writeLp(data) - let response = await conn.readLp(EchoMaxReadLen) - await conn.close() - return response - - # Send concurrent echo requests with unique payload from different nodes - const numConcurrent = 5 - var futs: seq[Future[seq[byte]]] - - for i in 0 ..< numConcurrent: - let payload = newSeqWith(4, i.byte) - futs.add(sendAndReceive(nodes[i], destNode.toMixDestination(), payload)) - - let responses = await allFinished(futs) - - # Every sender must receive exactly their own payload back - for i, fut in responses: - let expected = newSeqWith(4, i.byte) - check: - fut.completed() - fut.value() == expected diff --git a/tests/libp2p/mix/component/test_node_failures.nim b/tests/libp2p/mix/component/test_node_failures.nim deleted file mode 100644 index d4a8cca00d..0000000000 --- a/tests/libp2p/mix/component/test_node_failures.nim +++ /dev/null @@ -1,297 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos, results, stew/byteutils, sequtils, tables -import - ../../../../libp2p/[ - protocols/mix, - protocols/mix/mix_protocol, - protocols/mix/sphinx, - protocols/mix/delay_strategy, - protocols/ping, - peerid, - peerstore, - multiaddress, - switch, - builders, - crypto/crypto, - crypto/secp, - ] - -import ../../../tools/[lifecycle, unittest] -import ../utils -import ../mock_mix - -suite "Mix Protocol - Node Failures": - asyncTeardown: - checkTrackers() - - asyncTest "multiple SURBs - reply received when one path unavailable": - ## 2 SURBs with separate paths. - ## Stop a node from SURB[0]'s path after forward delivery. - ## Reply must arrive via SURB[1]. - const TestCodec = "/delayed-response/test/1.0.0" - const ReadLen = 1024 - - let testPayload = "forward message".toBytes() - let responseData = "reply via surviving SURB".toBytes() - - var received = newFuture[seq[byte]]() - var proceed = newFuture[void]() - - let destProto = LPProtocol.new() - destProto.codec = TestCodec - destProto.handler = proc( - conn: Connection, proto: string - ) {.async: (raises: [CancelledError]).} = - try: - received.complete(await conn.readLp(ReadLen)) - await proceed - await conn.writeLp(responseData) - except CatchableError as e: - raiseAssert e.msg - - let (nodes, mock) = await setupMixNodesWithMock( - 10, destReadBehavior = Opt.some((codec: TestCodec, callback: readLp(ReadLen))) - ) - - let (destNode, _) = await setupDestNode(destProto) - - # Force separate SURB paths: nodes[2..4] for SURB 0, nodes[5..7] for SURB 1 - mock.surbPeerSets = @[ - nodes[2 .. 4].mapIt(it.switch.peerInfo.peerId), - nodes[5 .. 7].mapIt(it.switch.peerInfo.peerId), - ] - - await startNodes(nodes) - defer: - await stopDestNode(destNode) - await stopNodes(nodes) - - let conn = mock - .toConnection( - destNode.toMixDestination(), - TestCodec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(2))), - ) - .expect("could not build connection") - - await conn.writeLp(testPayload) - - check testPayload == await received.wait(10.seconds) - - # Stop a node from SURB[0]'s path - let nodeToStop = - nodes.filterIt(it.switch.peerInfo.peerId == mock.actualSurbPeers[0][0])[0] - await nodeToStop.switch.stop() - - # Signal dest node to send reply — must arrive via SURB[1] - proceed.complete() - - let response = await conn.readLp(ReadLen).wait(10.seconds) - await conn.close() - - check: - response == responseData - mock.receivedPacketCount == 1 - - asyncTest "multiple SURBs - both replies received, only one delivered": - ## 2 SURBs, all paths healthy. Exit sends reply via ALL SURBs. - ## Both replies arrive at the sender's mix layer, - ## but only one is delivered to the application. - let (nodes, mock) = await setupMixNodesWithMock( - 10, destReadBehavior = Opt.some((codec: PingCodec, callback: readExactly(32))) - ) - - let (destNode, pingProto) = await setupDestNode(Ping.new()) - - await startNodes(nodes) - defer: - await stopDestNode(destNode) - await stopNodes(nodes) - - let conn = mock - .toConnection( - destNode.toMixDestination(), - pingProto.codec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(2))), - ) - .expect("could not build connection") - defer: - await conn.close() - - let response = await pingProto.ping(conn) - check response != 0.seconds - - # Second read: no more replies delivered - expect LPStreamEOFError: - var buf: byte - await conn.readExactly(addr buf, 1) - - # Both SURB replies arrived at the mix layer - checkUntilTimeout: - mock.receivedPacketCount == 2 - - asyncTest "sender receives empty response when destination is unreachable": - ## Exit node gets DialFailedError, sends empty reply via SURB, - ## sender receives an empty response from readLp(). - let nodes = await setupMixNodes( - 10, destReadBehavior = Opt.some((codec: PingCodec, callback: readExactly(32))) - ) - - let (destNode, pingProto) = await setupDestNode(Ping.new()) - let destPeerId = destNode.peerInfo.peerId - let destAddr = destNode.peerInfo.addrs[0] - await stopDestNode(destNode) - - startAndDeferStop(nodes) - - let conn = nodes[0] - .toConnection( - MixDestination.init(destPeerId, destAddr), - pingProto.codec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - .expect("could not build connection") - defer: - await conn.close() - - await conn.write(@[1.byte, 2, 3, 4, 5]) - - let response = await conn.readLp(1024).wait(10.seconds) - check response.len == 0 - - asyncTest "forward path node down - hop 2 or exit": - ## With 4 mix nodes the sender (node 0) has a pool of exactly 3 nodes. - ## After sending, we identify the first hop, then stop one of the other two nodes. - - ## The high delay ensures intermediaries hold the packet long enough - ## to stop the target node before it is forwarded. - let delayStrategy: DelayStrategy = FixedDelayStrategy(delay: 1000) - - let nodes = await setupMixNodes(4, delayStrategy = Opt.some(delayStrategy)) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let sender = nodes[0] - - let conn = sender.toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - defer: - await conn.close() - - await conn.writeLp(@[1.byte, 2, 3]) - - # Filter out the first hop - let nodesToStop = - nodes[1 ..^ 1].filterIt(not sender.switch.isConnected(it.switch.peerInfo.peerId)) - - # Stop hop 2 or exit - await nodesToStop[0].switch.stop() - - # Destination must never receive the message - # Wait at least 5s to ensure failure is from the stopped node - expect AsyncTimeoutError: - discard await nrProto.receivedMessages.get().wait(5.seconds) - - asyncTest "skip and remove nodes with invalid multiaddress from pool": - # This test validates that nodes with invalid multiaddrs are: - # 1. Skipped during path construction - # 2. Removed from peerStore MixPubKeyBook - # 3. Message still succeeds if enough valid nodes remain - - # Setup all mix protocol nodes (needed for forwarding) - let nodes = await setupMixNodes(10) - - # Create invalid node with a multiaddr missing transport layer - let invalidPeerId = PeerId.random().expect("could not generate peerId") - let invalidMultiAddr = - MultiAddress.init("/ip4/0.0.0.0").expect("could not initialize invalid multiaddr") - - # Get valid keys from any node's pub info (read from pool before clearing) - let validPubInfo = nodes[0].nodePool.get(nodes[1].switch.peerInfo.peerId).get() - let (_, _, validMixPubKey, validLibp2pPubKey) = validPubInfo.get() - - let invalidPubInfo = MixPubInfo.init( - invalidPeerId, invalidMultiAddr, validMixPubKey, validLibp2pPubKey - ) - - # Calculate how many valid nodes to include in the pool - # We need at least PathLength nodes after both: - # 1. Destination node is excluded from path selection - # 2. Invalid node is removed from pool - # So we need PathLength + 1 valid nodes minimum (3 + 1 = 4) - # Since destination (nodes[1]) is one of them, we need 4 total valid nodes - let validNodesCount = min(nodes.len - 1, PathLength + 1) - check nodes.len - 1 >= PathLength + 1 - - # Save pub info from pool before clearing (need it for re-population) - var savedPubInfos: seq[MixPubInfo] - for i in 1 ..< validNodesCount + 1: - savedPubInfos.add(nodes[0].nodePool.get(nodes[i].switch.peerInfo.peerId).get()) - - # Now inject invalid node into sender's (node 0) peerStore - # Include enough valid nodes so that even after invalid node is removed, - # we still have sufficient nodes for PathLength = 3 - # First clear the existing MixPubKeyBook entries for sender's switch - let senderPeerStore = nodes[0].switch.peerStore - for peerId in senderPeerStore[MixPubKeyBook].book.keys.toSeq(): - discard senderPeerStore[MixPubKeyBook].del(peerId) - - # Add valid nodes to peerStore - for pubInfo in savedPubInfos: - senderPeerStore[MixPubKeyBook][pubInfo.peerId] = pubInfo.mixPubKey - senderPeerStore[AddressBook][pubInfo.peerId] = @[pubInfo.multiAddr] - senderPeerStore[KeyBook][pubInfo.peerId] = - PublicKey(scheme: Secp256k1, skkey: pubInfo.libp2pPubKey) - - # Add invalid node to peerStore - senderPeerStore[MixPubKeyBook][invalidPeerId] = invalidPubInfo.mixPubKey - senderPeerStore[AddressBook][invalidPeerId] = @[invalidPubInfo.multiAddr] - senderPeerStore[KeyBook][invalidPeerId] = - PublicKey(scheme: Secp256k1, skkey: invalidPubInfo.libp2pPubKey) - - # Verify pool has validNodesCount + 1 invalid node - check senderPeerStore[MixPubKeyBook].len == validNodesCount + 1 - - # Setup destination node - let nrProto = NoReplyProtocol.new() - nodes[1].switch.mount(nrProto) - - # Start all switches - they're needed as intermediate nodes in the mix path - # even though only sender (0) and destination (1) are doing protocol work - startAndDeferStop(nodes) - - # Send messages in a loop until invalid node is encountered and removed - # With validNodesCount + 1 invalid nodes and PathLength=3, we need multiple attempts - let testPayload = "test message".toBytes() - var initialPoolSize = senderPeerStore[MixPubKeyBook].len - - # Try up to 20 times to encounter the invalid node - # Stop if pool size goes below PathLength (can't construct path anymore) - for attempt in 0 ..< 20: - if senderPeerStore[MixPubKeyBook].len < PathLength: - break - - let conn = nodes[0].toConnection(nodes[1].toMixDestination(), nrProto.codec) - - if conn.isErr: - # If we can't build connection due to insufficient nodes, break - break - - await conn.get().writeLp(testPayload) - discard await nrProto.receivedMessages.get().wait(5.seconds) - await conn.get().close() - - # Check if invalid node was removed - if senderPeerStore[MixPubKeyBook].len < initialPoolSize: - break - - # Verify invalid node was removed from pool (should be 6 valid nodes remaining) - check senderPeerStore[MixPubKeyBook].len == validNodesCount diff --git a/tests/libp2p/mix/component/test_security.nim b/tests/libp2p/mix/component/test_security.nim deleted file mode 100644 index 0a5c640b7a..0000000000 --- a/tests/libp2p/mix/component/test_security.nim +++ /dev/null @@ -1,82 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos, results, stew/byteutils -import - ../../../../libp2p/[ - protocols/mix, - protocols/mix/mix_protocol, - protocols/mix/serialization, - switch, - builders, - ] - -import ../../../tools/[lifecycle, unittest] -import ../utils -import ../mock_mix - -suite "Mix Protocol - Security": - asyncTeardown: - checkTrackers() - - asyncTest "no response sent back on failure": - let nodes = await setupMixNodes(2) - startAndDeferStop(nodes) - - let targetPeerId = nodes[1].switch.peerInfo.peerId - let targetAddr = nodes[1].switch.peerInfo.addrs[0] - - # Dial the mix node directly - let conn = await nodes[0].switch.dial(targetPeerId, @[targetAddr], @[MixProtocolID]) - defer: - await conn.close() - - # Send a "corrupted packet" - raw bytes that are not initialized according to the MixMessage structure - await conn.writeLp(newSeq[byte](PacketSize)) - - # Wait briefly to give the mix node time to process, then try to read. - # Assert that MixProtocol doesn't send any information back in case of a failure. - expect AsyncTimeoutError: - var buf = newSeq[byte](1) - await conn.readExactly(addr buf[0], 1).wait(1.seconds) - - asyncTest "replay protection - duplicate packet is silently dropped": - ## Use mock to capture real Sphinx packet, then replay it. - ## The second one must be silently dropped. - ## With 4 nodes and node 1 as a sender, mock is guaranteed to be in the path. - let (nodes, mock) = await setupMixNodesWithMock(4) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - await startNodes(nodes) - defer: - await stopNodes(nodes) - - let conn = nodes[1].toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - - let testPayload = "forward message".toBytes() - await conn.writeLp(testPayload) - await conn.close() - - let receivedMsg = await nrProto.receivedMessages.get().wait(5.seconds) - check receivedMsg.data == testPayload - - # Replay: send the exact same bytes to the mock again (who is the sender doesn't matter) - let rawConn = await nodes[1].switch.dial( - mock.switch.peerInfo.peerId, @[mock.switch.peerInfo.addrs[0]], @[MixProtocolID] - ) - defer: - await rawConn.close() - - check mock.capturedBytes.len > 0 - await rawConn.writeLp(mock.capturedBytes) - - # Destination must not receive a second message - expect AsyncTimeoutError: - discard await nrProto.receivedMessages.get().wait(2.seconds) diff --git a/tests/libp2p/mix/component/test_spam_protection.nim b/tests/libp2p/mix/component/test_spam_protection.nim deleted file mode 100644 index 066c411de8..0000000000 --- a/tests/libp2p/mix/component/test_spam_protection.nim +++ /dev/null @@ -1,103 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos, results, stew/byteutils -import ../../../../libp2p/protocols/[protocol, ping, mix, mix/delay_strategy] -import ../../../tools/[lifecycle, unittest] -import ../utils - -suite "Mix Protocol - Spam Protection": - asyncTeardown: - checkTrackers() - - asyncTest "rate limiting spam protection": - const - numMixNodes = 10 - rateLimitPerNode = 10 # Each node allows this many packets - numTestPackets = 5 # Number of packets to send in test - - # Each node gets its own spam protection instance with independent rate limit - # This reflects real-world deployment where each node independently enforces limits - let nodes = await setupMixNodes( - numMixNodes, - destReadBehavior = Opt.some((codec: PingCodec, callback: readExactly(32))), - spamProtectionRateLimit = Opt.some(rateLimitPerNode), - ) - startAndDeferStop(nodes) - - let (destNode, pingProto) = await setupDestNode(Ping.new()) - defer: - await stopDestNode(destNode) - - # Send packets (within rate limit) - for i in 0 ..< numTestPackets: - let conn = nodes[0] - .toConnection( - destNode.toMixDestination(), - pingProto.codec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - ) - .expect("could not build connection") - - let response = await pingProto.ping(conn) - await conn.close() - check response != 0.seconds - - asyncTest "rate limit exceeded - message rejected at intermediate node": - ## 4 nodes, PathLength=3 => all 3 non-sender nodes are on every path. - ## Each hop calls verifyProof once, so after 3 messages each node - ## has hit the rate limit. The 4th message gets dropped mid-path. - const - numNodes = 4 # sender + 3 path nodes - rateLimit = 3 - maxDelayPerHop = 1.Delay # big delay is not really needed here - - let nodes = await setupMixNodes( - numNodes, - spamProtectionRateLimit = Opt.some(rateLimit), - delayStrategy = Opt.some(DelayStrategy(FixedDelayStrategy(delay: maxDelayPerHop))), - ) - startAndDeferStop(nodes) - - let (destNode, nrProto) = await setupDestNode(NoReplyProtocol.new()) - defer: - await stopDestNode(destNode) - - let testPayload = "test message".toBytes() - - # Send 3 messages — all should arrive - var receivedMsgFut = newSeq[Future[ReceivedMessage]](rateLimit) - for i in 0 ..< rateLimit: - let conn = nodes[0] - .toConnection(destNode.toMixDestination(), nrProto.codec) - .expect("could not build connection") - defer: - await conn.close() - - await conn.writeLp(testPayload) - receivedMsgFut[i] = nrProto.receivedMessages.get() - - for fut in receivedMsgFut: - check testPayload == (await fut).data - - # 4th message — should be dropped at intermediate node - let conn = nodes[0].toConnection(destNode.toMixDestination(), nrProto.codec).expect( - "could not build connection" - ) - defer: - await conn.close() - - await conn.writeLp(testPayload) - - expect AsyncTimeoutError: - # wait longer than the maximum time needed for a message to propagate - # through the mix protocol, to ensure that the message is not delivered. - - # maxMixDelay = number of hops * delay per hop - let maxMixDelay = (maxDelayPerHop.toDuration * (numNodes - 1)) - # maxWaitTime = maxMixDelay + 2s (to accommodate network transmission overhead) - let maxWaitTime = maxMixDelay + 2.seconds - - discard await nrProto.receivedMessages.get().wait(maxWaitTime) diff --git a/tests/libp2p/mix/mock_mix.nim b/tests/libp2p/mix/mock_mix.nim deleted file mode 100644 index d93275195a..0000000000 --- a/tests/libp2p/mix/mock_mix.nim +++ /dev/null @@ -1,85 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos, results, sets, sequtils -import - ../../../libp2p/[ - protocols/mix, - protocols/mix/mix_node, - protocols/mix/mix_protocol, - protocols/mix/pool, - protocols/mix/serialization, - peerid, - switch, - stream/lpstream, - ] - -type MockMixProtocol* = ref object of MixProtocol - surbCallIndex: int - surbPeerSets*: seq[seq[PeerId]] - ## Forces each SURB onto a known path by temporarily narrowing the node pool - ## before delegating to the real buildSurb. - ## - ## Usage: - ## mock.surbPeerSets = @[ - ## @[nodeA, nodeB, nodeC], # candidates for SURB 0 - ## @[nodeD, nodeE, nodeF], # candidates for SURB 1 - ## ] - ## Each SURB reply path needs exactly 2 intermediary nodes. buildSurb filters - ## out the exit node from candidates, so if the exit happens to be one of the candidates, - ## there would be too few. Provide 3+ to be safe. - ## - ## After the message is sent, mock.actualSurbPeers records which 2 peers were - ## selected for each SURB, so tests can target specific nodes. - actualSurbPeers*: seq[seq[PeerId]] - ## Incoming raw bytes in handleMixMessages are captured here - capturedBytes*: seq[byte] - ## Count of packets received via handleMixMessages - receivedPacketCount*: int - -method buildSurb*( - mock: MockMixProtocol, id: SURBIdentifier, destPeerId: PeerId, exitPeerId: PeerId -): Result[SURB, string] {.gcsafe, raises: [].} = - # No forced paths configured or all sets consumed — fall back to random selection - if mock.surbPeerSets.len == 0 or mock.surbCallIndex >= mock.surbPeerSets.len: - return procCall buildSurb(MixProtocol(mock), id, destPeerId, exitPeerId) - - # The exit node is randomly chosen during forward path construction, so it - # might be one of our candidates. Remove it. - let candidates = mock.surbPeerSets[mock.surbCallIndex].filterIt(it != exitPeerId) - doAssert candidates.len >= 2 - let peers = candidates[0 .. 1] - - # Pool must have >= PathLength(3) nodes. We keep our 2 desired peers + the - # exit node. buildSurb's own filter removes the exit, leaving exactly our 2. - let keepInPool = (peers & exitPeerId).toHashSet() - - var removed: seq[MixPubInfo] - for peerId in mock.nodePool.peerIds(): - if peerId notin keepInPool: - removed.add(mock.nodePool.get(peerId).get()) - discard mock.nodePool.remove(peerId) - - let surb = procCall buildSurb(MixProtocol(mock), id, destPeerId, exitPeerId) - - for info in removed: - mock.nodePool.add(info) - - mock.actualSurbPeers.add(peers) - mock.surbCallIndex.inc - surb - -method handleMixMessages*( - mock: MockMixProtocol, - fromPeerId: PeerId, - receivedBytes: sink seq[byte], - metadataBytes: sink seq[byte], -) {.async: (raises: [LPStreamError, CancelledError]).} = - mock.capturedBytes = @receivedBytes - mock.receivedPacketCount.inc - - await procCall handleMixMessages( - MixProtocol(mock), fromPeerId, receivedBytes, metadataBytes - ) diff --git a/tests/libp2p/mix/spam_protection_impl.nim b/tests/libp2p/mix/spam_protection_impl.nim deleted file mode 100644 index 520f0483ca..0000000000 --- a/tests/libp2p/mix/spam_protection_impl.nim +++ /dev/null @@ -1,138 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -import results -import ../../../libp2p/protocols/mix/spam_protection - -# Custom spam protection implementations for testing integration scenarios - -const - PoWProofSize = 8 # Size of nonce in bytes - RateLimitProofSize = 4 # Size of timestamp in bytes - MaxPoWIterations = 100000 # Maximum iterations for PoW proof generation - -type - # Simple Proof-of-Work implementation for testing - PoWSpamProtection* = ref object of SpamProtection - difficulty*: int - verificationCount*: int # Track how many verifications were performed - -proc newPoWSpamProtection*(difficulty: int = 2): PoWSpamProtection = - PoWSpamProtection( - proofSize: PoWProofSize, difficulty: difficulty, verificationCount: 0 - ) - -method generateProof*( - self: PoWSpamProtection, bindingData: seq[byte] -): Result[ProofResult, string] = - # Simplified PoW: find nonce where last byte of hash has 'difficulty' leading zeros - let bindingBytes = bindingData - var nonce: uint64 = 0 - while nonce < MaxPoWIterations: - var testData = - bindingBytes & - @[ - byte(nonce shr 56), - byte(nonce shr 48), - byte(nonce shr 40), - byte(nonce shr 32), - byte(nonce shr 24), - byte(nonce shr 16), - byte(nonce shr 8), - byte(nonce), - ] - # Simple hash: XOR all bytes - var hash: byte = 0 - for b in testData: - hash = hash xor b - - # Check if hash meets difficulty (leading zeros in binary representation) - if (hash and byte((1 shl self.difficulty) - 1)) == 0: - return ok( - ProofResult( - proof: @[ - byte(nonce shr 56), - byte(nonce shr 48), - byte(nonce shr 40), - byte(nonce shr 32), - byte(nonce shr 24), - byte(nonce shr 16), - byte(nonce shr 8), - byte(nonce), - ], - token: @[], - ) - ) - nonce += 1 - - err("Failed to find valid nonce") - -method verifyProof*( - self: PoWSpamProtection, encodedProofData: seq[byte], bindingData: seq[byte] -): Result[bool, string] = - self.verificationCount += 1 - - let proofBytes = encodedProofData - let bindingBytes = bindingData - - if proofBytes.len != 8: - return ok(false) - - # Reconstruct the test data with the provided nonce - var testData = bindingBytes & proofBytes - - # Recompute hash - var hash: byte = 0 - for b in testData: - hash = hash xor b - - # Verify difficulty requirement - ok((hash and byte((1 shl self.difficulty) - 1)) == 0) - -type - # Rate limiting implementation for testing per-hop generation - RateLimitSpamProtection* = ref object of SpamProtection - maxPacketsPerWindow*: int - packetCount*: int - lastResetTime*: int - -proc newRateLimitSpamProtection*( - maxPacketsPerWindow: int = 10 -): RateLimitSpamProtection = - RateLimitSpamProtection( - proofSize: RateLimitProofSize, - maxPacketsPerWindow: maxPacketsPerWindow, - packetCount: 0, - lastResetTime: 0, - ) - -method generateProof*( - self: RateLimitSpamProtection, bindingData: seq[byte] -): Result[ProofResult, string] = - # Generate timestamp-based proof - let timestamp = 12345 # Simplified timestamp - ok( - ProofResult( - proof: @[ - byte(timestamp shr 24), - byte(timestamp shr 16), - byte(timestamp shr 8), - byte(timestamp), - ], - token: @[], - ) - ) - -method verifyProof*( - self: RateLimitSpamProtection, encodedProofData: seq[byte], bindingData: seq[byte] -): Result[bool, string] = - let proofBytes = encodedProofData - if proofBytes.len != 4: - return ok(false) - - # Check rate limit - self.packetCount += 1 - if self.packetCount > self.maxPacketsPerWindow: - return ok(false) - - ok(true) diff --git a/tests/libp2p/mix/test_cover_traffic.nim b/tests/libp2p/mix/test_cover_traffic.nim deleted file mode 100644 index 933fb757fe..0000000000 --- a/tests/libp2p/mix/test_cover_traffic.nim +++ /dev/null @@ -1,323 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos -import results -import ../../../libp2p/protocols/mix/[cover_traffic, serialization, spam_protection] -import ../../../libp2p/[multiaddress, peerid] -import ../../../libp2p/crypto/[crypto, secp] -import ../../tools/[unittest, crypto] - -proc makePeerInfo(): (PeerId, MultiAddress) = - let kp = SkKeyPair.random(rng()[]) - let pubKeyProto = PublicKey(scheme: Secp256k1, skkey: kp.pubkey) - let pid = PeerId.init(pubKeyProto).expect("PeerId init") - let ma = MultiAddress.init("/ip4/127.0.0.1/tcp/5000").tryGet() - (pid, ma) - -proc mockBuildCoverPacket(): BuildCoverPacketProc = - let (pid, ma) = makePeerInfo() - return proc(): Result[CoverPacketBuild, string] {.gcsafe, raises: [].} = - ok( - CoverPacketBuild( - packet: newSeq[byte](PacketSize), - firstHopPeerId: pid, - firstHopAddr: ma, - proofToken: @[0x42.byte], - ) - ) - -proc mockBuildCoverPacketFailing(): BuildCoverPacketProc = - return proc(): Result[CoverPacketBuild, string] {.gcsafe, raises: [].} = - err("mock build failure") - -proc mockSendCoverPacket(sentPackets: ref seq[seq[byte]]): SendCoverPacketProc = - return proc( - peerId: PeerId, multiAddr: MultiAddress, packet: seq[byte] - ): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - sentPackets[].add(packet) - return ok() - -proc mockSendCoverPacketFailing(): SendCoverPacketProc = - return proc( - peerId: PeerId, multiAddr: MultiAddress, packet: seq[byte] - ): Future[Result[void, string]] {.async: (raises: [CancelledError]).} = - return err("mock send failure") - -suite "SlotPool": - test "exhaustion returns false for both claim types": - let pool = SlotPool.new(2) - check pool.claimSlot().success == true - check pool.claimSlotForCover() == true - check pool.claimSlot().success == false - check pool.claimSlotForCover() == false - - test "beginEpoch refills pool and clears stale packets": - let pool = SlotPool.new(10) - let (pid, ma) = makePeerInfo() - discard pool.claimSlot() - discard pool.claimSlotForCover() - pool.addPacket( - CoverPacket(packet: @[1.byte], firstHopPeerId: pid, firstHopAddr: ma) - ) - pool.beginEpoch(42) - check: - pool.epoch == 42 - pool.availableSlots == 10 - pool.coverClaimed == 0 - pool.nonCoverClaimed == 0 - pool.queuedCount == 0 # Stale packets cleared on epoch change - - test "claimSlot discards a pre-built packet and returns its proof token": - let pool = SlotPool.new(10) - let (pid, ma) = makePeerInfo() - pool.addPacket( - CoverPacket( - packet: @[0xAA.byte], - firstHopPeerId: pid, - firstHopAddr: ma, - proofToken: @[0x01.byte], - ) - ) - pool.addPacket( - CoverPacket( - packet: @[0xBB.byte], - firstHopPeerId: pid, - firstHopAddr: ma, - proofToken: @[0x02.byte], - ) - ) - let claim = pool.claimSlot() - check claim.success == true - check claim.reclaimedToken == @[0x01.byte] - check pool.queuedCount == 1 - check pool.dequeue().get().packet == @[0xBB.byte] - - test "mixed traffic draws from same pool": - let pool = SlotPool.new(3) - check pool.claimSlot().success == true - check pool.claimSlotForCover() == true - check pool.claimSlot().success == true - check pool.claimSlot().success == false - check pool.claimSlotForCover() == false - check: - pool.nonCoverClaimed == 2 - pool.coverClaimed == 1 - -suite "ConstantRateCoverTraffic cover_rate_fraction": - test "emissionInterval follows ((1+L)*P)/(f*R) formula": - # L=3, P=100s, R=100: - # f=1.0 → 400s / 100 = 4s - # f=0.5 → 400s / 50 = 8s (2× the f=1.0 interval) - # f=0.7 (default) → between the two; exact value depends on - # platform float-to-int rounding of 100.0 * 0.7. - let ctFull = ConstantRateCoverTraffic.new( - totalSlots = 100, epochDuration = 100.seconds, coverRateFraction = 1.0 - ) - let ctDefault = - ConstantRateCoverTraffic.new(totalSlots = 100, epochDuration = 100.seconds) - let ctHalf = ConstantRateCoverTraffic.new( - totalSlots = 100, epochDuration = 100.seconds, coverRateFraction = 0.5 - ) - check: - ctFull.emissionInterval == 4.seconds - ctHalf.emissionInterval == 8.seconds - ctHalf.emissionInterval == ctFull.emissionInterval * 2 - ctFull.emissionInterval < ctDefault.emissionInterval - ctDefault.emissionInterval < ctHalf.emissionInterval - - test "precomputeBatchSize respects cover_rate_fraction": - let ctFull = ConstantRateCoverTraffic.new( - totalSlots = 100, - epochDuration = 60.seconds, - coverRateFraction = 1.0, - enablePrecomputation = true, - ) - let ctDefault = ConstantRateCoverTraffic.new( - totalSlots = 100, epochDuration = 60.seconds, enablePrecomputation = true - ) - check ctFull.precomputeBatchSize >= ctDefault.precomputeBatchSize - - test "cover_rate_fraction range validation": - # Valid boundary and interior values - check ConstantRateCoverTraffic.new(totalSlots = 10, coverRateFraction = 1.0).coverRateFraction == - 1.0 - check ConstantRateCoverTraffic.new(totalSlots = 10, coverRateFraction = 0.5).coverRateFraction == - 0.5 - # Invalid: <= 0 or > 1 must be rejected - for invalid in [0.0, -0.1, 1.01, 2.0]: - expect AssertionDefect: - discard - ConstantRateCoverTraffic.new(totalSlots = 10, coverRateFraction = invalid) - - test "small cover_rate_fraction still yields at least 1 slot": - # With R=10 and f=0.05, f*R = 0.5 → rounds to 0, clamp to 1 - let ct = ConstantRateCoverTraffic.new( - totalSlots = 10, epochDuration = 4.seconds, coverRateFraction = 0.05 - ) - # emissionInterval = 4s * 4 / 1 = 16s - check ct.emissionInterval == 16.seconds - -suite "ConstantRateCoverTraffic": - test "emission sends packet and claims slot": - let sentPackets = new seq[seq[byte]] - sentPackets[] = @[] - - let ct = ConstantRateCoverTraffic.new(totalSlots = 10, epochDuration = 1.seconds) - ct.setCoverPacketBuilder(mockBuildCoverPacket()) - ct.setCoverPacketSender(mockSendCoverPacket(sentPackets)) - ct.onEpochChange(1) - - waitFor ct.emitCoverPacket() - check sentPackets[].len == 1 - check ct.slotPool.coverClaimed == 1 - - test "emission stops when exhausted, resumes after epoch change": - let sentPackets = new seq[seq[byte]] - sentPackets[] = @[] - - let ct = ConstantRateCoverTraffic.new(totalSlots = 1, epochDuration = 1.seconds) - ct.setCoverPacketBuilder(mockBuildCoverPacket()) - ct.setCoverPacketSender(mockSendCoverPacket(sentPackets)) - ct.onEpochChange(1) - - waitFor ct.emitCoverPacket() - check sentPackets[].len == 1 - - waitFor ct.emitCoverPacket() - check sentPackets[].len == 1 - - ct.onEpochChange(2) - waitFor ct.emitCoverPacket() - check sentPackets[].len == 2 - - test "build failure does not crash emission and returns slot": - let ct = ConstantRateCoverTraffic.new(totalSlots = 10, epochDuration = 1.seconds) - ct.setCoverPacketBuilder(mockBuildCoverPacketFailing()) - ct.setCoverPacketSender(mockSendCoverPacket(new seq[seq[byte]])) - ct.onEpochChange(1) - - waitFor ct.emitCoverPacket() - check ct.slotPool.coverClaimed == 0 # Slot returned on build failure - - test "send failure does not crash emission": - let ct = ConstantRateCoverTraffic.new(totalSlots = 10, epochDuration = 1.seconds) - ct.setCoverPacketBuilder(mockBuildCoverPacket()) - ct.setCoverPacketSender(mockSendCoverPacketFailing()) - ct.onEpochChange(1) - - waitFor ct.emitCoverPacket() - check ct.slotPool.coverClaimed == 1 - - asyncTest "start and stop": - let ct = ConstantRateCoverTraffic.new( - totalSlots = 10, epochDuration = 100.seconds, useInternalEpochTimer = false - ) - ct.setCoverPacketBuilder(mockBuildCoverPacket()) - ct.setCoverPacketSender(mockSendCoverPacket(new seq[seq[byte]])) - ct.onEpochChange(1) - - await ct.start() - check ct.isRunning == true - - await ct.stop() - check ct.isRunning == false - -suite "CoverTraffic Pre-computation": - test "pre-built packets used before on-demand, falls back when empty": - let sentPackets = new seq[seq[byte]] - sentPackets[] = @[] - - let ct = ConstantRateCoverTraffic.new( - totalSlots = 10, epochDuration = 1.seconds, enablePrecomputation = true - ) - ct.setCoverPacketBuilder(mockBuildCoverPacket()) - ct.setCoverPacketSender(mockSendCoverPacket(sentPackets)) - ct.onEpochChange(1) - - let (pid, ma) = makePeerInfo() - let prebuiltPacket = @[0xAA.byte] - ct.slotPool.addPacket( - CoverPacket(packet: prebuiltPacket, firstHopPeerId: pid, firstHopAddr: ma) - ) - - # First: uses pre-built - waitFor ct.emitCoverPacket() - check sentPackets[][0] == prebuiltPacket - - # Second: queue empty, falls back to on-demand - waitFor ct.emitCoverPacket() - check sentPackets[][1].len == PacketSize - - test "epoch change clears stale cover packets": - let ct = ConstantRateCoverTraffic.new( - totalSlots = 10, epochDuration = 1.seconds, enablePrecomputation = true - ) - let (pid, ma) = makePeerInfo() - ct.slotPool.addPacket( - CoverPacket(packet: @[0xAA.byte], firstHopPeerId: pid, firstHopAddr: ma) - ) - ct.onEpochChange(2) - check ct.slotPool.queuedCount == 0 # Stale packets cleared - - test "stale prebuilt proof consumes only one slot": - ## Verify that when a prebuilt proof is stale, only ONE slot is consumed - ## (the initial claimSlotForCover), not two. - let sentPackets = new seq[seq[byte]] - sentPackets[] = @[] - - let ct = ConstantRateCoverTraffic.new( - totalSlots = 2, epochDuration = 1.seconds, enablePrecomputation = true - ) - ct.setCoverPacketBuilder(mockBuildCoverPacket()) - ct.setCoverPacketSender(mockSendCoverPacket(sentPackets)) - ct.setProofTokenValidator( - proc(token: seq[byte]): bool {.gcsafe, raises: [].} = - false # All proofs are stale - ) - ct.onEpochChange(1) - - let (pid, ma) = makePeerInfo() - ct.slotPool.addPacket( - CoverPacket( - packet: @[0xAA.byte], - firstHopPeerId: pid, - firstHopAddr: ma, - proofToken: @[0x01.byte], - ) - ) - - waitFor ct.emitCoverPacket() - # Stale proof: should rebuild on-demand but consume only 1 slot - check ct.slotPool.coverClaimed == 1 - check sentPackets[].len == 1 - # The sent packet should be on-demand (PacketSize), not the prebuilt one - check sentPackets[][0].len == PacketSize - - # Second emission should still succeed (1 of 2 slots used) - waitFor ct.emitCoverPacket() - check ct.slotPool.coverClaimed == 2 - check sentPackets[].len == 2 - -suite "CoverTraffic DoS Protection Independence": - test "SpamProtection epoch change propagates to cover traffic": - let sp = SpamProtection(proofSize: 0) - let ct = ConstantRateCoverTraffic.new(totalSlots = 5) - ct.setCoverPacketBuilder(mockBuildCoverPacket()) - ct.setCoverPacketSender(mockSendCoverPacket(new seq[seq[byte]])) - - sp.registerOnEpochChange( - proc(epoch: uint64) {.gcsafe, raises: [].} = - ct.onEpochChange(epoch) - ) - - for _ in 0 ..< 5: - discard ct.slotPool.claimSlot() - check ct.slotPool.availableSlots == 0 - - sp.notifyEpochChange(10) - check: - ct.slotPool.epoch == 10 - ct.slotPool.availableSlots == 5 diff --git a/tests/libp2p/mix/test_crypto.nim b/tests/libp2p/mix/test_crypto.nim deleted file mode 100644 index e054407b62..0000000000 --- a/tests/libp2p/mix/test_crypto.nim +++ /dev/null @@ -1,129 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import nimcrypto, results -import ../../../libp2p/protocols/mix/crypto -import ../../tools/[unittest] - -suite "cryptographic_functions_tests": - test "aes_ctr_encrypt_decrypt": - let - key = cast[array[16, byte]]("thisis16byteskey") - iv = cast[array[16, byte]]("thisis16bytesiv!") - data: seq[byte] = cast[seq[byte]]("thisisdata") - - let encrypted = aes_ctr(key, iv, data) - let decrypted = aes_ctr(key, iv, encrypted) - - check: - data == decrypted - data != encrypted - - test "sha256_hash_computation": - let - data: seq[byte] = cast[seq[byte]]("thisisdata") - expectedHashHex = - "b53a20ecf0814267a83be82f941778ffda4b85fbf93a07847539f645ff5f1b9b" - expectedHash = fromHex(expectedHashHex) - hash = sha256_hash(data) - - check hash == expectedHash - - test "kdf_computation": - let - key: seq[byte] = cast[seq[byte]]("thisiskey") - expectedKdfHex = "37c9842d37dc404854428a0a3554dcaa" - expectedKdf = fromHex(expectedKdfHex) - derivedKey = kdf(key) - - check derivedKey == expectedKdf - - test "hmac_computation": - let - key: seq[byte] = cast[seq[byte]]("thisiskey") - data: seq[byte] = cast[seq[byte]]("thisisdata") - expectedHmacHex = "b075dd302655e085d35e8cef5dfdf101" - expectedHmac = fromHex(expectedHmacHex) - hmacResult = hmac(key, data) - - check hmacResult == expectedHmac - - test "aes_ctr_empty_data": - let - key = cast[array[16, byte]]("thisis16byteskey") - iv = cast[array[16, byte]]("thisis16bytesiv!") - emptyData: array[0, byte] = [] - - let encrypted = aes_ctr(key, iv, emptyData) - let decrypted = aes_ctr(key, iv, encrypted) - - check: - emptyData == decrypted - emptyData == encrypted - - test "sha256_hash_empty_data": - let - emptyData: array[0, byte] = [] - expectedHashHex = - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - expectedHash = fromHex(expectedHashHex) - hash = sha256_hash(emptyData) - - check hash == expectedHash - - test "kdf_empty_key": - let - emptyKey: array[0, byte] = [] - expectedKdfHex = "e3b0c44298fc1c149afbf4c8996fb924" - expectedKdf = fromHex(expectedKdfHex) - derivedKey = kdf(emptyKey) - - check derivedKey == expectedKdf - - test "hmac_empty_key_and_data": - let - emptyKey: array[0, byte] = [] - emptyData: array[0, byte] = [] - expectedHmacHex = "b613679a0814d9ec772f95d778c35fc5" - expectedHmac = fromHex(expectedHmacHex) - hmacResult = hmac(emptyKey, emptyData) - - check hmacResult == expectedHmac - - test "aes_ctr_start_index_zero_index": - let - key = cast[array[16, byte]]("thisis16byteskey") - iv = cast[array[16, byte]]("thisis16bytesiv!") - data: seq[byte] = cast[seq[byte]]("thisisdata") - startIndex = 0 - - let encrypted = aes_ctr_start_index(key, iv, data, startIndex) - let expected = aes_ctr(key, iv, data) - - check encrypted == expected - - test "aes_ctr_start_index_empty_data": - let - key = cast[array[16, byte]]("thisis16byteskey") - iv = cast[array[16, byte]]("thisis16bytesiv!") - emptyData: array[0, byte] = [] - startIndex = 0 - - let encrypted = aes_ctr_start_index(key, iv, emptyData, startIndex) - - check emptyData == encrypted - - test "aes_ctr_start_index_middle": - let - key = cast[array[16, byte]]("thisis16byteskey") - iv = cast[array[16, byte]]("thisis16bytesiv!") - data: seq[byte] = cast[seq[byte]]("thisisverylongdata") - startIndex = 16 - - let encrypted2 = aes_ctr_start_index(key, iv, data[startIndex ..^ 1], startIndex) - let encrypted1 = aes_ctr(key, iv, data[0 .. startIndex - 1]) - let expected = aes_ctr(key, iv, data) - - check encrypted1 & encrypted2 == expected diff --git a/tests/libp2p/mix/test_curve25519.nim b/tests/libp2p/mix/test_curve25519.nim deleted file mode 100644 index d9a29d488f..0000000000 --- a/tests/libp2p/mix/test_curve25519.nim +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import results -import ../../../libp2p/[crypto/curve25519, protocols/mix/curve25519] -import ../../tools/[unittest] - -proc isNotZero(key: FieldElement): bool = - for byte in key: - if byte != 0: - return true - return false - -suite "curve25519_tests": - test "generate_key_pair": - let (privateKey, publicKey) = generateKeyPair().expect("generate keypair error") - - check: - fieldElementToBytes(privateKey).len == FieldElementSize - fieldElementToBytes(publicKey).len == FieldElementSize - privateKey.isNotZero() - publicKey.isNotZero() - - let derivedPublicKey = multiplyBasePointWithScalars(@[privateKey]).expect( - "multiply base point with scalar error" - ) - - check publicKey == derivedPublicKey - - test "commutativity": - let - x1 = generateRandomFieldElement().expect("generate random field element error") - x2 = generateRandomFieldElement().expect("generate random field element error") - res1 = multiplyBasePointWithScalars(@[x1, x2]).expect( - "multiply base point with scalar errors" - ) - res2 = multiplyBasePointWithScalars(@[x2, x1]).expect( - "multiply base point with scalar errors" - ) - res3 = multiplyPointWithScalars(public(x2), @[x1]) - res4 = multiplyPointWithScalars(public(x1), @[x2]) - - check: - res1 == res2 - res1 == res3 - res1 == res4 diff --git a/tests/libp2p/mix/test_delay_strategy.nim b/tests/libp2p/mix/test_delay_strategy.nim deleted file mode 100644 index f7141a961a..0000000000 --- a/tests/libp2p/mix/test_delay_strategy.nim +++ /dev/null @@ -1,180 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import std/[math, sets] -import ../../../libp2p/protocols/mix/delay_strategy -import ../../tools/[unittest, crypto] - -const - NumIterations = 100 - NumSamples = 10 - Tolerance = 0.2 # 20% tolerance for statistical tests - BoundarySamples = 10000 - MaxBoundaryHitRatePct = 1 - MinBoundaryHitRatePct = 5 - ## Boundary-hit tests use large sample counts to make accidental spikes at the - ## configured min/max easy to detect while leaving room for normal rounding - ## into the boundary bucket after float -> uint16 conversion. - -proc sampleUpperBoundStats( - strategy: DelayStrategy, encodedDelay, maximumDelay: Delay, sampleCount: int -): tuple[maximumDelayHits: int, sawDelayBelowMaximum: bool] = - var - maximumDelayHits = 0 - sawDelayBelowMaximum = false - - for _ in 0 ..< sampleCount: - let delay = strategy.generateForIntermediate(encodedDelay) - check delay <= maximumDelay - if delay == maximumDelay: - inc maximumDelayHits - elif delay < maximumDelay: - sawDelayBelowMaximum = true - - (maximumDelayHits, sawDelayBelowMaximum) - -proc sampleLowerBoundStats( - strategy: DelayStrategy, encodedDelay, minimumDelay: Delay, sampleCount: int -): tuple[minimumDelayHits: int, sawDelayAboveMinimum: bool] = - var - minimumDelayHits = 0 - sawDelayAboveMinimum = false - - for _ in 0 ..< sampleCount: - let delay = strategy.generateForIntermediate(encodedDelay) - check delay >= minimumDelay - if delay == minimumDelay: - inc minimumDelayHits - elif delay > minimumDelay: - sawDelayAboveMinimum = true - - (minimumDelayHits, sawDelayAboveMinimum) - -suite "DelayStrategy": - test "NoSamplingDelayStrategy generateForEntry returns values in [0, 2]": - let strategy = NoSamplingDelayStrategy.new(rng()) - - for _ in 0 ..< NumIterations: - check strategy.generateForEntry() <= 2 - - test "NoSamplingDelayStrategy generateForIntermediate returns encoded value": - let strategy = NoSamplingDelayStrategy.new(rng()) - - check: - strategy.generateForIntermediate(100) == 100 - strategy.generateForIntermediate(200) == 200 - - test "ExponentialDelayStrategy generateForEntry returns configured mean": - let rng = rng() - - check: - ExponentialDelayStrategy.new(50, rng).generateForEntry() == 50 - ExponentialDelayStrategy.new(100, rng).generateForEntry() == 100 - - test "ExponentialDelayStrategy generateForIntermediate returns 0 for mean 0": - let strategy = ExponentialDelayStrategy.new(0, rng()) - - check strategy.generateForIntermediate(0) == 0 - - test "ExponentialDelayStrategy generateForIntermediate samples from exponential distribution": - let - strategy = ExponentialDelayStrategy.new(100, rng()) - meanDelay: Delay = 100 - numSamples = 1000 - var sum: float64 = 0 - - for _ in 0 ..< numSamples: - let delay = strategy.generateForIntermediate(meanDelay) - sum += float64(delay) - - let empiricalMean = sum / float64(numSamples) - # Allow 20% tolerance for statistical variation - check: - empiricalMean > float64(meanDelay) * (1 - Tolerance) - empiricalMean < float64(meanDelay) * (1 + Tolerance) - - test "ExponentialDelayStrategy produces variable delays": - let - strategy = ExponentialDelayStrategy.new(100, rng()) - meanDelay: Delay = 100 - - var delays = initHashSet[Delay]() - for _ in 0 ..< NumSamples: - let delay = strategy.generateForIntermediate(meanDelay) - delays.incl(delay) - - check delays.len > NumSamples div 2 - - test "ExponentialDelayStrategy never samples above the practical maximum": - let - meanDelay: Delay = 100 - negligibleProb = 0.01 - strategy = ExponentialDelayStrategy.new(meanDelay, rng(), negligibleProb) - # maxDelay = -mean * ln(negligibleProb) - maxDelay = Delay(-float64(meanDelay) * ln(negligibleProb)) - (maxDelayHits, sawDelayBelowMaximum) = - sampleUpperBoundStats(strategy, meanDelay, maxDelay, BoundarySamples) - - check sawDelayBelowMaximum - check maxDelayHits * 100 < BoundarySamples * MaxBoundaryHitRatePct - - test "ExponentialDelayStrategy respects custom negligibleProb": - let - meanDelay: Delay = 100 - negligibleProb = 0.01 # aggressive truncation: max ≈ mean * 4.6 - strategy = ExponentialDelayStrategy.new(meanDelay, rng(), negligibleProb) - maxDelay = Delay(-float64(meanDelay) * ln(negligibleProb)) - - for _ in 0 ..< 10000: - check strategy.generateForIntermediate(meanDelay) <= maxDelay - - test "ExponentialDelayStrategy never samples below the configured minimum": - let - meanDelay: Delay = 100 - minimumDelay: Delay = 100 - strategy = - ExponentialDelayStrategy.new(meanDelay, rng(), minimumDelay = minimumDelay) - (minimumDelayHits, sawDelayAboveMinimum) = - sampleLowerBoundStats(strategy, meanDelay, minimumDelay, BoundarySamples) - - check minimumDelayHits > 0 - check sawDelayAboveMinimum - check minimumDelayHits * 100 < BoundarySamples * MinBoundaryHitRatePct - - test "ExponentialDelayStrategy falls back to minimum when floor exceeds practical maximum": - let - meanDelay: Delay = 100 - negligibleProb = 0.01 - minimumDelay: Delay = 500 - strategy = ExponentialDelayStrategy.new( - meanDelay, rng(), negligibleProb = negligibleProb, minimumDelay = minimumDelay - ) - - check strategy.generateForIntermediate(meanDelay) == minimumDelay - - test "SpamProtectionDelayStrategy applies the default delay floor": - let - meanDelay: Delay = 100 - strategy = SpamProtectionDelayStrategy.new(meanDelay, rng()) - (minimumDelayHits, sawDelayAboveMinimum) = sampleLowerBoundStats( - strategy, meanDelay, DefaultSpamProtectionDelayFloor, BoundarySamples - ) - - check minimumDelayHits > 0 - check sawDelayAboveMinimum - check minimumDelayHits * 100 < BoundarySamples * MinBoundaryHitRatePct - - test "SpamProtectionDelayStrategy allows overriding the default delay floor": - let - meanDelay: Delay = 100 - minimumDelay: Delay = 250 - strategy = - SpamProtectionDelayStrategy.new(meanDelay, rng(), minimumDelay = minimumDelay) - (minimumDelayHits, sawDelayAboveMinimum) = - sampleLowerBoundStats(strategy, meanDelay, minimumDelay, BoundarySamples) - - check minimumDelayHits > 0 - check sawDelayAboveMinimum - check minimumDelayHits * 100 < BoundarySamples * MinBoundaryHitRatePct diff --git a/tests/libp2p/mix/test_fragmentation.nim b/tests/libp2p/mix/test_fragmentation.nim deleted file mode 100644 index 43989fac9b..0000000000 --- a/tests/libp2p/mix/test_fragmentation.nim +++ /dev/null @@ -1,114 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import results -import - ../../../libp2p/[peerid, protocols/mix/serialization, protocols/mix/fragmentation] -import ../../tools/[unittest] - -suite "Fragmentation": - let peerId = - PeerId.init("16Uiu2HAmFkwLVsVh6gGPmSm9R3X4scJ5thVdKfWYeJsKeVrbcgVC").get() - - test "serialize and deserialize message chunk": - let - message = newSeq[byte](DataSize) - chunks = padAndChunkMessage(message, peerId) - serialized = chunks[0].serialize() - deserialized = - MessageChunk.deserialize(serialized).expect("Deserialization error") - - check chunks[0] == deserialized - - test "pad and unpad small message": - let - message = cast[seq[byte]]("Hello, World!") - messageBytesLen = len(message) - paddedMsg = addPadding(message, peerId) - unpaddedMessage = removePadding(paddedMsg).expect("Unpad error") - - let (paddingLength, data, _) = paddedMsg.get() - - check: - paddingLength == uint16(DataSize - messageBytesLen) - data.len == DataSize - unpaddedMessage.len == messageBytesLen - - test "pad and chunk large message": - let - message = newSeq[byte](MessageSize * 2 + (MessageSize - 1)) - messageBytesLen = len(message) - chunks = padAndChunkMessage(message, peerId) - totalChunks = max(1, ceilDiv(messageBytesLen, DataSize)) - - check chunks.len == totalChunks - - for i in 0 ..< totalChunks: - let (paddingLength, data, _) = chunks[i].get() - if i != totalChunks - 1: - check paddingLength == 0 - else: - let chunkSize = messageBytesLen mod DataSize - check paddingLength == uint16(DataSize - chunkSize) - - check data.len == DataSize - - test "chunk sequence numbers are consecutive": - let - message = newSeq[byte](MessageSize * 3) - messageBytesLen = len(message) - chunks = padAndChunkMessage(message, peerId) - totalChunks = max(1, ceilDiv(messageBytesLen, DataSize)) - - check chunks.len == totalChunks - - let (_, _, firstSeqNo) = chunks[0].get() - - for i in 1 ..< totalChunks: - let (_, _, seqNo) = chunks[i].get() - check seqNo == firstSeqNo + uint32(i) - - test "chunk data reconstructs original message": - let - message = cast[seq[byte]]("This is a test message that will be split into multiple chunks.") - chunks = padAndChunkMessage(message, peerId) - - var reconstructed: seq[byte] - for chunk in chunks: - let (paddingLength, data, _) = chunk.get() - reconstructed.add(data[paddingLength.int ..^ 1]) - - check reconstructed == message - - test "empty message handling": - let - message = cast[seq[byte]]("") - chunks = padAndChunkMessage(message, peerId) - - check chunks.len == 1 - - let (paddingLength, _, _) = chunks[0].get() - - check paddingLength == uint16(DataSize) - - test "message size equal to chunk size": - let - message = newSeq[byte](DataSize) - chunks = padAndChunkMessage(message, peerId) - - check chunks.len == 1 - - let (paddingLength, _, _) = chunks[0].get() - - check paddingLength == 0 - - test "removePadding with invalid padding length returns error": - let chunk = MessageChunk.init( - paddingLength = uint16(DataSize + 1), data = newSeq[byte](DataSize), seqNo = 0'u32 - ) - let res = removePadding(chunk) - check: - res.isErr() - res.error == "Invalid padding length" diff --git a/tests/libp2p/mix/test_mix_message.nim b/tests/libp2p/mix/test_mix_message.nim deleted file mode 100644 index 5642ba1459..0000000000 --- a/tests/libp2p/mix/test_mix_message.nim +++ /dev/null @@ -1,95 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import results, stew/byteutils -import - ../../../libp2p/protocols/mix/ - [fragmentation, mix_message, mix_protocol, serialization] -import ../../tools/[unittest] - -# Define test cases -suite "mix_message_tests": - test "serialize_and_deserialize_mix_message": - let - message = "Hello World!" - codec = "/test/codec/1.0.0" - mixMsg = MixMessage.init(message.toBytes(), codec) - - let serialized = mixMsg.serialize() - let deserializedMsg = - MixMessage.deserialize(serialized).expect("deserialization failed") - - check: - message == string.fromBytes(deserializedMsg.message) - codec == deserializedMsg.codec - - test "serialize_empty_mix_message": - let - emptyMessage = "" - codec = "/test/codec/1.0.0" - mixMsg = MixMessage.init(emptyMessage.toBytes(), codec) - - let serialized = mixMsg.serialize() - let dMixMsg = MixMessage.deserialize(serialized).expect("deserialization failed") - - check: - emptyMessage == string.fromBytes(dMixMsg.message) - codec == dMixMsg.codec - - test "deserialize with empty data returns error": - let res = MixMessage.deserialize(@[]) - check: - res.isErr() - res.error == "deserialization failed: data is empty" - - test "deserialize with invalid codec length returns error": - # LEB128 continuation bit set — incomplete varint - let res = MixMessage.deserialize(@[0b10000000'u8, 0b00000000'u8]) - check: - res.isErr() - res.error == "deserialization failed: invalid codec length" - - test "deserialize with insufficient data returns error": - # Varint says codec is 5 bytes, but only 1 byte follows - let res = MixMessage.deserialize(@[0b00000101'u8, 0b00000000'u8]) - check: - res.isErr() - res.error == "deserialization failed: not enough data" - - test "getMaxMessageSizeForCodec returns correct size": - let codec = "/test/1.0.0" - - let size0 = getMaxMessageSizeForCodec(codec, 0) - check: - size0.get() > 0 - - # Adding 1 SURB should reduce available size by a fixed amount (SurbSize) - let size1 = getMaxMessageSizeForCodec(codec, 1) - check: - size1.get() < size0.get() - let surbOverhead = size0.get() - size1.get() - - # Adding 2 SURBs should reduce by exactly double the per-SURB overhead - let size2 = getMaxMessageSizeForCodec(codec, 2) - check: - size2.get() == size0.get() - 2 * surbOverhead - - # A longer codec should return a smaller max message size - let longCodec = "/test/with/a/much/longer/codec/name/1.0.0" - let sizeLong = getMaxMessageSizeForCodec(longCodec, 0) - check: - sizeLong.get() < size0.get() - sizeLong.get() == size0.get() - (longCodec.len - codec.len) - - test "getMaxMessageSizeForCodec errors when overhead exceeds capacity": - let codec = "/test/1.0.0" - - # Max SURBs that fit in payload: - # (total size - codec overhead - SURB count byte) / SURB size - let codecOverhead = MixMessage.init(@[], codec).serialize().len - let maxSurbs = uint8((DataSize - codecOverhead - SurbLenSize) div SurbSize) - check: - getMaxMessageSizeForCodec(codec, maxSurbs).isOk - getMaxMessageSizeForCodec(codec, maxSurbs + 1).isErr diff --git a/tests/libp2p/mix/test_multiaddr.nim b/tests/libp2p/mix/test_multiaddr.nim deleted file mode 100644 index 6aedc98465..0000000000 --- a/tests/libp2p/mix/test_multiaddr.nim +++ /dev/null @@ -1,61 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import results -import ../../../libp2p/protocols/mix/[serialization, multiaddr] -import ../../../libp2p/[peerid, multiaddress] -import ../../tools/[unittest] - -template maddr(ma: string): MultiAddress = - MultiAddress.init(ma).tryGet() - -proc maddrConversionShouldFail(ma: string, msg: string) = - test msg: - let peerId = PeerId.random().expect("could not generate peerId") - let ma = MultiAddress.init(ma).expect("could not initialize multiaddr") - check: - multiAddrToBytes(peerId, ma).isErr - -suite "Utils tests": - test "multiaddress conversion": - let multiAddrs = [ - "/ip4/0.0.0.0/tcp/4242", "/ip4/10.0.0.1/tcp/1234", - "/ip4/192.168.1.1/udp/8080/quic-v1", - "/ip4/10.0.0.1/tcp/1234/p2p/16Uiu2HAmDHw4mwBdEjxjJPhrt8Eq1kvDjXAuwkqCmhNiz363AFV2/p2p-circuit", - "/ip4/10.0.0.1/udp/1234/quic-v1/p2p/16Uiu2HAmDHw4mwBdEjxjJPhrt8Eq1kvDjXAuwkqCmhNiz363AFV2/p2p-circuit", - ] - - for multiAddr in multiAddrs: - let - ma = maddr(multiAddr) - peerId = PeerId.random().expect("could not generate peerId") - multiAddrBytes = multiAddrToBytes(peerId, ma).expect("conversion failed") - - check multiAddrBytes.len == AddrSize - - let (dPeerId, deserializedMa) = - bytesToMultiAddr(multiAddrBytes).expect("conversion failed") - - check: - deserializedMa == ma - dPeerId == peerId - - maddrConversionShouldFail("/ip4/0.0.0.0/tcp/4242/quic-v1/", "invalid protocol") - - maddrConversionShouldFail("/ip4/0.0.0.0", "invalid multiaddress format") - - maddrConversionShouldFail( - "/ip4/0.0.0.0/tcp/4242/p2p-circuit", "invalid multiaddress format circuit relay" - ) - - maddrConversionShouldFail( - "/ip4/0.0.0.0/tcp/4242/p2p/QmcycySVeRSftFQGM392xCqDh6UUbhSU9ykNpxrFBPX3gJ/p2p-circuit", - "invalid peerId in circuit relay addr", - ) - - test "invalid address length": - let invalidBytes = newSeq[byte](AddrSize - 1) - check: - bytesToMultiAddr(invalidBytes).isErr diff --git a/tests/libp2p/mix/test_pool.nim b/tests/libp2p/mix/test_pool.nim deleted file mode 100644 index b5ce044b36..0000000000 --- a/tests/libp2p/mix/test_pool.nim +++ /dev/null @@ -1,232 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import results, sequtils -import ../../../libp2p/[crypto/crypto, crypto/secp, multiaddress, peerid, peerstore] -import ../../../libp2p/protocols/mix/[mix_node, pool] -import ../../tools/unittest -import ./utils - -suite "MixNodePool Tests": - var - peerStore {.threadvar.}: PeerStore - pool {.threadvar.}: MixNodePool - mixNodes {.threadvar.}: seq[MixNodeInfo] - - setup: - peerStore = PeerStore.new() - pool = MixNodePool.new(peerStore) - mixNodes = MixNodeInfo.generateRandomMany(5) - - test "new creates empty pool": - check pool.len == 0 - - test "add stores mix node info": - let pubInfo = mixNodes[0].toMixPubInfo() - - pool.add(pubInfo) - - check: - pool.len == 1 - pool.get(pubInfo.peerId).isSome - pool.get(pubInfo.peerId).get() == pubInfo - - test "add stores all required data in peerStore": - let pubInfo = mixNodes[0].toMixPubInfo() - - pool.add(pubInfo) - - check: - peerStore[MixPubKeyBook][pubInfo.peerId] == pubInfo.mixPubKey - peerStore[AddressBook][pubInfo.peerId] == @[pubInfo.multiAddr] - peerStore[KeyBook][pubInfo.peerId].scheme == Secp256k1 - peerStore[KeyBook][pubInfo.peerId].skkey == pubInfo.libp2pPubKey - - test "bulk add stores multiple mix nodes": - let pubInfos = mixNodes.mapIt(it.toMixPubInfo()) - - pool.add(pubInfos) - - check pool.len == mixNodes.len - for pubInfo in pubInfos: - check pool.get(pubInfo.peerId).isSome - check pool.get(pubInfo.peerId).get() == pubInfo - - test "bulk add with empty seq is no-op": - pool.add(newSeq[MixPubInfo]()) - check pool.len == 0 - - test "remove deletes from pool": - let pubInfo = mixNodes[0].toMixPubInfo() - - pool.add(pubInfo) - check pool.len == 1 - - let removed = pool.remove(pubInfo.peerId) - - check: - removed == true - pool.len == 0 - pool.get(pubInfo.peerId).isNone - - test "remove returns false for non-existent peer": - let peerId = PeerId.random().expect("could not generate peerId") - check pool.remove(peerId) == false - - test "get returns none for non-existent peer": - let peerId = PeerId.random().expect("could not generate peerId") - check pool.get(peerId).isNone - - test "get returns none when address is missing": - let pubInfo = mixNodes[0].toMixPubInfo() - - # Manually add only the mix key, not the address - peerStore[MixPubKeyBook][pubInfo.peerId] = pubInfo.mixPubKey - - check pool.get(pubInfo.peerId).isNone - - test "get returns none when key scheme is not Secp256k1": - let pubInfo = mixNodes[0].toMixPubInfo() - - # Manually add with wrong key scheme - peerStore[MixPubKeyBook][pubInfo.peerId] = pubInfo.mixPubKey - peerStore[AddressBook][pubInfo.peerId] = @[pubInfo.multiAddr] - # KeyBook is not set, so scheme defaults to something other than Secp256k1 - - check pool.get(pubInfo.peerId).isNone - - test "get filters for supported addresses (IPv4 with TCP or QUIC-v1)": - let pubInfo = mixNodes[0].toMixPubInfo() - let relayPeerId = PeerId.random().expect("could not generate relay peerId") - let ipv6Addr = - MultiAddress.init("/ip6/::1/tcp/4242").expect("could not create multiaddr") - let udpAddr = - MultiAddress.init("/ip4/127.0.0.1/udp/4242").expect("could not create multiaddr") - let tcpAddr = - MultiAddress.init("/ip4/127.0.0.1/tcp/4243").expect("could not create multiaddr") - let quicAddr = MultiAddress.init("/ip4/127.0.0.1/udp/4244/quic-v1").expect( - "could not create multiaddr" - ) - # Circuit-relay addresses - let tcpCircuitAddr = MultiAddress - .init("/ip4/127.0.0.1/tcp/4245/p2p/" & $relayPeerId & "/p2p-circuit") - .expect("could not create multiaddr") - let quicCircuitAddr = MultiAddress - .init("/ip4/127.0.0.1/udp/4246/quic-v1/p2p/" & $relayPeerId & "/p2p-circuit") - .expect("could not create multiaddr") - let ipv6CircuitAddr = MultiAddress - .init("/ip6/::1/tcp/4247/p2p/" & $relayPeerId & "/p2p-circuit") - .expect("could not create multiaddr") - - peerStore[MixPubKeyBook][pubInfo.peerId] = pubInfo.mixPubKey - peerStore[KeyBook][pubInfo.peerId] = - PublicKey(scheme: Secp256k1, skkey: pubInfo.libp2pPubKey) - - # Only IPv6 - should return none - peerStore[AddressBook][pubInfo.peerId] = @[ipv6Addr] - check pool.get(pubInfo.peerId).isNone - - # Only UDP (without QUIC-v1) - should return none - peerStore[AddressBook][pubInfo.peerId] = @[udpAddr] - check pool.get(pubInfo.peerId).isNone - - # IPv6 and UDP first, then TCP - should return TCP - peerStore[AddressBook][pubInfo.peerId] = @[ipv6Addr, udpAddr, tcpAddr] - check pool.get(pubInfo.peerId).get().multiAddr == tcpAddr - - # UDP first, then QUIC-v1 - should return QUIC-v1 - peerStore[AddressBook][pubInfo.peerId] = @[udpAddr, quicAddr] - check pool.get(pubInfo.peerId).get().multiAddr == quicAddr - - # TCP and QUIC-v1 both available - should return first match (TCP) - peerStore[AddressBook][pubInfo.peerId] = @[tcpAddr, quicAddr] - check pool.get(pubInfo.peerId).get().multiAddr == tcpAddr - - # TCP circuit-relay - should be supported - peerStore[AddressBook][pubInfo.peerId] = @[tcpCircuitAddr] - check pool.get(pubInfo.peerId).get().multiAddr == tcpCircuitAddr - - # QUIC-v1 circuit-relay - should be supported - peerStore[AddressBook][pubInfo.peerId] = @[quicCircuitAddr] - check pool.get(pubInfo.peerId).get().multiAddr == quicCircuitAddr - - # IPv6 circuit-relay - should return none (unsupported transport) - peerStore[AddressBook][pubInfo.peerId] = @[ipv6CircuitAddr] - check pool.get(pubInfo.peerId).isNone - - # Mixed: unsupported circuit-relay first, then supported - should return supported - peerStore[AddressBook][pubInfo.peerId] = @[ipv6CircuitAddr, tcpCircuitAddr] - check pool.get(pubInfo.peerId).get().multiAddr == tcpCircuitAddr - - test "peerIds returns all peer IDs": - for i in 0 ..< mixNodes.len: - let pubInfo = mixNodes[i].toMixPubInfo() - pool.add(pubInfo) - - let peerIds = pool.peerIds() - - check peerIds.len == mixNodes.len - - for i in 0 ..< mixNodes.len: - let pubInfo = mixNodes[i].toMixPubInfo() - check pubInfo.peerId in peerIds - - test "len returns correct count": - check pool.len == 0 - - for i in 0 ..< 3: - let pubInfo = mixNodes[i].toMixPubInfo() - pool.add(pubInfo) - check pool.len == i + 1 - - test "get prefers LastSeenOutboundBook over AddressBook": - let pubInfo = mixNodes[0].toMixPubInfo() - let addressBookAddr = MultiAddress.init("/ip4/192.168.1.1/tcp/4242").expect( - "could not create multiaddr" - ) - let lastSeenAddr = - MultiAddress.init("/ip4/10.0.0.1/tcp/4243").expect("could not create multiaddr") - let lastSeenIpv6Addr = - MultiAddress.init("/ip6/::1/tcp/4244").expect("could not create multiaddr") - - peerStore[MixPubKeyBook][pubInfo.peerId] = pubInfo.mixPubKey - peerStore[KeyBook][pubInfo.peerId] = - PublicKey(scheme: Secp256k1, skkey: pubInfo.libp2pPubKey) - peerStore[AddressBook][pubInfo.peerId] = @[addressBookAddr] - - # When LastSeenOutboundBook has a supported address, prefer it - peerStore[LastSeenOutboundBook][pubInfo.peerId] = Opt.some(lastSeenAddr) - check pool.get(pubInfo.peerId).get().multiAddr == lastSeenAddr - - # When LastSeenOutboundBook has unsupported address (IPv6), fall back to AddressBook - peerStore[LastSeenOutboundBook][pubInfo.peerId] = Opt.some(lastSeenIpv6Addr) - check pool.get(pubInfo.peerId).get().multiAddr == addressBookAddr - - # When LastSeenOutboundBook is empty, fall back to AddressBook - peerStore[LastSeenOutboundBook][pubInfo.peerId] = Opt.none(MultiAddress) - check pool.get(pubInfo.peerId).get().multiAddr == addressBookAddr - - test "multiple operations sequence": - # Add 3 nodes - for i in 0 ..< 3: - let pubInfo = mixNodes[i].toMixPubInfo() - pool.add(pubInfo) - - check pool.len == 3 - - # Remove middle node - let middlePubInfo = mixNodes[1].toMixPubInfo() - discard pool.remove(middlePubInfo.peerId) - - check: - pool.len == 2 - pool.get(middlePubInfo.peerId).isNone - - # Add two more nodes - for i in 3 ..< 5: - let pubInfo = mixNodes[i].toMixPubInfo() - pool.add(pubInfo) - - check pool.len == 4 diff --git a/tests/libp2p/mix/test_seq_no_generator.nim b/tests/libp2p/mix/test_seq_no_generator.nim deleted file mode 100644 index 0e19262410..0000000000 --- a/tests/libp2p/mix/test_seq_no_generator.nim +++ /dev/null @@ -1,96 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import sets, std/[os] -import ../../../libp2p/[peerid, protocols/mix/seqno_generator] -import ../../tools/[unittest] - -const second = 1000 - -suite "Sequence Number Generator": - test "init_seq_no_from_peer_id": - let - peerId = - PeerId.init("16Uiu2HAmFkwLVsVh6gGPmSm9R3X4scJ5thVdKfWYeJsKeVrbcgVC").get() - seqNo = SeqNo.init(peerId) - - check seqNo != 0 - - test "generate_seq_nos_for_different_messages": - let - peerId = - PeerId.init("16Uiu2HAmFkwLVsVh6gGPmSm9R3X4scJ5thVdKfWYeJsKeVrbcgVC").get() - msg1 = @[byte 1, 2, 3] - msg2 = @[byte 4, 5, 6] - - var seqNo = SeqNo.init(peerId) - - seqNo.generate(msg1) - let seqNo1 = seqNo - - seqNo.generate(msg2) - let seqNo2 = seqNo - - check seqNo1 != seqNo2 - - test "generate_seq_nos_for_same_message": - let - peerId = - PeerId.init("16Uiu2HAmFkwLVsVh6gGPmSm9R3X4scJ5thVdKfWYeJsKeVrbcgVC").get() - msg = @[byte 1, 2, 3] - var seqNo = SeqNo.init(peerId) - - seqNo.generate(msg) - let seqNo1 = seqNo - - sleep(second) - - seqNo.generate(msg) - let seqNo2 = seqNo - - check seqNo1 != seqNo2 - - test "generate_seq_nos_for_different_peer_ids": - let - peerId1 = - PeerId.init("16Uiu2HAmFkwLVsVh6gGPmSm9R3X4scJ5thVdKfWYeJsKeVrbcgVC").get() - peerId2 = - PeerId.init("16Uiu2HAm6WNzw8AssyPscYYi8x1bY5wXyQrGTShRH75bh5dPCjBQ").get() - - var - seqNo1 = SeqNo.init(peerId1) - seqNo2 = SeqNo.init(peerId2) - - check seqNo1 != seqNo2 - - test "increment_seq_no": - let peerId = - PeerId.init("16Uiu2HAmFkwLVsVh6gGPmSm9R3X4scJ5thVdKfWYeJsKeVrbcgVC").get() - var seqNo: SeqNo = SeqNo.init(peerId) - let initialCounter = seqNo - - seqNo.inc() - - check seqNo == initialCounter + 1 - - test "seq_no_wraps_around_at_max_value": - var seqNo = high(uint32) - 1 - - seqNo.inc() - - check seqNo == 0 - - test "generate_seq_no_uses_entire_uint32_range": - let peerId = - PeerId.init("16Uiu2HAmFkwLVsVh6gGPmSm9R3X4scJ5thVdKfWYeJsKeVrbcgVC").get() - var - seqNo = SeqNo.init(peerId) - seenValues = initHashSet[uint32]() - - for i in 0 ..< 10000: - seqNo.generate(@[i.uint8]) - seenValues.incl(seqNo) - - check seenValues.len > 9000 diff --git a/tests/libp2p/mix/test_serialization.nim b/tests/libp2p/mix/test_serialization.nim deleted file mode 100644 index b6ac6fa81d..0000000000 --- a/tests/libp2p/mix/test_serialization.nim +++ /dev/null @@ -1,135 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import results, std/sequtils -import ../../../libp2p/protocols/mix/[serialization, delay] -import ../../tools/[unittest] - -proc makeSurb(seed: byte): SURB = - SURB( - hop: Hop.init(newSeq[byte](AddrSize)), - header: Header.init( - newSeqWith(AlphaSize, seed), - newSeqWith(BetaSize, seed), - newSeqWith(GammaSize, seed), - ), - key: newSeqWith(k, seed), - ) - -suite "serialization_tests": - test "serialize_and_deserialize_header": - let header = Header.init( - newSeq[byte](AlphaSize), newSeq[byte](BetaSize), newSeq[byte](GammaSize) - ) - let serialized = header.serialize() - - check serialized.len() == HeaderSize - - test "serialize_and_deserialize_message": - let message = Message(newSeq[byte](MessageSize)) - let serialized = message.serialize() - let deserialized = - Message.deserialize(serialized).expect("Failed to deserialize message") - - check message == deserialized - - test "serialize_and_deserialize_hop": - let hop = Hop.init(newSeq[byte](AddrSize)) - let serialized = hop.serialize() - let deserialized = Hop.deserialize(serialized).expect("Failed to deserialize hop") - - check hop.get() == deserialized.get() - - test "serialize_and_deserialize_routing_info": - let routingInfo = RoutingInfo.init( - Hop.init(newSeq[byte](AddrSize)), - NoDelay, - newSeq[byte](GammaSize), - newSeq[byte](((r * (t + 1)) - t) * k), - ) - let serialized = routingInfo.serialize() - let suffixLength = (t + 1) * k - let suffix = newSeq[byte](suffixLength) - let deserialized = RoutingInfo.deserialize(serialized & suffix).expect( - "Failed to deserialize routing info" - ) - let - (hop, delay, gamma, beta) = getRoutingInfo(routingInfo) - (dHop, dDelay, dGamma, dBeta) = getRoutingInfo(deserialized) - - check: - hop.get() == dHop.get() - delay == dDelay - gamma == dGamma - beta == dBeta[0 .. (((r * (t + 1)) - t) * k) - 1] - - test "serialize_and_deserialize_sphinx_packet": - let - header = Header.init( - newSeq[byte](AlphaSize), newSeq[byte](BetaSize), newSeq[byte](GammaSize) - ) - payload = newSeq[byte](PayloadSize) - packet = SphinxPacket.init(header, payload) - - let serialized = packet.serialize() - - let deserializedSP = - SphinxPacket.deserialize(serialized).expect("Failed to deserialize sphinx packet") - - check: - header.Alpha == deserializedSP.Hdr.Alpha - header.Beta == deserializedSP.Hdr.Beta - header.Gamma == deserializedSP.Hdr.Gamma - payload == deserializedSP.Payload - - test "serializeMessageWithSURBs and extractSURBs round-trip": - let - msg = @[1'u8, 2, 3, 4, 5] - surbs = @[makeSurb(0xAA), makeSurb(0xBB)] - serialized = serializeMessageWithSURBs(msg, surbs).expect("serialize error") - (extractedSurbs, extractedMsg) = extractSURBs(serialized).expect("extract error") - - check: - extractedSurbs.len == 2 - extractedMsg == msg - extractedSurbs[0].header.Alpha == surbs[0].header.Alpha - extractedSurbs[1].header.Alpha == surbs[1].header.Alpha - extractedSurbs[0].key == surbs[0].key - extractedSurbs[1].key == surbs[1].key - - test "serializeMessageWithSURBs rejects too many SURBs": - let maxSurbs = (MessageSize - SurbLenSize - 1) div SurbSize - let tooMany = newSeqWith(maxSurbs + 1, makeSurb(0x01)) - let res = serializeMessageWithSURBs(@[], tooMany) - check: - res.isErr() - res.error == "too many SURBs" - - test "extractSURBs rejects too many declared SURBs": - let maxSurbs = (MessageSize - SurbLenSize - 1) div SurbSize - let data = @[byte(maxSurbs + 1)] & newSeq[byte](100) - let res = extractSURBs(data) - check: - res.isErr() - res.error == "too many SURBs" - - test "Header deserialize rejects undersized input": - let tooShort = newSeq[byte](HeaderSize - 1) - check: - Header.deserialize(tooShort).isErr() - - test "SphinxPacket deserialize rejects wrong-size input": - let tooShort = newSeq[byte](PacketSize - 1) - let tooLong = newSeq[byte](PacketSize + 1) - check: - SphinxPacket.deserialize(tooShort).isErr() - SphinxPacket.deserialize(tooLong).isErr() - - test "Message deserialize rejects wrong-size input": - let tooShort = newSeq[byte](PayloadSize - 1) - let tooLong = newSeq[byte](PayloadSize + 1) - check: - Message.deserialize(tooShort).isErr() - Message.deserialize(tooLong).isErr() diff --git a/tests/libp2p/mix/test_spam_protection_interface.nim b/tests/libp2p/mix/test_spam_protection_interface.nim deleted file mode 100644 index 2ae8e1451c..0000000000 --- a/tests/libp2p/mix/test_spam_protection_interface.nim +++ /dev/null @@ -1,156 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import unittest2 -import results -import ../../../libp2p/protocols/mix/spam_protection -import ./spam_protection_impl - -const testPacketData = @[1.byte, 2, 3, 4, 5] - -suite "Spam Protection - Per Hop Proof Generation": - test "Proof generation and verification cycle": - let spamProtection = newPoWSpamProtection(2) - - # Simulate node generating proof for packet - let packetData = testPacketData - let proofResult = spamProtection.generateProof(packetData).get() - check proofResult.proof.len == 8 - - # Simulate next node verifying proof with same packet - let verifyResult = spamProtection.verifyProof(proofResult.proof, packetData) - - check verifyResult.isOk() - check verifyResult.get() == true - check spamProtection.verificationCount == 1 - - test "Proof verification fails with wrong binding data": - let spamProtection = newPoWSpamProtection(2) - - let originalPacket = testPacketData - let proofResult = spamProtection.generateProof(originalPacket).get() - - # Try to verify with different packet - let differentPacket = @[1.byte, 2, 3, 4, 6] - let verifyResult = spamProtection.verifyProof(proofResult.proof, differentPacket) - - check verifyResult.isOk() - check verifyResult.get() == false - - test "Proof verification rejects malformed proofs": - let spamProtection = newPoWSpamProtection(2) - - let packetData = testPacketData - - # Malformed proof (wrong size) - let malformedProof = @[1.byte, 2, 3] - let verifyResult = spamProtection.verifyProof(malformedProof, packetData) - - check verifyResult.isOk() - check verifyResult.get() == false - - test "Multiple proofs can be generated for different packets": - let spamProtection = newPoWSpamProtection(2) - - let packet1 = testPacketData - let packet2 = @[4.byte, 5, 6] - - let pr1 = spamProtection.generateProof(packet1).get() - let pr2 = spamProtection.generateProof(packet2).get() - - # Each proof should verify with its corresponding packet - check spamProtection.verifyProof(pr1.proof, packet1).get() == true - check spamProtection.verifyProof(pr2.proof, packet2).get() == true - - # But not with the other packet - check spamProtection.verifyProof(pr1.proof, packet2).get() == false - check spamProtection.verifyProof(pr2.proof, packet1).get() == false - - test "Rate limiting blocks packets exceeding limit": - let spamProtection = newRateLimitSpamProtection(3) - - let packetData = testPacketData - - # First 3 packets should be accepted - for i in 0 ..< 3: - let pr = spamProtection.generateProof(packetData).get() - let valid = spamProtection.verifyProof(pr.proof, packetData).get() - check valid == true - - # 4th packet should be rejected - let pr4 = spamProtection.generateProof(packetData).get() - let valid4 = spamProtection.verifyProof(pr4.proof, packetData).get() - check valid4 == false - - test "Per-hop proofs are independently generated": - let spamProtection = newRateLimitSpamProtection(10) - - let packet1 = testPacketData - let packet2 = @[4.byte, 5, 6] - - # Generate fresh proofs for each packet - let pr1 = spamProtection.generateProof(packet1).get() - let pr2 = spamProtection.generateProof(packet2).get() - - # Both should verify successfully (rate limit not exceeded) - check spamProtection.verifyProof(pr1.proof, packet1).get() == true - check spamProtection.verifyProof(pr2.proof, packet2).get() == true - -suite "Spam Protection - Packet Integration": - test "Proof can be appended and extracted from packet": - let sphinxPacket = @[1.byte, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - let proof = @[77.byte, 88, 99, 11, 22, 33, 44, 55] - - let spamProtection = newPoWSpamProtection(2) - - # Append proof to packet - var packetWithProof = appendProofToPacket(sphinxPacket, proof).get() - - # Extract proof from packet (mutates packetWithProof) - let (extractedSphinx, extractedProof) = - extractProofFromPacket(packetWithProof, spamProtection).get() - check extractedProof == proof - check extractedSphinx == sphinxPacket - - test "Packet structure maintained after proof append/extract": - let sphinxPacket = @[1.byte, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] - let proof = @[100.byte, 101, 102, 103, 104, 105, 106, 107] - - let packetWithProof = appendProofToPacket(sphinxPacket, proof).get() - - # Check packet structure: [sphinx][proof] - check packetWithProof.len == sphinxPacket.len + 8 - check packetWithProof[0 ..< sphinxPacket.len] == sphinxPacket - check packetWithProof[^8 ..^ 1] == proof - -suite "Spam Protection - Edge Cases": - test "extractProofFromPacket fails when packet too small": - let spamProtection = newPoWSpamProtection(2) - - # Create a packet smaller than proof size (8 bytes) - var tinyPacket = @[1.byte, 2, 3] # Only 3 bytes, but proof needs 8 - - let res = extractProofFromPacket(tinyPacket, spamProtection) - check res.isErr() - check res.error() == "Packet too small to contain proof" - - test "appendProofToPacket with empty proof returns original packet": - let sphinxPacket = testPacketData - let emptyProof = newSeq[byte](0) - - check appendProofToPacket(sphinxPacket, emptyProof).get() == sphinxPacket - - test "extractProofFromPacket with zero proof size returns packet unchanged": - # Create a spam protection with zero proof size - let spamProtection = newRateLimitSpamProtection(10) - spamProtection.proofSize = 0 - - var packet: seq[byte] = testPacketData - let originalPacket = packet - - let (extractedPacket, extractedProof) = - extractProofFromPacket(packet, spamProtection).get() - check extractedPacket == originalPacket - check extractedProof.len == 0 diff --git a/tests/libp2p/mix/test_sphinx.nim b/tests/libp2p/mix/test_sphinx.nim deleted file mode 100644 index 72ee1359b2..0000000000 --- a/tests/libp2p/mix/test_sphinx.nim +++ /dev/null @@ -1,471 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import random, results, chronicles, bearssl/rand -import ../../../libp2p/crypto/crypto -import - ../../../libp2p/protocols/mix/[curve25519, serialization, sphinx, tag_manager, delay] -import ../../tools/[unittest, crypto] - -# Helper function to pad/truncate message -proc addPadding(message: openArray[byte], size: int): seq[byte] = - if message.len >= size: - return message[0 .. size - 1] # Truncate if larger - else: - result = @message - let paddingLength = size - message.len - result.add(newSeq[byte](paddingLength)) # Pad with zeros - -# Helper function to create dummy data -proc createDummyData(): ( - Message, seq[FieldElement], seq[FieldElement], seq[Delay], seq[Hop], Hop -) = - let (privateKey1, publicKey1) = generateKeyPair().expect("generate keypair error") - let (privateKey2, publicKey2) = generateKeyPair().expect("generate keypair error") - let (privateKey3, publicKey3) = generateKeyPair().expect("generate keypair error") - - let - privateKeys = @[privateKey1, privateKey2, privateKey3] - publicKeys = @[publicKey1, publicKey2, publicKey3] - - delay: seq[Delay] = @[NoDelay, NoDelay, NoDelay] - - hops = @[ - Hop.init(newSeq[byte](AddrSize)), - Hop.init(newSeq[byte](AddrSize)), - Hop.init(newSeq[byte](AddrSize)), - ] - - message = newSeq[byte](MessageSize) - dest = Hop.init(newSeq[byte](AddrSize)) - return (message, privateKeys, publicKeys, delay, hops, dest) - -template randomI(): SURBIdentifier = - rng[].generate(SURBIdentifier) - -# Unit tests for sphinx.nim -suite "Sphinx Tests": - var tm: TagManager - - setup: - tm = TagManager.new() - - teardown: - clearTags(tm) - - test "sphinx wrap and process": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "sphinx wrap error" - ) - let packetBytes = sp.serialize() - - check packetBytes.len == PacketSize - - let packet = SphinxPacket.deserialize(packetBytes).expect("Sphinx wrap error") - - let processedSP1 = - processSphinxPacket(packet, privateKeys[0], tm).expect("Sphinx processing error") - - check: - processedSP1.status == Intermediate - processedSP1.serializedSphinxPacket.len == PacketSize - - let processedPacket1 = SphinxPacket - .deserialize(processedSP1.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP2 = processSphinxPacket(processedPacket1, privateKeys[1], tm).expect( - "Sphinx processing error" - ) - - check: - processedSP2.status == Intermediate - processedSP2.serializedSphinxPacket.len == PacketSize - - let processedPacket2 = SphinxPacket - .deserialize(processedSP2.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP3 = processSphinxPacket(processedPacket2, privateKeys[2], tm).expect( - "Sphinx processing error" - ) - - check: - processedSP3.status == Exit - processedSP3.messageChunk == message - - test "sphinx wrap empty public keys": - let (message, _, _, delay, _, dest) = createDummyData() - check wrapInSphinxPacket(message, @[], delay, @[], dest).isErr - - test "sphinx_process_invalid_mac": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "Sphinx wrap error" - ) - let packetBytes = sp.serialize() - - check packetBytes.len == PacketSize - - # Corrupt the MAC for testing - var tamperedPacketBytes = packetBytes - tamperedPacketBytes[0] = packetBytes[0] xor 0x01 - - let tamperedPacket = - SphinxPacket.deserialize(tamperedPacketBytes).expect("Sphinx wrap error") - let invalidMacPkt = processSphinxPacket(tamperedPacket, privateKeys[0], tm).expect( - "Sphinx processing error" - ) - - check invalidMacPkt.status == InvalidMAC - - test "tampered Beta invalidates MAC": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "sphinx wrap error" - ) - var packetBytes = sp.serialize() - - # Tamper a byte in Beta - packetBytes[AlphaSize] = packetBytes[AlphaSize] xor 0x01 - - let tamperedPacket = - SphinxPacket.deserialize(packetBytes).expect("deserialize error") - let processed = - processSphinxPacket(tamperedPacket, privateKeys[0], tm).expect("processing error") - - check processed.status == InvalidMAC - - test "tampered Gamma invalidates MAC": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "sphinx wrap error" - ) - var packetBytes = sp.serialize() - - # Tamper a byte in Gamma - let gammaOffset = AlphaSize + BetaSize - packetBytes[gammaOffset] = packetBytes[gammaOffset] xor 0x01 - - let tamperedPacket = - SphinxPacket.deserialize(packetBytes).expect("deserialize error") - let processed = - processSphinxPacket(tamperedPacket, privateKeys[0], tm).expect("processing error") - - check processed.status == InvalidMAC - - test "tampered Delta passes MAC but corrupts message": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "sphinx wrap error" - ) - var packetBytes = sp.serialize() - - # Tamper a byte in Delta - packetBytes[HeaderSize] = packetBytes[HeaderSize] xor 0x01 - - let tamperedPacket = - SphinxPacket.deserialize(packetBytes).expect("deserialize error") - - # MAC passes at intermediate hops - proving MAC only covers Beta, not Delta - let processedSP1 = - processSphinxPacket(tamperedPacket, privateKeys[0], tm).expect("processing error") - check processedSP1.status == Intermediate - - let packet2 = SphinxPacket.deserialize(processedSP1.serializedSphinxPacket).expect( - "deserialize error" - ) - let processedSP2 = - processSphinxPacket(packet2, privateKeys[1], tm).expect("processing error") - check processedSP2.status == Intermediate - - # At exit, the delta integrity check catches the corruption (not MAC) - let packet3 = SphinxPacket.deserialize(processedSP2.serializedSphinxPacket).expect( - "deserialize error" - ) - let exitResult = processSphinxPacket(packet3, privateKeys[2], tm) - check: - exitResult.isErr() - exitResult.error() == "delta_prime should be all zeros" - - test "sphinx process duplicate tag": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "Sphinx wrap error" - ) - let packetBytes = sp.serialize() - - check packetBytes.len == PacketSize - - let packet = SphinxPacket.deserialize(packetBytes).expect("Sphinx wrap error") - - # Process the packet twice to test duplicate tag handling - let processedSP1 = - processSphinxPacket(packet, privateKeys[0], tm).expect("Sphinx processing error") - - check processedSP1.status == Intermediate - - let processedSP2 = - processSphinxPacket(packet, privateKeys[0], tm).expect("Sphinx processing error") - - check processedSP2.status == Duplicate - - test "sphinx wrap and process message sizes": - let MessageSizes = @[32, 64, 128, 256, 512] - for size in MessageSizes: - let (_, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - var message = newSeq[byte](size) - randomize() - for i in 0 ..< size: - message[i] = byte(rand(256)) - let paddedMessage = addPadding(message, MessageSize) - - let sp = wrapInSphinxPacket(paddedMessage, publicKeys, delay, hops, dest).expect( - "Sphinx wrap error" - ) - let packetBytes = sp.serialize() - - check packetBytes.len == PacketSize - - let packet = SphinxPacket.deserialize(packetBytes).expect("Sphinx wrap error") - - let processedSP1 = processSphinxPacket(packet, privateKeys[0], tm).expect( - "Sphinx processing error" - ) - - check: - processedSP1.status == Intermediate - processedSP1.serializedSphinxPacket.len == PacketSize - - let processedPacket1 = SphinxPacket - .deserialize(processedSP1.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP2 = processSphinxPacket(processedPacket1, privateKeys[1], tm) - .expect("Sphinx processing error") - - check: - processedSP2.status == Intermediate - processedSP2.serializedSphinxPacket.len == PacketSize - - let processedPacket2 = SphinxPacket - .deserialize(processedSP2.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP3 = processSphinxPacket(processedPacket2, privateKeys[2], tm) - .expect("Error in Sphinx processing") - - check: - processedSP3.status == Exit - processedSP3.messageChunk == paddedMessage - - test "create and use surb": - let (message, privateKeys, publicKeys, delay, hops, _) = createDummyData() - - let surb = - createSURB(publicKeys, delay, hops, randomI(), rng()).expect("Create SURB error") - let packetBytes = useSURB(surb, message).serialize() - - check packetBytes.len == PacketSize - - let packet = SphinxPacket.deserialize(packetBytes).expect("Sphinx wrap error") - let processedSP1 = - processSphinxPacket(packet, privateKeys[0], tm).expect("Sphinx processing error") - - check: - processedSP1.status == Intermediate - processedSP1.serializedSphinxPacket.len == PacketSize - - let processedPacket1 = SphinxPacket - .deserialize(processedSP1.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP2 = processSphinxPacket(processedPacket1, privateKeys[1], tm).expect( - "Sphinx processing error" - ) - - check: - processedSP2.status == Intermediate - processedSP2.serializedSphinxPacket.len == PacketSize - - let processedPacket2 = SphinxPacket - .deserialize(processedSP2.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP3 = processSphinxPacket(processedPacket2, privateKeys[2], tm).expect( - "Sphinx processing error" - ) - - check processedSP3.status == Reply - - let msg = processReply(surb.key, surb.secret.get(), processedSP3.delta_prime).expect( - "Reply processing failed" - ) - - check msg == message - - test "create surb empty public keys": - let (_, _, _, delay, _, _) = createDummyData() - check createSURB(@[], delay, @[], randomI(), rng()).isErr() - - test "create surb with zero id returns error": - let (_, _, publicKeys, delay, hops, _) = createDummyData() - let zeroId = default(SURBIdentifier) - let res = createSURB(publicKeys, delay, hops, zeroId, rng()) - check: - res.isErr() - res.error == "id should be initialized" - - test "create surb with nil rng returns error": - let (_, _, publicKeys, delay, hops, _) = createDummyData() - let res = createSURB(publicKeys, delay, hops, randomI(), nil) - check: - res.isErr() - res.error == "rng must not be nil" - - test "surb sphinx process invalid mac": - let (message, privateKeys, publicKeys, delay, hops, _) = createDummyData() - - let surb = - createSURB(publicKeys, delay, hops, randomI(), rng()).expect("Create SURB error") - - let packetBytes = useSURB(surb, message).serialize() - - check packetBytes.len == PacketSize - - # Corrupt the MAC for testing - var tamperedPacketBytes = packetBytes - tamperedPacketBytes[0] = packetBytes[0] xor 0x01 - - let tamperedPacket = - SphinxPacket.deserialize(tamperedPacketBytes).expect("Sphinx wrap error") - - let processedSP1 = processSphinxPacket(tamperedPacket, privateKeys[0], tm).expect( - "Sphinx processing error" - ) - - check processedSP1.status == InvalidMAC - - test "surb sphinx process duplicate tag": - let (message, privateKeys, publicKeys, delay, hops, _) = createDummyData() - - let surb = - createSURB(publicKeys, delay, hops, randomI(), rng()).expect("Create SURB error") - - let packetBytes = useSURB(surb, message).serialize() - - check packetBytes.len == PacketSize - - let packet = SphinxPacket.deserialize(packetBytes).expect("Sphinx wrap error") - - # Process the packet twice to test duplicate tag handling - let processedSP1 = - processSphinxPacket(packet, privateKeys[0], tm).expect("Sphinx processing error") - - check processedSP1.status == Intermediate - - let processedSP2 = - processSphinxPacket(packet, privateKeys[0], tm).expect("Sphinx processing error") - - check processedSP2.status == Duplicate - - test "create and use surb message sizes": - let messageSizes = @[32, 64, 128, 256, 512] - for size in messageSizes: - let (_, privateKeys, publicKeys, delay, hops, _) = createDummyData() - var message = newSeq[byte](size) - randomize() - for i in 0 ..< size: - message[i] = byte(rand(256)) - let paddedMessage = addPadding(message, MessageSize) - - let surb = createSURB(publicKeys, delay, hops, randomI(), rng()).expect( - "Create SURB error" - ) - - let packetBytes = useSURB(surb, Message(paddedMessage)).serialize() - - check packetBytes.len == PacketSize - - let packet = SphinxPacket.deserialize(packetBytes).expect("Sphinx wrap error") - - let processedSP1 = processSphinxPacket(packet, privateKeys[0], tm).expect( - "Sphinx processing error" - ) - - check: - processedSP1.status == Intermediate - processedSP1.serializedSphinxPacket.len == PacketSize - - let processedPacket1 = SphinxPacket - .deserialize(processedSP1.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP2 = processSphinxPacket(processedPacket1, privateKeys[1], tm) - .expect("Sphinx processing error") - - check: - processedSP2.status == Intermediate - processedSP2.serializedSphinxPacket.len == PacketSize - - let processedPacket2 = SphinxPacket - .deserialize(processedSP2.serializedSphinxPacket) - .expect("Sphinx wrap error") - - let processedSP3 = processSphinxPacket(processedPacket2, privateKeys[2], tm) - .expect("Sphinx processing error") - - check processedSP3.status == Reply - - let msg = processReply(surb.key, surb.secret.get(), processedSP3.delta_prime) - .expect("Reply processing failed") - - check paddedMessage == msg - - test "checkReplay returns false for new packet, true for replay": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "sphinx wrap error" - ) - let packet = SphinxPacket.deserialize(sp.serialize()).expect("deserialize error") - - # First check - not a replay - let first = checkReplay(packet, privateKeys[0], tm).expect("checkReplay error") - check not first.isReplay - - # Second check - replay detected - let second = checkReplay(packet, privateKeys[0], tm).expect("checkReplay error") - check second.isReplay - - # Shared secret should be the same both times - check first.sharedSecret == second.sharedSecret - - test "processSphinxPacket with reused sharedSecret": - let (message, privateKeys, publicKeys, delay, hops, dest) = createDummyData() - let sp = wrapInSphinxPacket(message, publicKeys, delay, hops, dest).expect( - "sphinx wrap error" - ) - let packet = SphinxPacket.deserialize(sp.serialize()).expect("deserialize error") - - # Normal path - let normal = - processSphinxPacket(packet, privateKeys[0], tm).expect("normal processing error") - - # Reused sharedSecret - var tm2 = TagManager.new(autoStart = false) - let replay = checkReplay(packet, privateKeys[0], tm2).expect("checkReplay error") - check not replay.isReplay - - let reused = processSphinxPacket( - packet, privateKeys[0], tm2, Opt.some(replay.sharedSecret) - ) - .expect("reused processing error") - - check: - normal.status == reused.status - normal.serializedSphinxPacket == reused.serializedSphinxPacket diff --git a/tests/libp2p/mix/test_tag_manager.nim b/tests/libp2p/mix/test_tag_manager.nim deleted file mode 100644 index f2c2622c82..0000000000 --- a/tests/libp2p/mix/test_tag_manager.nim +++ /dev/null @@ -1,110 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos -import ../../../libp2p/protocols/mix/tag_manager -import ../../tools/unittest - -proc makeTag(seed: byte): Tag = - ## Helper to create a tag with a specific seed pattern - for i in 0 ..< result.len: - result[i] = byte((int(seed) * 32 + i) mod 256) - -suite "Tag Manager": - var tm: TagManager - - setup: - # autoStart=false to avoid async loop in synchronous tests - tm = TagManager.new(autoStart = false) - - teardown: - tm.clearTags() - - test "add, check, and remove tags": - let tag1 = makeTag(1) - let tag2 = makeTag(2) - - # Initially empty - check tm.len == 0 - check not tm.isTagSeen(tag1) - check not tm.isTagSeen(tag2) - - # Add tags - tm.addTag(tag1) - tm.addTag(tag2) - check tm.len == 2 - check tm.isTagSeen(tag1) - check tm.isTagSeen(tag2) - - # Remove one tag - tm.removeTag(tag1) - check tm.len == 1 - check not tm.isTagSeen(tag1) - check tm.isTagSeen(tag2) - - # Clear all - tm.clearTags() - check tm.len == 0 - - test "duplicate tag is no-op": - let tag = makeTag(1) - - tm.addTag(tag) - check tm.len == 1 - - # Adding same tag again should not change count - tm.addTag(tag) - check tm.len == 1 - - test "checkAndAddTag returns false for new tag, true for duplicate": - let tag = makeTag(1) - - # First call - tag is new, should return false - check: - not tm.checkAndAddTag(tag) - tm.len == 1 - - # Second call - tag already exists, should return true - check: - tm.checkAndAddTag(tag) - tm.len == 1 - - test "tag expiration and purge": - let shortTTL = chronos.milliseconds(30) - let tmShort = TagManager.new(tagTTL = shortTTL, autoStart = false) - - let baseTime = Moment.now() - - # Add 3 tags at baseTime - for i in 0 ..< 3: - tmShort.addTag(makeTag(byte(i)), baseTime) - check tmShort.len == 3 - - # Add 2 more tags later (before first ones expire) - let laterTime = baseTime + chronos.milliseconds(20) - for i in 3 ..< 5: - tmShort.addTag(makeTag(byte(i)), laterTime) - check tmShort.len == 5 - - # Purge when first 3 are expired but last 2 are not - let purgeTime = baseTime + chronos.milliseconds(40) - let purged = tmShort.purgeExpiredTags(purgeTime) - - check purged == 3 - check tmShort.len == 2 - check not tmShort.isTagSeen(makeTag(0)) - check not tmShort.isTagSeen(makeTag(1)) - check not tmShort.isTagSeen(makeTag(2)) - check tmShort.isTagSeen(makeTag(3)) - check tmShort.isTagSeen(makeTag(4)) - - test "purge with no expired tags": - tm.addTag(makeTag(1)) - - # Purge immediately (nothing expired with 1-hour default TTL) - let purged = tm.purgeExpiredTags() - - check purged == 0 - check tm.len == 1 diff --git a/tests/libp2p/mix/utils.nim b/tests/libp2p/mix/utils.nim deleted file mode 100644 index f11258c6b4..0000000000 --- a/tests/libp2p/mix/utils.nim +++ /dev/null @@ -1,197 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 OR MIT -# Copyright (c) Status Research & Development GmbH - -{.used.} - -import chronos, results, strformat -import - ../../../libp2p/[ - protocols/mix, - protocols/mix/mix_protocol, - protocols/mix/curve25519, - protocols/mix/delay_strategy, - protocols/ping, - peerid, - multiaddress, - switch, - builders, - crypto/crypto, - crypto/secp, - ] - -import ../../tools/[unittest, crypto] -import ./[mock_mix, spam_protection_impl] - -proc createSwitch*( - multiAddr: MultiAddress = MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet(), - libp2pPrivKey: Opt[SkPrivateKey] = Opt.none(SkPrivateKey), -): Switch = - let privKey = PrivateKey( - scheme: Secp256k1, skkey: libp2pPrivKey.valueOr(SkKeyPair.random(rng[]).seckey) - ) - return - newStandardSwitchBuilder(privKey = Opt.some(privKey), addrs = multiAddr).build() - -proc setupMixNode[T: MixProtocol]( - mixNodeInfo: MixNodeInfo, - switch: Switch, - destReadBehavior: Opt[tuple[codec: string, callback: DestReadBehavior]], - spamProtectionRateLimit: Opt[int], - delayStrategy: Opt[DelayStrategy] = Opt.none(DelayStrategy), -): T = - let spamProtection = - if spamProtectionRateLimit.isSome(): - Opt.some( - SpamProtection(newRateLimitSpamProtection(spamProtectionRateLimit.get())) - ) - else: - Opt.none(SpamProtection) - let actualDelayStrategy = delayStrategy.valueOr: - if spamProtectionRateLimit.isSome(): - DelayStrategy(SpamProtectionDelayStrategy.new(DefaultMeanDelay, rng())) - else: - DelayStrategy(NoSamplingDelayStrategy.new(rng())) - - let proto = T.new( - mixNodeInfo, - switch, - spamProtection = spamProtection, - delayStrategy = Opt.some(actualDelayStrategy), - ) - - if destReadBehavior.isSome(): - let (codec, callback) = destReadBehavior.get() - proto.registerDestReadBehavior(codec, callback) - - switch.mount(proto) - proto - -proc setupMixNodes*( - numNodes: int, - destReadBehavior = Opt.none(tuple[codec: string, callback: DestReadBehavior]), - spamProtectionRateLimit = Opt.none(int), - delayStrategy = Opt.none(DelayStrategy), -): Future[seq[MixProtocol]] {.async.} = - var nodes: seq[MixProtocol] = @[] - let nodeInfos = MixNodeInfo.generateRandomMany(numNodes) - for mixNodeInfo in nodeInfos: - let switch = - createSwitch(mixNodeInfo.multiAddr, Opt.some(mixNodeInfo.libp2pPrivKey)) - let mixNode = setupMixNode[MixProtocol]( - mixNodeInfo, switch, destReadBehavior, spamProtectionRateLimit, delayStrategy - ) - mixNode.nodePool.add(nodeInfos.includeAllExcept(mixNodeInfo)) - nodes.add(mixNode) - - nodes - -proc setupMixNodesWithMock*( - numNodes: int, - destReadBehavior = Opt.none(tuple[codec: string, callback: DestReadBehavior]), -): Future[tuple[nodes: seq[MixProtocol], mock: MockMixProtocol]] {.async.} = - ## Like setupMixNodes, but the first node is a MockMixProtocol. - var nodes: seq[MixProtocol] = @[] - - let nodeInfos = MixNodeInfo.generateRandomMany(numNodes) - - let mockMixNodeInfo = nodeInfos[0] - let mockSwitch = - createSwitch(mockMixNodeInfo.multiAddr, Opt.some(mockMixNodeInfo.libp2pPrivKey)) - - let mock = setupMixNode[MockMixProtocol]( - mockMixNodeInfo, mockSwitch, destReadBehavior, Opt.none(int) - ) - mock.nodePool.add(nodeInfos.includeAllExcept(mockMixNodeInfo)) - nodes.add(mock) - - for index in 1 ..< numNodes: - let mixNodeInfo = nodeInfos[index] - let switch = - createSwitch(mixNodeInfo.multiAddr, Opt.some(mixNodeInfo.libp2pPrivKey)) - let mixNode = - setupMixNode[MixProtocol](mixNodeInfo, switch, destReadBehavior, Opt.none(int)) - mixNode.nodePool.add(nodeInfos.includeAllExcept(mixNodeInfo)) - nodes.add(mixNode) - - (nodes, mock) - -proc setupDestNode*[T: LPProtocol]( - proto: T -): Future[tuple[switch: Switch, proto: T]] {.async.} = - let switch = createSwitch() - switch.mount(proto) - await switch.start() - return (switch, proto) - -proc stopDestNode*(switch: Switch) {.async.} = - await switch.stop() - -proc toMixDestination*(switch: Switch): MixDestination = - MixDestination.init(switch.peerInfo.peerId, switch.peerInfo.addrs[0]) - -proc toMixDestination*(node: MixProtocol): MixDestination = - node.switch.toMixDestination() - -### - -const NoReplyProtocolCodec = "/test/1.0.0" - -type ReceivedMessage* = object - connPeerId*: PeerId - data*: seq[byte] - -type NoReplyProtocol* = ref object of LPProtocol - receivedMessages*: AsyncQueue[ReceivedMessage] - -proc new*(T: typedesc[NoReplyProtocol]): NoReplyProtocol = - let nrProto = NoReplyProtocol() - nrProto.receivedMessages = newAsyncQueue[ReceivedMessage]() - - proc handler(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} = - try: - let buffer = await conn.readLp(1024) - await nrProto.receivedMessages.put( - ReceivedMessage(connPeerId: conn.peerId, data: buffer) - ) - except LPStreamError: - raiseAssert "should not happen" - finally: - await conn.close() - - nrProto.handler = handler - nrProto.codec = NoReplyProtocolCodec - nrProto - -const EchoCodec = "/echo/test/1.0.0" -const EchoMaxReadLen* = 1024 - -type EchoProtocol* = ref object of LPProtocol - -proc new*(T: typedesc[EchoProtocol]): EchoProtocol = - let echoProto = EchoProtocol() - - proc handler(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} = - try: - let request = await conn.readLp(EchoMaxReadLen) - await conn.writeLp(request) - except CancelledError as e: - raise e - except CatchableError as e: - raiseAssert "Echo handler error: " & e.msg - finally: - await conn.close() - - echoProto.handler = handler - echoProto.codec = EchoCodec - echoProto - -### - -type FixedDelayStrategy* = ref object of DelayStrategy - delay*: Delay - -method generateForEntry*(self: FixedDelayStrategy): Delay = - self.delay - -method generateForIntermediate*(self: FixedDelayStrategy, encodedDelay: Delay): Delay = - self.delay diff --git a/tests/libp2p/test_peer_store.nim b/tests/libp2p/test_peer_store.nim index e93919de61..5df1377566 100644 --- a/tests/libp2p/test_peer_store.nim +++ b/tests/libp2p/test_peer_store.nim @@ -133,58 +133,3 @@ suite "PeerStore": peerStore.cleanup(randomPeerId) check peerStore[AgentBook].len == 30 - - test "MixPubKeyBook API": - # Set up peer store with MixPubKeyBook - var peerStore = PeerStore.new() - - # Generate random Curve25519 keys for testing - let - mixKey1 = Curve25519Key.random(rng[]) - mixKey2 = Curve25519Key.random(rng[]) - - # Test MixPubKeyBook::set - peerStore[MixPubKeyBook][peerId1] = mixKey1 - peerStore[MixPubKeyBook][peerId2] = mixKey2 - - check: - peerStore[MixPubKeyBook][peerId1] == mixKey1 - peerStore[MixPubKeyBook][peerId2] == mixKey2 - - # Test MixPubKeyBook::contains - check: - peerId1 in peerStore[MixPubKeyBook] - peerId2 in peerStore[MixPubKeyBook] - - # Test MixPubKeyBook::len - check: - peerStore[MixPubKeyBook].len == 2 - - # Test MixPubKeyBook::del - check: - peerStore[MixPubKeyBook].del(peerId1) == true - peerId1 notin peerStore[MixPubKeyBook] - peerStore[MixPubKeyBook].len == 1 - - # Test getting non-existent peer returns default - let nonExistentPeerId = PeerId.init(KeyPair.random(ECDSA, rng[]).get().pubkey).get() - check: - peerStore[MixPubKeyBook][nonExistentPeerId] == default(Curve25519Key) - - test "PeerStore::del removes MixPubKeyBook entry": - var peerStore = PeerStore.new() - - let mixKey = Curve25519Key.random(rng[]) - peerStore[MixPubKeyBook][peerId1] = mixKey - peerStore[AddressBook][peerId1] = @[multiaddr1] - - check: - peerId1 in peerStore[MixPubKeyBook] - peerId1 in peerStore[AddressBook] - - # Delete peer from all books - peerStore.del(peerId1) - - check: - peerId1 notin peerStore[MixPubKeyBook] - peerId1 notin peerStore[AddressBook] From 074fe1560bc8a413315e2f3d917a63f0ac12105f Mon Sep 17 00:00:00 2001 From: Prem Chaitanya Prathi Date: Wed, 6 May 2026 10:38:41 +0530 Subject: [PATCH 2/2] chore: drop now-unused curve25519 import from test_peer_store After the MixPubKeyBook tests were removed, the file no longer references any Curve25519 symbols. With UnusedImport treated as an error in config.nims, this would fail strict builds (it's silently tolerated today only because the surrounding bracketed import has other still-used entries). --- tests/libp2p/test_peer_store.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/libp2p/test_peer_store.nim b/tests/libp2p/test_peer_store.nim index 5df1377566..37c1ee8da6 100644 --- a/tests/libp2p/test_peer_store.nim +++ b/tests/libp2p/test_peer_store.nim @@ -4,7 +4,7 @@ {.used.} import std/[tables, sequtils] -import ../../libp2p/[crypto/crypto, crypto/curve25519, multiaddress, peerid, peerstore] +import ../../libp2p/[crypto/crypto, multiaddress, peerid, peerstore] import ../tools/[unittest, crypto] suite "PeerStore":