mirror of
https://github.com/logos-storage/hs-mix-sim.git
synced 2026-07-29 14:45:59 +00:00
hs simulator v1
This commit is contained in:
commit
754bf5cc93
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
#IDE Related
|
||||
.idea
|
||||
|
||||
# Cargo build
|
||||
/target
|
||||
Cargo.lock
|
||||
/output
|
||||
|
||||
# Profile-guided optimization
|
||||
/tmp
|
||||
pgo-data.profdata
|
||||
|
||||
# MacOS nuisances
|
||||
.DS_Store
|
||||
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "hs-mix-sim"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rayon = "1.5"
|
||||
rand = {version = "0.8.4", features=["small_rng"]}
|
||||
rand_distr = "0.4.3"
|
||||
clap = { version = "3.1.0", features = ["derive"] }
|
||||
rustc-hash = "1.0"
|
||||
array-init = "2.0.0"
|
||||
chrono = "0.4.19"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
debug = 1
|
||||
codegen-units = 1
|
||||
178
LICENSE-APACHE-v2.txt
Normal file
178
LICENSE-APACHE-v2.txt
Normal file
@ -0,0 +1,178 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
21
LICENSE-MIT.txt
Normal file
21
LICENSE-MIT.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2026 Status Research & Development GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
121
README.md
Normal file
121
README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# hs-mix-sim
|
||||
|
||||
`hs-mix-sim` is a simplified simulator for route-compromise simulations. It is based on [routesim](https://github.com/frochet/routesim/tree/main) and currently requires the topology output from [MTG-Simulator
|
||||
](https://github.com/sus0pid/MTG-Simulator), though I plan to generate these topologies here later so we don't need the MTG output. It
|
||||
currently supports a simple user model and a simple hidden-service model.
|
||||
|
||||
The simulator reads a layout CSV file, treats each epoch column as one
|
||||
topology snapshot, and samples user message paths through the active topology.
|
||||
A route is marked compromised when every hop in the sampled path is malicious.
|
||||
|
||||
## Current Models
|
||||
|
||||
The simple model:
|
||||
|
||||
- simulates `--users` independent users
|
||||
- samples one message at a time per user
|
||||
- waits a uniformly random interval in `[5, 15)` minutes between messages
|
||||
- optionally uses persistent vanguard and guard selection
|
||||
- emits route records until `--days` virtual days have elapsed
|
||||
- can print an aggregate compromise summary with `--summary`
|
||||
|
||||
The simple hidden-service model:
|
||||
|
||||
- simulates `--users` independent hidden services
|
||||
- waits a uniformly random interval in `[5, 15]` minutes between client requests
|
||||
- sends a uniformly random burst of `[1, 100]` messages for each request
|
||||
- places all messages in a burst within a 60 second window
|
||||
- uses the console request id column to identify the burst when `--to-console` is enabled
|
||||
|
||||
When persistent positions are enabled, routes use:
|
||||
|
||||
- hop 0: persistent vanguard sampled from layer 0
|
||||
- hop 1: persistent guard sampled from layer 1
|
||||
- hop 2: random weighted sample from layer 2
|
||||
|
||||
Use `--disable-vanguards` for guard-only routes:
|
||||
|
||||
- hop 0: random weighted sample from layer 0
|
||||
- hop 1: persistent guard sampled from layer 1
|
||||
- hop 2: random weighted sample from layer 2
|
||||
|
||||
Use `-d` to disable both persistent guards and vanguards.
|
||||
|
||||
The current codebase:
|
||||
|
||||
- `src/main.rs`: CLI, topology loading, runner setup
|
||||
- `src/simulation.rs`: simulation runner, path sampling, compromise check, output
|
||||
- `src/simplemodel.rs`: simple synchronous user behavior
|
||||
- `src/simplehiddenservicemodel.rs`: simple hidden-service burst behavior
|
||||
- `src/usermodel.rs`: minimal user-model traits, route event type, guard state
|
||||
- `src/config.rs`: topology CSV parsing and weighted path sampling
|
||||
- `src/mixnodes/`: mix node parsing/types
|
||||
|
||||
## Build And Test
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
```bash
|
||||
cargo run -- --help
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
cargo run -- \
|
||||
--topology-file testfiles/single_layout/1000_137_Random_BP_layout.csv \
|
||||
--model simple \
|
||||
--epoch 86401 \
|
||||
--users 2 \
|
||||
--days 1 \
|
||||
--to-console
|
||||
```
|
||||
|
||||
Example output line:
|
||||
|
||||
```text
|
||||
1970-01-01 00:44:31 2538 570,260,1007 false
|
||||
```
|
||||
|
||||
That line contains the virtual timestamp, user id, sampled path mix ids, and
|
||||
whether the sampled route was fully compromised.
|
||||
|
||||
For large runs, avoid route output and print only the aggregate summary:
|
||||
|
||||
simple model:
|
||||
```bash
|
||||
cargo run --release -- \
|
||||
--topology-file testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.03_layout.csv \
|
||||
--model simple \
|
||||
--epoch 3600 \
|
||||
--users 5000 \
|
||||
--days 30 \
|
||||
--summary
|
||||
```
|
||||
|
||||
hidden service model:
|
||||
```bash
|
||||
cargo run --release -- \
|
||||
--topology-file testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.03_layout.csv \
|
||||
--model simple-hidden-service \
|
||||
--epoch 3600 \
|
||||
--users 5000 \
|
||||
--days 30 \
|
||||
--summary
|
||||
```
|
||||
|
||||
|
||||
## TODO
|
||||
- [ ] generate topologies locally (this will probably best work for free-route because of bin-packing in the stratified mix from the paper)
|
||||
- [ ] simulate free-route mix
|
||||
- [ ] fix the selection logic for vanguards, current logic is the same as guard, Tor spec defines differently. Also vanguards are sampled from the first hop/layer and that layer doesn't have the guard maintenance logic, i.e., active guards, backup guards, down guards, etc. so it is less effective.
|
||||
- [ ] add more complex hidden services simulations
|
||||
- [ ] add model for dead-drops/mailboxes, see approaches used in these paper:
|
||||
- [Vuvuzela](https://dl.acm.org/doi/pdf/10.1145/2815400.2815417)
|
||||
- [Alpenhorn](https://www.usenix.org/system/files/conference/osdi16/osdi16-lazar.pdf)
|
||||
- [Stadium](https://dl.acm.org/doi/pdf/10.1145/3132747.3132783)
|
||||
- [Karaoke](https://www.usenix.org/system/files/osdi18-lazar.pdf)
|
||||
- [Groove](https://www.usenix.org/system/files/osdi22-barman.pdf)
|
||||
- [PingPong](https://arxiv.org/pdf/2504.19566)
|
||||
BIN
results/img/hs-msgs.png
Normal file
BIN
results/img/hs-msgs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
results/img/hs-time.png
Normal file
BIN
results/img/hs-time.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
results/img/simple-guard.png
Normal file
BIN
results/img/simple-guard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
results/img/simple-vanguard.png
Normal file
BIN
results/img/simple-vanguard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
results/img/simple.png
Normal file
BIN
results/img/simple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
70
results/results.md
Normal file
70
results/results.md
Normal file
@ -0,0 +1,70 @@
|
||||
## Simple Model
|
||||
|
||||
### without guards
|
||||
Summary:
|
||||
- users=5000
|
||||
- days=30
|
||||
- total_messages=21616205
|
||||
- compromised_users=5000
|
||||
- percentage_compromised_users=100.000000
|
||||
|
||||

|
||||
|
||||
### with guards
|
||||
Summary:
|
||||
- users=5000
|
||||
- days=30
|
||||
- total_messages=21612322
|
||||
- compromised_users=1943
|
||||
- percentage_compromised_users=38.860000
|
||||
|
||||

|
||||
|
||||
the result seem to be around 0.4 probably
|
||||
because we have 10% adversary control and a set of 5 guards which we sample.
|
||||
changing these will probably change outcome.
|
||||
|
||||
### with guards and vanguards
|
||||
Summary:
|
||||
- users=5000
|
||||
- days=30
|
||||
- total_messages=21616884
|
||||
- compromised_users=1589
|
||||
- percentage_compromised_users=31.780000
|
||||
|
||||

|
||||
|
||||
## hidden service Model
|
||||
|
||||
### without guards
|
||||
Summary:
|
||||
- users=5000
|
||||
- days=1
|
||||
- total_messages=36224095
|
||||
- compromised_users=5000
|
||||
- percentage_compromised_users=100.000000
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
### with guards
|
||||
Summary:
|
||||
- users=5000
|
||||
- days=30
|
||||
- total_messages=1090812738
|
||||
- compromised_users=2165
|
||||
- percentage_compromised_users=43.300000
|
||||
|
||||
TODO: generating figures takes too long, need to fix few things.
|
||||
|
||||
### with guards and vanguards
|
||||
|
||||
Summary:
|
||||
- users=5000
|
||||
- days=30
|
||||
- total_messages=1090615016
|
||||
- compromised_users=1720
|
||||
- percentage_compromised_users=34.400000
|
||||
|
||||
246
scripts/plot_v2_console.py
Executable file
246
scripts/plot_v2_console.py
Executable file
@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import calendar
|
||||
import csv
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserStats:
|
||||
messages: int = 0
|
||||
bursts_seen: set[str] = field(default_factory=set)
|
||||
first_compromise_seconds: float = math.inf
|
||||
messages_until_compromise: float = math.inf
|
||||
bursts_until_compromise: float = math.inf
|
||||
|
||||
|
||||
def parse_bool(value: str) -> bool:
|
||||
lowered = value.strip().lower()
|
||||
if lowered in {"true", "1", "yes"}:
|
||||
return True
|
||||
if lowered in {"false", "0", "no"}:
|
||||
return False
|
||||
raise ValueError(f"invalid boolean: {value}")
|
||||
|
||||
|
||||
def parse_timestamp(date_part: str, time_part: str) -> int:
|
||||
dt = datetime.fromisoformat(f"{date_part} {time_part}")
|
||||
return calendar.timegm(dt.timetuple())
|
||||
|
||||
|
||||
def parse_console_line(line: str, expected_format: str):
|
||||
parts = line.split()
|
||||
if expected_format == "simple":
|
||||
if len(parts) != 5:
|
||||
return None
|
||||
date_part, time_part, user, _path, compromised = parts
|
||||
return parse_timestamp(date_part, time_part), int(user), None, parse_bool(compromised)
|
||||
|
||||
if expected_format == "hidden-service":
|
||||
if len(parts) != 6:
|
||||
return None
|
||||
date_part, time_part, user, burst_id, _path, compromised = parts
|
||||
return parse_timestamp(date_part, time_part), int(user), burst_id, parse_bool(compromised)
|
||||
|
||||
raise ValueError(f"unsupported format: {expected_format}")
|
||||
|
||||
|
||||
def process_console_output(path: Path, expected_format: str, users: Optional[int]):
|
||||
stats: dict[int, UserStats] = {}
|
||||
skipped = 0
|
||||
|
||||
with path.open() as infile:
|
||||
for line in infile:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parsed = parse_console_line(line, expected_format)
|
||||
if parsed is None:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
timestamp, user, burst_id, compromised = parsed
|
||||
user_stats = stats.setdefault(user, UserStats())
|
||||
user_stats.messages += 1
|
||||
if burst_id is not None:
|
||||
user_stats.bursts_seen.add(burst_id)
|
||||
|
||||
if compromised and math.isinf(user_stats.first_compromise_seconds):
|
||||
user_stats.first_compromise_seconds = timestamp
|
||||
user_stats.messages_until_compromise = user_stats.messages
|
||||
if burst_id is not None:
|
||||
user_stats.bursts_until_compromise = len(user_stats.bursts_seen)
|
||||
|
||||
if users is None:
|
||||
users = max(stats.keys(), default=-1) + 1
|
||||
|
||||
for user in range(users):
|
||||
stats.setdefault(user, UserStats())
|
||||
|
||||
return stats, skipped
|
||||
|
||||
|
||||
def finite_values(values):
|
||||
return sorted(value for value in values if math.isfinite(value))
|
||||
|
||||
|
||||
def choose_time_unit(values):
|
||||
finite = finite_values(values)
|
||||
if not finite:
|
||||
return 1, "seconds"
|
||||
max_value = max(finite)
|
||||
if max_value <= 7 * 24 * 60 * 60:
|
||||
return 60 * 60, "hours"
|
||||
if max_value <= 30 * 24 * 60 * 60:
|
||||
return 24 * 60 * 60, "days"
|
||||
if max_value <= 12 * 30 * 24 * 60 * 60:
|
||||
return 7 * 24 * 60 * 60, "weeks"
|
||||
return 30 * 24 * 60 * 60, "months"
|
||||
|
||||
|
||||
def plot_cdf(values, total_users, xlabel, out_file):
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("PDF")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
finite = finite_values(values)
|
||||
fig, ax = plt.subplots(figsize=(6.4, 3.8))
|
||||
if finite:
|
||||
xs = []
|
||||
ys = []
|
||||
last_y = 0.0
|
||||
for idx, value in enumerate(finite, start=1):
|
||||
y = idx / total_users
|
||||
xs.extend([value, value])
|
||||
ys.extend([last_y, y])
|
||||
last_y = y
|
||||
ax.plot(xs, ys, "-o", linewidth=2, markevery=max(1, len(xs) // 10))
|
||||
ax.set_xlim(left=0)
|
||||
else:
|
||||
ax.text(0.5, 0.5, "No compromised users", ha="center", va="center")
|
||||
ax.set_xlim(0, 1)
|
||||
|
||||
ax.set_ylim(0, 1)
|
||||
ax.set_xlabel(xlabel)
|
||||
ax.set_ylabel("Cumulative probability")
|
||||
ax.grid(True)
|
||||
fig.tight_layout()
|
||||
fig.savefig(out_file)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def write_per_user_csv(stats, out_file):
|
||||
with out_file.open("w", newline="") as outfile:
|
||||
writer = csv.writer(outfile)
|
||||
writer.writerow(
|
||||
[
|
||||
"user",
|
||||
"messages",
|
||||
"bursts",
|
||||
"first_compromise_seconds",
|
||||
"messages_until_compromise",
|
||||
"bursts_until_compromise",
|
||||
]
|
||||
)
|
||||
for user in sorted(stats):
|
||||
user_stats = stats[user]
|
||||
writer.writerow(
|
||||
[
|
||||
user,
|
||||
user_stats.messages,
|
||||
len(user_stats.bursts_seen),
|
||||
"inf"
|
||||
if math.isinf(user_stats.first_compromise_seconds)
|
||||
else int(user_stats.first_compromise_seconds),
|
||||
"inf"
|
||||
if math.isinf(user_stats.messages_until_compromise)
|
||||
else int(user_stats.messages_until_compromise),
|
||||
"inf"
|
||||
if math.isinf(user_stats.bursts_until_compromise)
|
||||
else int(user_stats.bursts_until_compromise),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def write_summary(stats, skipped, out_file):
|
||||
users = len(stats)
|
||||
compromised_users = sum(
|
||||
1 for user_stats in stats.values() if math.isfinite(user_stats.first_compromise_seconds)
|
||||
)
|
||||
total_messages = sum(user_stats.messages for user_stats in stats.values())
|
||||
compromised_pct = compromised_users / users * 100 if users else 0
|
||||
|
||||
with out_file.open("w") as outfile:
|
||||
outfile.write(f"users={users}\n")
|
||||
outfile.write(f"total_messages={total_messages}\n")
|
||||
outfile.write(f"compromised_users={compromised_users}\n")
|
||||
outfile.write(f"percentage_compromised_users={compromised_pct:.6f}\n")
|
||||
outfile.write(f"skipped_lines={skipped}\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Parse routesim-v2 --to-console output and plot CDFs."
|
||||
)
|
||||
parser.add_argument("--in-file", required=True, type=Path)
|
||||
parser.add_argument("--out-prefix", required=True, type=Path)
|
||||
parser.add_argument("--users", type=int)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["simple", "hidden-service"],
|
||||
required=True,
|
||||
help="Expected routesim-v2 console output format.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plots",
|
||||
default=None,
|
||||
help="Comma-separated plots to generate: time,messages,bursts. Defaults by format.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
stats, skipped = process_console_output(args.in_file, args.format, args.users)
|
||||
args.out_prefix.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
write_per_user_csv(stats, args.out_prefix.with_suffix(".per_user.csv"))
|
||||
write_summary(stats, skipped, args.out_prefix.with_suffix(".summary.txt"))
|
||||
|
||||
plots = (
|
||||
args.plots.split(",")
|
||||
if args.plots
|
||||
else (["time", "messages", "bursts"] if args.format == "hidden-service" else ["time", "messages"])
|
||||
)
|
||||
|
||||
if "time" in plots:
|
||||
raw_values = [user_stats.first_compromise_seconds for user_stats in stats.values()]
|
||||
divider, unit = choose_time_unit(raw_values)
|
||||
plot_cdf(
|
||||
[value / divider if math.isfinite(value) else value for value in raw_values],
|
||||
len(stats),
|
||||
f"time to first compromise [{unit}]",
|
||||
args.out_prefix.with_name(args.out_prefix.name + "_time_cdf.pdf"),
|
||||
)
|
||||
|
||||
if "messages" in plots:
|
||||
plot_cdf(
|
||||
[user_stats.messages_until_compromise for user_stats in stats.values()],
|
||||
len(stats),
|
||||
"messages until first compromise",
|
||||
args.out_prefix.with_name(args.out_prefix.name + "_messages_cdf.pdf"),
|
||||
)
|
||||
|
||||
if "bursts" in plots:
|
||||
plot_cdf(
|
||||
[user_stats.bursts_until_compromise for user_stats in stats.values()],
|
||||
len(stats),
|
||||
"bursts until first compromise",
|
||||
args.out_prefix.with_name(args.out_prefix.name + "_bursts_cdf.pdf"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
59
scripts/run_hidden_service_plot.sh
Executable file
59
scripts/run_hidden_service_plot.sh
Executable file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 TOPOLOGY_FILE OUT_DIR [USERS=5000] [DAYS=30] [EPOCH=3600] [ROUTESIM_FLAGS...]" >&2
|
||||
echo "Example: $0 testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.03_layout.csv results 5000 30 3600 --disable-vanguards" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
CALLER_CWD="$(pwd)"
|
||||
|
||||
TOPOLOGY_FILE="$1"
|
||||
OUT_DIR="$2"
|
||||
USERS="${3:-5000}"
|
||||
DAYS="${4:-30}"
|
||||
EPOCH="${5:-3600}"
|
||||
shift $(( $# >= 5 ? 5 : $# ))
|
||||
|
||||
case "$TOPOLOGY_FILE" in
|
||||
/*) ;;
|
||||
*) TOPOLOGY_FILE="$CALLER_CWD/$TOPOLOGY_FILE" ;;
|
||||
esac
|
||||
|
||||
case "$OUT_DIR" in
|
||||
/*) ;;
|
||||
*) OUT_DIR="$CALLER_CWD/$OUT_DIR" ;;
|
||||
esac
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
export MPLCONFIGDIR="${MPLCONFIGDIR:-$OUT_DIR/.matplotlib}"
|
||||
mkdir -p "$MPLCONFIGDIR"
|
||||
|
||||
RAW_OUT="$OUT_DIR/hidden_service_${USERS}_${DAYS}_${EPOCH}.routes"
|
||||
OUT_PREFIX="$OUT_DIR/hidden_service_${USERS}_${DAYS}_${EPOCH}"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
cargo run --release -- \
|
||||
--model simple-hidden-service \
|
||||
--topology-file "$TOPOLOGY_FILE" \
|
||||
--users "$USERS" \
|
||||
--days "$DAYS" \
|
||||
--epoch "$EPOCH" \
|
||||
--to-console \
|
||||
"$@" > "$RAW_OUT"
|
||||
|
||||
python3 "$SCRIPT_DIR/plot_v2_console.py" \
|
||||
--format hidden-service \
|
||||
--users "$USERS" \
|
||||
--in-file "$RAW_OUT" \
|
||||
--out-prefix "$OUT_PREFIX"
|
||||
|
||||
echo "raw_output=$RAW_OUT"
|
||||
echo "per_user=$OUT_PREFIX.per_user.csv"
|
||||
echo "summary=$OUT_PREFIX.summary.txt"
|
||||
echo "time_plot=${OUT_PREFIX}_time_cdf.pdf"
|
||||
echo "messages_plot=${OUT_PREFIX}_messages_cdf.pdf"
|
||||
echo "bursts_plot=${OUT_PREFIX}_bursts_cdf.pdf"
|
||||
58
scripts/run_simple_plot.sh
Executable file
58
scripts/run_simple_plot.sh
Executable file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 TOPOLOGY_FILE OUT_DIR [USERS=5000] [DAYS=30] [EPOCH=3600] [ROUTESIM_FLAGS...]" >&2
|
||||
echo "Example: $0 testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.03_layout.csv results 5000 30 3600 --disable-vanguards" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
CALLER_CWD="$(pwd)"
|
||||
|
||||
TOPOLOGY_FILE="$1"
|
||||
OUT_DIR="$2"
|
||||
USERS="${3:-5000}"
|
||||
DAYS="${4:-30}"
|
||||
EPOCH="${5:-3600}"
|
||||
shift $(( $# >= 5 ? 5 : $# ))
|
||||
|
||||
case "$TOPOLOGY_FILE" in
|
||||
/*) ;;
|
||||
*) TOPOLOGY_FILE="$CALLER_CWD/$TOPOLOGY_FILE" ;;
|
||||
esac
|
||||
|
||||
case "$OUT_DIR" in
|
||||
/*) ;;
|
||||
*) OUT_DIR="$CALLER_CWD/$OUT_DIR" ;;
|
||||
esac
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
export MPLCONFIGDIR="${MPLCONFIGDIR:-$OUT_DIR/.matplotlib}"
|
||||
mkdir -p "$MPLCONFIGDIR"
|
||||
|
||||
RAW_OUT="$OUT_DIR/simple_${USERS}_${DAYS}_${EPOCH}.routes"
|
||||
OUT_PREFIX="$OUT_DIR/simple_${USERS}_${DAYS}_${EPOCH}"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
cargo run --release -- \
|
||||
--model simple \
|
||||
--topology-file "$TOPOLOGY_FILE" \
|
||||
--users "$USERS" \
|
||||
--days "$DAYS" \
|
||||
--epoch "$EPOCH" \
|
||||
--to-console \
|
||||
"$@" > "$RAW_OUT"
|
||||
|
||||
python3 "$SCRIPT_DIR/plot_v2_console.py" \
|
||||
--format simple \
|
||||
--users "$USERS" \
|
||||
--in-file "$RAW_OUT" \
|
||||
--out-prefix "$OUT_PREFIX"
|
||||
|
||||
echo "raw_output=$RAW_OUT"
|
||||
echo "per_user=$OUT_PREFIX.per_user.csv"
|
||||
echo "summary=$OUT_PREFIX.summary.txt"
|
||||
echo "time_plot=${OUT_PREFIX}_time_cdf.pdf"
|
||||
echo "messages_plot=${OUT_PREFIX}_messages_cdf.pdf"
|
||||
292
src/config.rs
Normal file
292
src/config.rs
Normal file
@ -0,0 +1,292 @@
|
||||
use array_init::array_init;
|
||||
use rand::prelude::*;
|
||||
use rand_distr::weighted_alias::WeightedAliasIndex;
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
pub const PATH_LENGTH: i8 = 3;
|
||||
#[allow(dead_code)]
|
||||
/// in byte
|
||||
pub const PAYLOAD_SIZE: usize = 2048;
|
||||
/// Default sample size for guards -- todo move this in clap
|
||||
pub const GUARDS_SAMPLE_SIZE: usize = 5;
|
||||
/// How much do we extend the sample size each time we ran out of guard?
|
||||
pub const GUARDS_SAMPLE_SIZE_EXTEND: usize = 2;
|
||||
/// guards are use in layer... [0..n[
|
||||
pub const GUARDS_LAYER: usize = 1;
|
||||
/// Default sample size for vanguards.
|
||||
pub const VANGUARDS_SAMPLE_SIZE: usize = 5;
|
||||
/// How much do we extend the vanguard sample size each time we ran out.
|
||||
pub const VANGUARDS_SAMPLE_SIZE_EXTEND: usize = 2;
|
||||
/// vanguards are used in the first route hop.
|
||||
pub const VANGUARDS_LAYER: usize = 0;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mixnode {
|
||||
pub layer: i8,
|
||||
pub weight: f64,
|
||||
pub mixid: u32,
|
||||
pub is_malicious: bool,
|
||||
}
|
||||
|
||||
/// A config is a set of mixes for each layer
|
||||
/// and a hashmap for unselected mixes.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TopologyConfig {
|
||||
#[allow(dead_code)]
|
||||
pub filename: String,
|
||||
|
||||
pub epoch: u32,
|
||||
/// The path length
|
||||
layers: [Vec<Mixnode>; PATH_LENGTH as usize],
|
||||
wc_layers: [Box<Option<WeightedAliasIndex<f64>>>; PATH_LENGTH as usize],
|
||||
unselected: HashMap<u32, Mixnode>,
|
||||
}
|
||||
|
||||
impl TopologyConfig {
|
||||
pub fn new(filename: String, epoch: u32) -> Self {
|
||||
TopologyConfig {
|
||||
filename,
|
||||
epoch,
|
||||
wc_layers: array_init(|_| Box::new(None)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn layers(&self) -> &[Vec<Mixnode>] {
|
||||
&self.layers
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn unselected(&self) -> &HashMap<u32, Mixnode> {
|
||||
&self.unselected
|
||||
}
|
||||
pub fn has_mix_in_layer(&self, layer: usize, mixid: u32) -> bool {
|
||||
self.layers[layer].iter().any(|mix| mix.mixid == mixid)
|
||||
}
|
||||
|
||||
fn push_mix(&mut self, mut mix: Mixnode, layer: i8) {
|
||||
mix.layer = layer;
|
||||
match mix.layer {
|
||||
// This trick gets arround the unsupported excluded range syntax for now
|
||||
0..=PATH_LENGTH if mix.layer < PATH_LENGTH => self.layers[mix.layer as usize].push(mix),
|
||||
_ => {
|
||||
self.unselected.insert(mix.mixid, mix);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn build_weighted_samplers(&mut self) {
|
||||
for i in 0..PATH_LENGTH {
|
||||
self.wc_layers[i as usize] = Box::new(Some(
|
||||
WeightedAliasIndex::new(
|
||||
self.layers[i as usize]
|
||||
.iter()
|
||||
.map(|item| item.weight)
|
||||
.collect(),
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Unable to build weighted sampler for {} epoch {} layer {}",
|
||||
self.filename, self.epoch, i
|
||||
)
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample n mixes from layer l.
|
||||
pub fn sample_layer<'a, R: Rng + ?Sized>(
|
||||
&'a self,
|
||||
l: usize,
|
||||
n_mixes: usize,
|
||||
rng: &mut R,
|
||||
) -> IntoIter<&'a Mixnode> {
|
||||
let mut sample_mixes = vec![];
|
||||
for _ in 0..n_mixes {
|
||||
if let Some(wc) = &*self.wc_layers[l] {
|
||||
sample_mixes.push(&self.layers[l][wc.sample(rng)]);
|
||||
}
|
||||
}
|
||||
sample_mixes.into_iter()
|
||||
}
|
||||
|
||||
/// Sample a route from the network layer configuration
|
||||
#[inline]
|
||||
pub fn sample_path<'a, R: Rng + ?Sized>(
|
||||
&'a self,
|
||||
rng: &mut R,
|
||||
vanguard: Option<&'a Mixnode>,
|
||||
guard: Option<&'a Mixnode>,
|
||||
) -> IntoIter<&'a Mixnode> {
|
||||
let mut path = vec![];
|
||||
// returns an owned iterator
|
||||
for i in 0..PATH_LENGTH {
|
||||
if let Some(wc) = &*self.wc_layers[i as usize] {
|
||||
match (vanguard, guard) {
|
||||
(Some(v), _) if i as usize == VANGUARDS_LAYER => path.push(v),
|
||||
(_, Some(g)) if i as usize == GUARDS_LAYER => path.push(g),
|
||||
_ => path.push(&self.layers[i as usize][wc.sample(rng)]),
|
||||
}
|
||||
}
|
||||
}
|
||||
path.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all topology epochs from one layout file.
|
||||
///
|
||||
/// Each row must start with:
|
||||
/// mixid [integer], weight [float], is_malicious [bool]
|
||||
///
|
||||
/// Remaining columns are layer assignments. A single `layer` column produces one topology.
|
||||
/// Multiple `epoch_N` columns produce one topology per epoch.
|
||||
pub fn load<P>(filename: P) -> Vec<TopologyConfig>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(&filename).expect("Unable to open the file");
|
||||
let filename_string: String = filename.as_ref().to_str().unwrap().to_owned();
|
||||
let mut line_reader = BufReader::new(file).lines();
|
||||
let header = line_reader
|
||||
.next()
|
||||
.expect("Topology file is empty")
|
||||
.expect("Unable to read topology header");
|
||||
let header_cols: Vec<_> = header.split(',').map(|col| col.trim()).collect();
|
||||
if header_cols.len() < 4 {
|
||||
panic!(
|
||||
"Topology file {} must have at least 4 columns",
|
||||
filename.as_ref().display()
|
||||
);
|
||||
}
|
||||
|
||||
let epochs = parse_epoch_columns(&header_cols[3..]);
|
||||
let mut configs: Vec<TopologyConfig> = epochs
|
||||
.iter()
|
||||
.map(|epoch| TopologyConfig::new(filename_string.clone(), *epoch))
|
||||
.collect();
|
||||
|
||||
for line_r in line_reader {
|
||||
let line = line_r.expect("Something went wrong while reading topology file");
|
||||
let cols: Vec<_> = line.split(',').map(|col| col.trim()).collect();
|
||||
if cols.len() < header_cols.len() {
|
||||
panic!(
|
||||
"Unable to parse {} into a topology row -- expected {} columns, got {}: {}",
|
||||
filename.as_ref().display(),
|
||||
header_cols.len(),
|
||||
cols.len(),
|
||||
line
|
||||
);
|
||||
}
|
||||
|
||||
let mix = parse_mix_row(&cols, filename.as_ref());
|
||||
for (idx, config) in configs.iter_mut().enumerate() {
|
||||
let layer = cols[idx + 3].parse::<i8>().unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Unable to parse layer value '{}' in {}",
|
||||
cols[idx + 3],
|
||||
filename.as_ref().display()
|
||||
)
|
||||
});
|
||||
config.push_mix(mix.clone(), layer);
|
||||
}
|
||||
}
|
||||
|
||||
for config in &mut configs {
|
||||
config.build_weighted_samplers();
|
||||
}
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
fn parse_epoch_columns(columns: &[&str]) -> Vec<u32> {
|
||||
columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, column)| {
|
||||
if *column == "layer" {
|
||||
idx as u32
|
||||
} else {
|
||||
column
|
||||
.split('_')
|
||||
.nth(1)
|
||||
.and_then(|epoch| epoch.parse::<u32>().ok())
|
||||
.unwrap_or(idx as u32)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_mix_row(cols: &[&str], filename: &Path) -> Mixnode {
|
||||
let mixid = cols[0].parse::<u32>().unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Unable to parse mix id '{}' in {}",
|
||||
cols[0],
|
||||
filename.display()
|
||||
)
|
||||
});
|
||||
let weight = cols[1].parse::<f64>().unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Unable to parse bandwidth '{}' in {}",
|
||||
cols[1],
|
||||
filename.display()
|
||||
)
|
||||
});
|
||||
let is_malicious = cols[2].to_lowercase().parse::<bool>().unwrap_or(false);
|
||||
|
||||
Mixnode {
|
||||
layer: -1,
|
||||
weight,
|
||||
mixid,
|
||||
is_malicious,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_test_topology_config() {
|
||||
let configs = load("testfiles/single_layout/1000_137_Random_BP_layout.csv");
|
||||
let config = &configs[0];
|
||||
let mix = &config.layers()[0][42];
|
||||
//42│20.430784458454426│False│0
|
||||
assert_eq!(mix.is_malicious, false);
|
||||
assert_eq!(mix.weight, 1.8480844586590168);
|
||||
}
|
||||
#[test]
|
||||
fn test_sample_path() {
|
||||
let configs = load("testfiles/single_layout/1000_137_Random_BP_layout.csv");
|
||||
let config = &configs[0];
|
||||
let mut rng = thread_rng();
|
||||
let path = config.sample_path(&mut rng, None, None);
|
||||
let path = path.collect::<Vec<_>>();
|
||||
assert_eq!(path.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_path_uses_fixed_vanguard_and_guard() {
|
||||
let configs = load("testfiles/single_layout/1000_137_Random_BP_layout.csv");
|
||||
let config = &configs[0];
|
||||
let mut rng = thread_rng();
|
||||
let vanguard = &config.layers()[VANGUARDS_LAYER][0];
|
||||
let guard = &config.layers()[GUARDS_LAYER][0];
|
||||
let path = config
|
||||
.sample_path(&mut rng, Some(vanguard), Some(guard))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(path.len(), 3);
|
||||
assert_eq!(path[0].mixid, vanguard.mixid);
|
||||
assert_eq!(path[1].mixid, guard.mixid);
|
||||
assert_eq!(path[2].layer, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_multi_epoch_layout() {
|
||||
let configs = load("testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.03_layout.csv");
|
||||
assert_eq!(configs.len(), 1000);
|
||||
assert_eq!(configs[0].epoch, 0);
|
||||
assert_eq!(configs[999].epoch, 999);
|
||||
assert_eq!(configs[0].layers().len(), PATH_LENGTH as usize);
|
||||
}
|
||||
120
src/main.rs
Normal file
120
src/main.rs
Normal file
@ -0,0 +1,120 @@
|
||||
//! Simulating behavioural activity within a Mixnet
|
||||
//!
|
||||
//! This tool evaluates the probability of deanonymization through time,
|
||||
//! assuming some level of adversarial activity among the mixes.
|
||||
//!
|
||||
//! We expect to take in input mixnet [`topologies`](config::TopologyConfig).
|
||||
//! The simple model samples one message per user at a time and virtually sends
|
||||
//! those messages through the mixnet.
|
||||
//!
|
||||
//! The simulator appliess a Monte Carlo method to draw paths and outputs path
|
||||
//! information for each message sent by each sample.
|
||||
//!
|
||||
|
||||
mod config;
|
||||
mod simulation;
|
||||
mod simplehiddenservicemodel;
|
||||
mod simplemodel;
|
||||
mod usermodel;
|
||||
|
||||
use clap::{ArgEnum, Parser};
|
||||
use config::TopologyConfig;
|
||||
use simulation::Runable;
|
||||
use simplehiddenservicemodel::*;
|
||||
use simplemodel::*;
|
||||
|
||||
/// currently supported models
|
||||
#[derive(Copy, Clone, Debug, ArgEnum)]
|
||||
enum Model {
|
||||
Simple,
|
||||
SimpleHiddenService,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Opts {
|
||||
#[clap(
|
||||
short = 'i',
|
||||
long,
|
||||
required = true,
|
||||
parse(from_os_str),
|
||||
help = "MTG layout CSV containing one or more topology epochs"
|
||||
)]
|
||||
topology_file: std::path::PathBuf,
|
||||
#[clap(long, default_value = "1", help = "Number of simulated days")]
|
||||
days: u32,
|
||||
#[clap(long, default_value = "5000", help = "Number of users to simulate")]
|
||||
users: u32,
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
default_value = "86401",
|
||||
help = "Validity period for a given topologies"
|
||||
)]
|
||||
epoch: u32,
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Print human-readable route output, one route per line"
|
||||
)]
|
||||
to_console: bool,
|
||||
#[clap(long, help = "Print aggregate compromise summary after simulation")]
|
||||
summary: bool,
|
||||
#[clap(
|
||||
long,
|
||||
arg_enum,
|
||||
default_value = "simple",
|
||||
help = "User/service traffic model to simulate"
|
||||
)]
|
||||
model: Model,
|
||||
#[clap(short, help = "Disable persistent guard and vanguard positions")]
|
||||
disable_guards: bool,
|
||||
#[clap(
|
||||
long,
|
||||
help = "Disable the persistent vanguard position while keeping guards enabled"
|
||||
)]
|
||||
disable_vanguards: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
let mut topologies: Vec<TopologyConfig> = config::load(&opts.topology_file);
|
||||
// We need sorting the topologies for accessing the right one depending
|
||||
// on the current epoch
|
||||
topologies.sort_by(|a, b| a.epoch.cmp(&b.epoch));
|
||||
let n = topologies.len();
|
||||
|
||||
let mut epoch = opts.epoch;
|
||||
// check whether the parameters days; config and epoch make sense
|
||||
if epoch * n as u32 <= opts.days * 24 * 60 * 60 {
|
||||
eprintln!("Make sure you have enough configuration files, and that the epoch and days value make sense!");
|
||||
epoch = 86400 * opts.days + 1;
|
||||
eprintln!("Setting epoch to {epoch}. Maybe you want to change that");
|
||||
}
|
||||
let mut runner = Runable::new(opts.users, topologies, opts.days, epoch);
|
||||
|
||||
if !opts.disable_guards {
|
||||
runner.with_guards();
|
||||
}
|
||||
if !opts.disable_guards && !opts.disable_vanguards {
|
||||
runner.with_vanguards();
|
||||
}
|
||||
if opts.to_console {
|
||||
runner.with_console();
|
||||
}
|
||||
if opts.summary {
|
||||
runner.with_summary();
|
||||
}
|
||||
|
||||
match opts.model {
|
||||
Model::Simple => {
|
||||
let usermodels = runner.init::<SimpleSynchronousModel>();
|
||||
runner.run(usermodels);
|
||||
}
|
||||
Model::SimpleHiddenService => {
|
||||
let usermodels = runner.init::<SimpleHiddenServiceModel>();
|
||||
runner.run(usermodels);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/simplehiddenservicemodel.rs
Normal file
105
src/simplehiddenservicemodel.rs
Normal file
@ -0,0 +1,105 @@
|
||||
//! A simple hidden-service model.
|
||||
//!
|
||||
//! Each simulated service receives a client request after a random `[INTERVAL_MIN, INTERVAL_MAX]`
|
||||
//! (`default = [5, 15] minute`)
|
||||
//! interval. For every request, it sends a burst of `[MESSAGES_MIN, MESSAGES_MAX]`
|
||||
//! (`default = [1, 100]`) messages within the following `BURST_WINDOW_SECONDS` (default = 60 seconds).
|
||||
use crate::usermodel::{RequestHandler, RouteEvent, UserModel, UserModelInfo};
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::SeedableRng;
|
||||
|
||||
const INTERVAL_MAX: u64 = 900;
|
||||
const INTERVAL_MIN: u64 = 300;
|
||||
const BURST_WINDOW_SECONDS: u64 = 60;
|
||||
const MESSAGES_MAX: u64 = 100;
|
||||
const MESSAGES_MIN: u64 = 1;
|
||||
|
||||
pub struct SimpleHiddenServiceModel<'a> {
|
||||
/// timestamp of the current burst start, starting at 0.
|
||||
current_time: u64,
|
||||
/// the max value of a message timing
|
||||
limit: u64,
|
||||
rng: SmallRng,
|
||||
interval_die: Uniform<u64>,
|
||||
message_count_die: Uniform<u64>,
|
||||
burst_offset_die: Uniform<u64>,
|
||||
burst_messages: Vec<u64>,
|
||||
next_burst_message: usize,
|
||||
current_burst_id: u128,
|
||||
user_info: UserModelInfo<'a>,
|
||||
}
|
||||
|
||||
impl<'a> UserModel<'a> for SimpleHiddenServiceModel<'a> {
|
||||
fn new(_tot_users: u32, _epoch: u32, user_info: UserModelInfo<'a>) -> Self {
|
||||
SimpleHiddenServiceModel {
|
||||
rng: SmallRng::from_entropy(),
|
||||
interval_die: Uniform::from(INTERVAL_MIN..=INTERVAL_MAX),
|
||||
message_count_die: Uniform::from(MESSAGES_MIN..=MESSAGES_MAX),
|
||||
burst_offset_die: Uniform::from(0..=BURST_WINDOW_SECONDS),
|
||||
current_time: 0,
|
||||
limit: 0,
|
||||
burst_messages: Vec::new(),
|
||||
next_burst_message: 0,
|
||||
current_burst_id: 0,
|
||||
user_info,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_limit(&mut self, limit: u64) {
|
||||
self.limit = limit;
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleHiddenServiceModel<'_> {
|
||||
fn has_pending_burst_messages(&self) -> bool {
|
||||
self.next_burst_message < self.burst_messages.len()
|
||||
}
|
||||
|
||||
fn start_next_burst(&mut self) {
|
||||
self.current_time += self.interval_die.sample(&mut self.rng);
|
||||
self.current_burst_id += 1;
|
||||
self.next_burst_message = 0;
|
||||
self.burst_messages.clear();
|
||||
|
||||
let message_count = self.message_count_die.sample(&mut self.rng) as usize;
|
||||
self.burst_messages.reserve(message_count);
|
||||
for _ in 0..message_count {
|
||||
let offset = self.burst_offset_die.sample(&mut self.rng);
|
||||
self.burst_messages.push(self.current_time + offset);
|
||||
}
|
||||
self.burst_messages.sort_unstable();
|
||||
}
|
||||
|
||||
fn next_message_timing(&mut self) -> u64 {
|
||||
if !self.has_pending_burst_messages() {
|
||||
self.start_next_burst();
|
||||
}
|
||||
|
||||
let message_timing = self.burst_messages[self.next_burst_message];
|
||||
self.next_burst_message += 1;
|
||||
message_timing
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RequestHandler for SimpleHiddenServiceModel<'a> {
|
||||
type Out = RouteEvent<'a>;
|
||||
|
||||
#[inline]
|
||||
fn fetch_next(&mut self) -> Option<Self::Out> {
|
||||
let next_timing = self.next_message_timing();
|
||||
match next_timing {
|
||||
currt if currt < self.limit => {
|
||||
self.user_info.update(currt, &mut self.rng);
|
||||
Some((
|
||||
currt,
|
||||
self.user_info.get_selected_vanguard(),
|
||||
self.user_info.get_selected_guard(),
|
||||
Some(self.current_burst_id),
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
src/simplemodel.rs
Normal file
68
src/simplemodel.rs
Normal file
@ -0,0 +1,68 @@
|
||||
//! A simple user model -- It samples messages within a [5, 15min] interval.
|
||||
//!
|
||||
//! Currently does not send the message to any simulated user in particular, and it is one message
|
||||
//! at a time.
|
||||
use crate::usermodel::{RequestHandler, RouteEvent, UserModel, UserModelInfo};
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::SeedableRng;
|
||||
|
||||
const INTERVAL_MAX: u64 = 900;
|
||||
const INTERVAL_MIN: u64 = 300;
|
||||
|
||||
pub struct SimpleSynchronousModel<'a> {
|
||||
/// timestamp of current time, starting at 0.
|
||||
current_time: u64,
|
||||
/// the max value of a timing message
|
||||
limit: u64,
|
||||
rng: SmallRng,
|
||||
die: Uniform<u64>,
|
||||
user_info: UserModelInfo<'a>,
|
||||
}
|
||||
|
||||
/// This simple model uniformly samples a new message to send in the next [300 ... 900] second
|
||||
/// interval.
|
||||
impl<'a> UserModel<'a> for SimpleSynchronousModel<'a> {
|
||||
fn new(_tot_users: u32, _epoch: u32, user_info: UserModelInfo<'a>) -> Self {
|
||||
SimpleSynchronousModel {
|
||||
rng: SmallRng::from_entropy(),
|
||||
die: Uniform::from(INTERVAL_MIN..INTERVAL_MAX),
|
||||
current_time: 0,
|
||||
limit: 0,
|
||||
user_info,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_limit(&mut self, limit: u64) {
|
||||
self.limit = limit;
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleSynchronousModel<'_> {
|
||||
/// We simply increase the current time with the sampled value.
|
||||
fn get_next_message_timing(&mut self) -> u64 {
|
||||
self.current_time += self.die.sample(&mut self.rng);
|
||||
self.current_time
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RequestHandler for SimpleSynchronousModel<'a> {
|
||||
type Out = RouteEvent<'a>;
|
||||
|
||||
#[inline]
|
||||
fn fetch_next(&mut self) -> Option<Self::Out> {
|
||||
let next_timing = self.get_next_message_timing();
|
||||
match next_timing {
|
||||
currt if currt < self.limit => {
|
||||
self.user_info.update(currt, &mut self.rng);
|
||||
Some((
|
||||
currt,
|
||||
self.user_info.get_selected_vanguard(),
|
||||
self.user_info.get_selected_guard(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
340
src/simulation.rs
Normal file
340
src/simulation.rs
Normal file
@ -0,0 +1,340 @@
|
||||
use crate::config::TopologyConfig;
|
||||
use crate::config::PATH_LENGTH;
|
||||
use crate::config::Mixnode;
|
||||
use crate::usermodel::{RequestHandler, RouteEvent, UserModel, UserModelInfo, UserModelIterator};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use rand::prelude::*;
|
||||
use rayon::prelude::*;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
/// Contains information required for running the simulation
|
||||
#[derive(Default)]
|
||||
pub struct Runable {
|
||||
/// The number of users we want to simulate
|
||||
users: u32,
|
||||
/// The Network config
|
||||
configs: Vec<TopologyConfig>,
|
||||
/// The number of virtual days for running the experiment
|
||||
days: u32,
|
||||
/// Does this simulation use persistent guard positions?
|
||||
use_guards: bool,
|
||||
/// Does this simulation use persistent vanguard positions?
|
||||
use_vanguards: bool,
|
||||
/// each topology lifetime -- we assume this to be unique (e.g., 1 day)
|
||||
epoch: u32,
|
||||
/// print human-readable per-route output --- default: false
|
||||
to_console: bool,
|
||||
/// print aggregate simulation summary at the end --- default: false
|
||||
summary: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SimulationSummary {
|
||||
users: u32,
|
||||
total_messages: u64,
|
||||
compromised_messages: u64,
|
||||
users_with_compromise: u64,
|
||||
first_compromise_timestamp: Option<u64>,
|
||||
last_message_timestamp: Option<u64>,
|
||||
}
|
||||
|
||||
impl SimulationSummary {
|
||||
fn for_user() -> Self {
|
||||
SimulationSummary {
|
||||
users: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(mut self, other: Self) -> Self {
|
||||
self.users += other.users;
|
||||
self.total_messages += other.total_messages;
|
||||
self.compromised_messages += other.compromised_messages;
|
||||
self.users_with_compromise += other.users_with_compromise;
|
||||
self.first_compromise_timestamp = min_option(
|
||||
self.first_compromise_timestamp,
|
||||
other.first_compromise_timestamp,
|
||||
);
|
||||
self.last_message_timestamp =
|
||||
max_option(self.last_message_timestamp, other.last_message_timestamp);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn min_option(a: Option<u64>, b: Option<u64>) -> Option<u64> {
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => Some(a.min(b)),
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_option(a: Option<u64>, b: Option<u64>) -> Option<u64> {
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => Some(a.max(b)),
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Runable {
|
||||
/// Creates a new simulation to run.
|
||||
pub fn new(users: u32, configs: Vec<TopologyConfig>, days: u32, epoch: u32) -> Self {
|
||||
Runable {
|
||||
configs,
|
||||
users,
|
||||
days,
|
||||
epoch,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
/// Do we enable persistent guards for this simulation?
|
||||
pub fn with_guards(&mut self) -> &mut Self {
|
||||
self.use_guards = true;
|
||||
self
|
||||
}
|
||||
/// Do we enable persistent vanguards for this simulation?
|
||||
pub fn with_vanguards(&mut self) -> &mut Self {
|
||||
self.use_vanguards = true;
|
||||
self
|
||||
}
|
||||
/// Do we print human-readable route results?
|
||||
pub fn with_console(&mut self) -> &mut Self {
|
||||
self.to_console = true;
|
||||
self
|
||||
}
|
||||
/// Do we print an aggregate summary at the end?
|
||||
pub fn with_summary(&mut self) -> &mut Self {
|
||||
self.summary = true;
|
||||
self
|
||||
}
|
||||
/// Get a random path from the right mixnet configuration.
|
||||
///
|
||||
/// A vanguard and guard are optionally given. They are persistent mixnet nodes chosen to
|
||||
/// change as little as possible.
|
||||
#[inline]
|
||||
pub fn sample_path<'a>(
|
||||
&'a self,
|
||||
message_timing: u64,
|
||||
rng: &mut ThreadRng,
|
||||
vanguard: Option<&'a Mixnode>,
|
||||
guard: Option<&'a Mixnode>,
|
||||
) -> IntoIter<&'a Mixnode> {
|
||||
self.configs[(message_timing / self.epoch as u64) as usize]
|
||||
.sample_path(rng, vanguard, guard)
|
||||
}
|
||||
|
||||
/// Check whether the three mixnode in path are compromised.
|
||||
/// Returns true if they are, false otherwise.
|
||||
pub fn is_path_malicious(&self, path: &[&Mixnode]) -> bool {
|
||||
let mut mal_mix = 0;
|
||||
for i in 0..PATH_LENGTH {
|
||||
if path[i as usize].is_malicious {
|
||||
mal_mix += 1;
|
||||
}
|
||||
}
|
||||
mal_mix == PATH_LENGTH
|
||||
}
|
||||
|
||||
/// Format the message's sending time as a naive time
|
||||
#[inline]
|
||||
fn format_message_timing(timing: u64) -> String {
|
||||
let dt = Utc
|
||||
.timestamp_opt(timing as i64, 0)
|
||||
.single()
|
||||
.expect("message timing should be a valid timestamp");
|
||||
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn days_to_timestamp(&self) -> u64 {
|
||||
u64::from(self.days) * 24 * 60 * 60
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn log_stdout(
|
||||
&self,
|
||||
user: u32,
|
||||
strdate: &str,
|
||||
path: IntoIter<&Mixnode>,
|
||||
is_malicious: bool,
|
||||
requestid: Option<u128>,
|
||||
) {
|
||||
if !self.to_console {
|
||||
return;
|
||||
}
|
||||
|
||||
let path_ids: Vec<String> = path.map(|hop| hop.mixid.to_string()).collect();
|
||||
let path = path_ids.join(",");
|
||||
|
||||
if let Some(rid) = requestid {
|
||||
println!("{strdate} {user} {rid} {path} {is_malicious}");
|
||||
} else {
|
||||
println!("{strdate} {user} {path} {is_malicious}");
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn log_summary(
|
||||
&self,
|
||||
summary: &mut SimulationSummary,
|
||||
message_timing: u64,
|
||||
is_malicious: bool,
|
||||
) {
|
||||
summary.total_messages += 1;
|
||||
summary.last_message_timestamp = Some(
|
||||
summary
|
||||
.last_message_timestamp
|
||||
.map_or(message_timing, |last| last.max(message_timing)),
|
||||
);
|
||||
|
||||
if is_malicious {
|
||||
summary.compromised_messages += 1;
|
||||
if summary.first_compromise_timestamp.is_none() {
|
||||
summary.users_with_compromise = 1;
|
||||
summary.first_compromise_timestamp = Some(message_timing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_summary(&self, summary: &SimulationSummary) {
|
||||
println!("route_summary");
|
||||
println!("users={}", summary.users);
|
||||
println!("days={}", self.days);
|
||||
println!("epoch_seconds={}", self.epoch);
|
||||
println!("topologies_loaded={}", self.configs.len());
|
||||
println!("guards_enabled={}", self.use_guards);
|
||||
println!("vanguards_enabled={}", self.use_vanguards);
|
||||
println!("total_messages={}", summary.total_messages);
|
||||
println!("compromised_messages={}", summary.compromised_messages);
|
||||
println!(
|
||||
"percentage_messages_compromised={:.6}",
|
||||
percentage(summary.compromised_messages, summary.total_messages)
|
||||
);
|
||||
println!(
|
||||
"users_with_at_least_one_compromised_message={}",
|
||||
summary.users_with_compromise
|
||||
);
|
||||
println!(
|
||||
"users_without_compromised_message={}",
|
||||
summary
|
||||
.users
|
||||
.saturating_sub(summary.users_with_compromise as u32)
|
||||
);
|
||||
println!(
|
||||
"percentage_users_with_at_least_one_compromised_message={:.6}",
|
||||
percentage(summary.users_with_compromise, u64::from(summary.users))
|
||||
);
|
||||
println!(
|
||||
"first_compromise_timestamp_seconds={}",
|
||||
optional_u64(summary.first_compromise_timestamp)
|
||||
);
|
||||
println!(
|
||||
"last_message_timestamp_seconds={}",
|
||||
optional_u64(summary.last_message_timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize user models
|
||||
pub fn init<'a, T>(&'a self) -> Vec<UserModelIterator<T>>
|
||||
where
|
||||
T: UserModel<'a>,
|
||||
{
|
||||
(0..self.users)
|
||||
.map(|user| {
|
||||
let model = T::new(
|
||||
self.users,
|
||||
self.epoch,
|
||||
UserModelInfo::new(
|
||||
user,
|
||||
&self.configs,
|
||||
self.epoch,
|
||||
self.use_guards,
|
||||
self.use_vanguards,
|
||||
),
|
||||
);
|
||||
UserModelIterator(model)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Run the simulation -- this function should output
|
||||
/// route taken for each user each time the user requires to send
|
||||
/// a message, which depends of the user model through time.
|
||||
pub fn run<'a, T>(&'a self, mut usermodels: Vec<UserModelIterator<T>>)
|
||||
where
|
||||
T: UserModel<'a> + Send,
|
||||
T: RequestHandler<Out = RouteEvent<'a>>,
|
||||
{
|
||||
// for_each should block until they all completed
|
||||
let summary = (0..self.users)
|
||||
.into_par_iter()
|
||||
.zip(&mut usermodels)
|
||||
.map(|(user, mut usermodel)| {
|
||||
let mut rng = thread_rng();
|
||||
let mut user_summary = SimulationSummary::for_user();
|
||||
// move this in the init part?
|
||||
usermodel.set_limit(self.days_to_timestamp());
|
||||
//let userinfo = &mut userinfos[user as usize];
|
||||
for (message_timing, vanguard, guard, requestid) in &mut usermodel {
|
||||
// do we need to update userinfo relative to the current timing?
|
||||
let path = self.sample_path(message_timing, &mut rng, vanguard, guard);
|
||||
let strdate = Runable::format_message_timing(message_timing);
|
||||
// write out the path for this message_timing
|
||||
let is_malicious = self.is_path_malicious(path.as_slice());
|
||||
self.log_stdout(user, &strdate, path, is_malicious, requestid);
|
||||
self.log_summary(&mut user_summary, message_timing, is_malicious);
|
||||
}
|
||||
user_summary
|
||||
})
|
||||
.reduce(SimulationSummary::default, SimulationSummary::merge);
|
||||
|
||||
if self.summary {
|
||||
self.print_summary(&summary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn percentage(numerator: u64, denominator: u64) -> f64 {
|
||||
if denominator == 0 {
|
||||
0.0
|
||||
} else {
|
||||
numerator as f64 / denominator as f64 * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_u64(value: Option<u64>) -> String {
|
||||
value.map_or_else(|| "none".to_string(), |value| value.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_formatting() {
|
||||
let mut timing = 60 * 11;
|
||||
let mut strdate = Runable::format_message_timing(timing);
|
||||
assert_eq!(strdate, "1970-01-01 00:11:00");
|
||||
timing = timing + 1;
|
||||
strdate = Runable::format_message_timing(timing);
|
||||
assert_eq!(strdate, "1970-01-01 00:11:01");
|
||||
timing = timing + 25 * 60 * 60;
|
||||
strdate = Runable::format_message_timing(timing);
|
||||
assert_eq!(strdate, "1970-01-02 01:11:01");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary_counts_user_once() {
|
||||
let runner = Runable::default();
|
||||
let mut summary = SimulationSummary::for_user();
|
||||
|
||||
runner.log_summary(&mut summary, 10, false);
|
||||
runner.log_summary(&mut summary, 20, true);
|
||||
runner.log_summary(&mut summary, 30, true);
|
||||
|
||||
assert_eq!(summary.users, 1);
|
||||
assert_eq!(summary.total_messages, 3);
|
||||
assert_eq!(summary.compromised_messages, 2);
|
||||
assert_eq!(summary.users_with_compromise, 1);
|
||||
assert_eq!(summary.first_compromise_timestamp, Some(20));
|
||||
assert_eq!(summary.last_message_timestamp, Some(30));
|
||||
}
|
||||
203
src/usermodel.rs
Normal file
203
src/usermodel.rs
Normal file
@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Simple trait definition for any concrete user model
|
||||
*/
|
||||
use crate::config::TopologyConfig;
|
||||
use crate::config::{GUARDS_LAYER, GUARDS_SAMPLE_SIZE, GUARDS_SAMPLE_SIZE_EXTEND};
|
||||
use crate::config::{VANGUARDS_LAYER, VANGUARDS_SAMPLE_SIZE, VANGUARDS_SAMPLE_SIZE_EXTEND};
|
||||
use crate::config::Mixnode;
|
||||
use rand::prelude::*;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub trait RequestHandler {
|
||||
type Out;
|
||||
|
||||
fn fetch_next(&mut self) -> Option<Self::Out>;
|
||||
}
|
||||
|
||||
pub type RouteEvent<'a> = (u64, Option<&'a Mixnode>, Option<&'a Mixnode>, Option<u128>);
|
||||
|
||||
pub trait UserModel<'a> {
|
||||
fn new(tot_users: u32, epoch: u32, user_info: UserModelInfo<'a>) -> Self;
|
||||
fn set_limit(&mut self, limit: u64);
|
||||
}
|
||||
|
||||
// Wrapper that gives every user model a common Iterator implementation.
|
||||
pub struct UserModelIterator<T>(pub T);
|
||||
|
||||
impl<T> Deref for UserModelIterator<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for UserModelIterator<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for UserModelIterator<T>
|
||||
where
|
||||
T: UserModel<'a> + RequestHandler<Out = RouteEvent<'a>>,
|
||||
{
|
||||
type Item = RouteEvent<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.fetch_next()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserModelInfo<'a> {
|
||||
/// Mixnet topology
|
||||
topos: &'a [TopologyConfig],
|
||||
/// Vanguards information
|
||||
vanguards: Option<Vec<&'a Mixnode>>,
|
||||
/// The vanguard we're currently using
|
||||
selected_vanguard: Option<&'a Mixnode>,
|
||||
/// Guards information
|
||||
guards: Option<Vec<&'a Mixnode>>,
|
||||
/// The guard we're currently using
|
||||
selected_guard: Option<&'a Mixnode>,
|
||||
/// epoch length in seconds
|
||||
epoch: u32,
|
||||
curr_idx: usize,
|
||||
}
|
||||
|
||||
impl<'a> UserModelInfo<'a> {
|
||||
pub fn new(
|
||||
_userid: u32,
|
||||
topos: &'a [TopologyConfig],
|
||||
epoch: u32,
|
||||
use_guards: bool,
|
||||
use_vanguards: bool,
|
||||
) -> Self {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut vanguards: Option<Vec<&'a Mixnode>> = None;
|
||||
let mut selected_vanguard: Option<&'a Mixnode> = None;
|
||||
let mut guards: Option<Vec<&'a Mixnode>> = None;
|
||||
let mut selected_guard: Option<&'a Mixnode> = None;
|
||||
if use_vanguards {
|
||||
vanguards = Some(
|
||||
topos[0]
|
||||
.sample_layer(VANGUARDS_LAYER, VANGUARDS_SAMPLE_SIZE, &mut rng)
|
||||
.collect(),
|
||||
);
|
||||
selected_vanguard = Some(vanguards.as_ref().unwrap()[0]);
|
||||
}
|
||||
if use_guards {
|
||||
guards = Some(
|
||||
topos[0]
|
||||
.sample_layer(GUARDS_LAYER, GUARDS_SAMPLE_SIZE, &mut rng)
|
||||
.collect(),
|
||||
);
|
||||
selected_guard = Some(guards.as_ref().unwrap()[0]);
|
||||
}
|
||||
UserModelInfo {
|
||||
topos,
|
||||
vanguards,
|
||||
selected_vanguard,
|
||||
guards,
|
||||
selected_guard,
|
||||
epoch,
|
||||
curr_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_selected_vanguard(&self) -> Option<&'a Mixnode> {
|
||||
self.selected_vanguard
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_selected_guard(&self) -> Option<&'a Mixnode> {
|
||||
self.selected_guard
|
||||
}
|
||||
|
||||
/// Potentially changes this user's persistent vanguard and guard.
|
||||
#[inline]
|
||||
pub fn update<R: Rng + ?Sized>(&mut self, message_timing: u64, rng: &mut R) {
|
||||
let idx = (message_timing / self.epoch as u64) as usize;
|
||||
if idx > self.curr_idx {
|
||||
self.curr_idx = idx;
|
||||
update_persistent_selection(
|
||||
self.topos,
|
||||
self.curr_idx,
|
||||
VANGUARDS_LAYER,
|
||||
VANGUARDS_SAMPLE_SIZE_EXTEND,
|
||||
&mut self.vanguards,
|
||||
&mut self.selected_vanguard,
|
||||
rng,
|
||||
);
|
||||
update_persistent_selection(
|
||||
self.topos,
|
||||
self.curr_idx,
|
||||
GUARDS_LAYER,
|
||||
GUARDS_SAMPLE_SIZE_EXTEND,
|
||||
&mut self.guards,
|
||||
&mut self.selected_guard,
|
||||
rng,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_persistent_selection<'a, R: Rng + ?Sized>(
|
||||
topos: &'a [TopologyConfig],
|
||||
topoidx: usize,
|
||||
layer: usize,
|
||||
extend_size: usize,
|
||||
candidates: &mut Option<Vec<&'a Mixnode>>,
|
||||
selected: &mut Option<&'a Mixnode>,
|
||||
rng: &mut R,
|
||||
) {
|
||||
let Some(candidates) = candidates else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(candidate) = candidates
|
||||
.iter()
|
||||
.find(|candidate| topos[topoidx].has_mix_in_layer(layer, candidate.mixid))
|
||||
{
|
||||
*selected = Some(*candidate);
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate_idx = candidates.len();
|
||||
candidates.extend(topos[topoidx].sample_layer(layer, extend_size, rng));
|
||||
if candidates.len() <= candidate_idx {
|
||||
panic!(
|
||||
"Did the persistent candidate set properly extend? len: {}",
|
||||
candidates.len()
|
||||
);
|
||||
}
|
||||
*selected = Some(candidates[candidate_idx]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_model_info_can_use_guards_without_vanguards() {
|
||||
let configs = crate::config::load("testfiles/single_layout/1000_137_Random_BP_layout.csv");
|
||||
let user_info = UserModelInfo::new(0, &configs, 86401, true, false);
|
||||
|
||||
assert!(user_info.get_selected_guard().is_some());
|
||||
assert!(user_info.get_selected_vanguard().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_model_info_can_use_guards_and_vanguards() {
|
||||
let configs = crate::config::load("testfiles/single_layout/1000_137_Random_BP_layout.csv");
|
||||
let user_info = UserModelInfo::new(0, &configs, 86401, true, true);
|
||||
|
||||
assert!(user_info.get_selected_guard().is_some());
|
||||
assert!(user_info.get_selected_vanguard().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_model_info_can_disable_guards_and_vanguards() {
|
||||
let configs = crate::config::load("testfiles/single_layout/1000_137_Random_BP_layout.csv");
|
||||
let user_info = UserModelInfo::new(0, &configs, 86401, false, false);
|
||||
|
||||
assert!(user_info.get_selected_guard().is_none());
|
||||
assert!(user_info.get_selected_vanguard().is_none());
|
||||
}
|
||||
1111
testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.03_layout.csv
Normal file
1111
testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.03_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1111
testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.15_layout.csv
Normal file
1111
testfiles/layout_data/bow_tie/dynamic_hybrid_steady_0.15_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1111
testfiles/layout_data/bow_tie/static_hybrid_layout.csv
Normal file
1111
testfiles/layout_data/bow_tie/static_hybrid_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1033
testfiles/layout_data/bw_rand/bw_rand_layout.csv
Normal file
1033
testfiles/layout_data/bw_rand/bw_rand_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1033
testfiles/layout_data/bw_rand/dynamic_bw_rand_0.03_layout.csv
Normal file
1033
testfiles/layout_data/bw_rand/dynamic_bw_rand_0.03_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1033
testfiles/layout_data/bw_rand/dynamic_bw_rand_0.15_layout.csv
Normal file
1033
testfiles/layout_data/bw_rand/dynamic_bw_rand_0.15_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1195
testfiles/layout_data/rand_bp/dynamic_rand_lp_0.03_layout.csv
Normal file
1195
testfiles/layout_data/rand_bp/dynamic_rand_lp_0.03_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1195
testfiles/layout_data/rand_bp/dynamic_rand_lp_0.15_layout.csv
Normal file
1195
testfiles/layout_data/rand_bp/dynamic_rand_lp_0.15_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1195
testfiles/layout_data/rand_bp/rand_lp_layout.csv
Normal file
1195
testfiles/layout_data/rand_bp/rand_lp_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1193
testfiles/layout_data/rand_rand/dynamic_rand_rand_0.03_layout.csv
Normal file
1193
testfiles/layout_data/rand_rand/dynamic_rand_rand_0.03_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1193
testfiles/layout_data/rand_rand/dynamic_rand_rand_0.15_layout.csv
Normal file
1193
testfiles/layout_data/rand_rand/dynamic_rand_rand_0.15_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1193
testfiles/layout_data/rand_rand/rand_rand_layout.csv
Normal file
1193
testfiles/layout_data/rand_rand/rand_rand_layout.csv
Normal file
File diff suppressed because one or more lines are too long
1138
testfiles/single_layout/1000_137_Random_BP_layout.csv
Normal file
1138
testfiles/single_layout/1000_137_Random_BP_layout.csv
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user