This repository holds the code, the result files and the data pipelines of the
paper Block-A-Mole: The Sustainability Frontier of Moving-Target Censorship
Resistance. The model results and both figures can be checked and recomputed
offline from what is here. The four data analyses download their public
inputs again and rebuild their outputs. PAPER_MAP.md maps each number and
factual statement of the paper to a file, a key and a command.
simulations/code/ exact solver (exact.py) and exact experiments R1-R13 (exact_experiments.py),
event-driven simulator (rotation_game.py, censor_models.py, theory.py) and
its experiments E1-E10 (run_experiments.py), extra_values.py, figure style
simulations/results/ the 23 result files behind the paper's model results
paper/figures/make_figures.py draws Figures 1 and 2 from the result files and checks them
figures/ frontier.pdf (Figure 1) and phase.pdf (Figure 2)
data/burn_units/ which names China and Iran block and Russia orders blocked (Table 1,
Sections 1, 3, 6.1, 7, 8 and 9, Appendix D)
data/snowflake/ Snowflake's rendezvous names (Sections 1, 2, 3, 6.2 and 9, Appendix D)
data/literature/ published parameter values with verbatim quotes (Sections 1, 2, 3, 5.3, 5.4
and 6.2, Appendix D)
data/minting/ minting costs and exposure at mint (Sections 1, 3, 5.4, 6.3, 7 and 8,
Appendix D)
PAPER_MAP.md where each number of the paper comes from
LICENSE licenses of code, results and data
requirements.txt Python packages, pinned to the versions of the paper's runs
Each part has its own documentation.
simulations/README.mddescribes the model code, andsimulations/results/README.mdeach result file, its SHA-256, the paper's numbers in it and how to compare a rerun with it.data/README.mddescribes the layout of the four analyses. In eachdata/<analysis>/,raw/README.mdsays how to download the inputs,derived/README.mddescribes the outputs and prints the paper's numbers from them, andSOURCES.mdlists the sources and their licenses.
- Third-party data. Each
data/<analysis>/raw/holds onlyREADME.md, with the download instructions, and the paper'smanifest.json, which records the URL, retrieval time (2026-10-03, UTC), SHA-256, size and license of each input. Many inputs have no license or one that does not allow redistribution, and some name hosts that Russia's blocking registry lists. The fetch scripts download the 492 inputs (525 MB inraw/) again. - Outputs that would republish third-party data or name individual hosts,
such as per-host OONI tables, tables built from rdsys-admin, IODA records
and daily scores, a registrar's full price list, Chrome's CT log list and the
full text of documentation pages. The analysis scripts write them again
after the downloads, and each
derived/README.mdlists them with the reason they are left out. - Figures that the paper does not use.
make_figures.py --all, the FIG step ofexact_experiments.pyand three of the analyses draw them when run. - The independent implementation behind Appendix B's accuracy check, which Appendix E describes as a separate check outside the artifact.
- The independent reimplementation that recomputed the numbers of the four data analyses (Appendix D).
- Linux or macOS.
exact_experiments.pyruns its Monte Carlo checks with Python'sforkstart method, and the burn-units fetch scripts lock the manifest withfcntl. Windows has neither. The commands below are for a POSIX shell. - Python 3 with NumPy, SciPy, Matplotlib, pandas and requests.
python3 -m pip install -r requirements.txtinstalls the versions of the paper's runs, NumPy 2.2.4, SciPy 1.15.3, Matplotlib 3.10.1, pandas 3.0.6 and requests 2.32.3, as PyPI builds. Other versions are untested. - The paper's runs used Python 3.13.7 on Ubuntu 25.10, with Ubuntu's packages of NumPy (2.2.4+ds-1ubuntu1) and SciPy (1.15.3-1), which use the reference BLAS and LAPACK 3.12.1, of Matplotlib (3.10.1+dfsg1-4) with fontTools 4.55.3 and of requests, and with pandas 3.0.6 from PyPI. The PyPI wheels of NumPy and SciPy bundle OpenBLAS and can change the last digits of the exact results.
git(2.51.0 in the paper's runs) for the burn-units and Snowflake pipelines,pdftotextfrom poppler-utils (25.03.0) for the Snowflake, literature and minting analyses, andpdfinfofrom the same package for the Snowflake analysis. Withoutpdftotext,data/literature/analysis.pyfalls back to thepypdfpackage, which does not reproduce the shipped outputs. Two quotes are not found, and the run exits with status 1.mutoolfrom MuPDF is optional. With it,make_figures.pyalso checks the page size, font sizes and page bounds of each figure.- The byte-for-byte identity of the figures was verified with the Matplotlib build of the paper's runs, the Ubuntu package above. Other builds, such as the PyPI wheel of the same version, can embed different bytes while all checks pass.
These commands run from the repository root and need no network access. The
first three take under a minute in all, and the rerun of the exact suite
about five minutes. simulations/results/README.md gives the expected output
of each.
The result files record the SHA-256 of the solver code, and R1 records the SHA-256 of the ten simulation files it compared. This checks both records.
python3 - <<'EOF'
import hashlib, json, os
res = "simulations/results"
sha = lambda p: hashlib.sha256(open(p, "rb").read()).hexdigest()
code = {f: sha(f"simulations/code/{f}") for f in ("exact.py", "exact_experiments.py")}
bad = [n for n in sorted(os.listdir(res)) if n.startswith("R") and n.endswith(".json")
and json.load(open(f"{res}/{n}"))["_meta"]["code_sha256"] != code]
inputs = json.load(open(f"{res}/R1_exact_validation.json"))["inputs"]
bad += [n for n, v in inputs.items()
if v != {"sha256": sha(f"{res}/{n}"), "bytes": os.path.getsize(f"{res}/{n}")}]
print("mismatch:", bad if bad else "none (13 R files match the code, R1 matches the 10 E files)")
EOFIt prints mismatch: none (13 R files match the code, R1 matches the 10 E files). The sha256sum -c block in simulations/results/README.md checks
the 23 result files, the two solver files and the two figures, and each
data/<analysis>/derived/README.md has one for its shipped outputs.
Redraw both figures into figures_check/ and compare them with the shipped
ones.
python3 paper/figures/make_figures.py --outdir figures_check
cmp figures/frontier.pdf figures_check/frontier.pdf
cmp figures/phase.pdf figures_check/phase.pdfThe script prints 85 checks of the plotted values and ends with
all checks passed. cmp prints nothing when the files are identical.
Run the solver's self-test, which prints the accuracy figures of Appendix B, and print the values that the paper computes outside the result files.
cd simulations/code
python3 exact.py
python3 extra_values.py
cd ../..exact.py ends with self-test passed. extra_values.py prints the values
of Figure 2's description, Section 5.3, Section 7 and Appendices B and C that
no result file stores, and it recomputes the per-name discovery values of
Appendix C. Its timing line depends on the machine.
Rerun the exact suite in a copy, so that the shipped result files stay
unchanged, and compare the copy with them as simulations/results/README.md
describes under "Rerun and compare".
mkdir ../rerun && cp -r simulations ../rerun/
(cd ../rerun/simulations/code && python3 exact_experiments.py)The exact suite takes about five minutes on a 20-core machine. Its Monte
Carlo checks use one process per core, so it takes longer on fewer cores. To
rerun the simulations too, run
(cd ../rerun/simulations/code && python3 run_experiments.py && python3 exact_experiments.py)
instead, which takes about one hour more on one core. A rerun of the
simulations changes _meta.runtime_s in their files and therefore the input
hashes that R1 records, so never copy rerun files over the shipped ones.
Each analysis runs in its own directory, data/<analysis>/, in five steps.
- Keep the paper's manifest with
cp raw/manifest.json manifest.paper.json. The fetch scripts replace the manifest entry of every file they download. - Download the inputs. This step needs network access.
- Compare the downloads with
manifest.paper.json, with the snippet inraw/README.md. Pinned and archived inputs match, while live sources can differ, as eachraw/README.mdlists. - Rebuild the outputs offline with
analysis.py. - Compare the outputs with the shipped files, with the check in
derived/README.md.
| analysis | download (step 2) | rebuild (step 4) | time and size in the paper's run |
|---|---|---|---|
burn_units |
python3 fetch.py <step> for each step, after setting BURN_UNITS_SCRATCH and pinning gfwlist, as listed under "Rebuild raw/" in raw/README.md |
python3 analysis.py |
GFWatch lookups take 13 minutes, the registry steps about 6 minutes and each other step a few minutes. 1.4 GB transferred and 423 MB kept. Rebuild about 2 minutes and 0.6 GB of memory |
snowflake |
python3 fetch_raw.py --paper-commits --only collector tormetrics ioda gitlab lit |
python3 analysis.py --no-fig |
download about 7 minutes, 30.7 MB. Rebuild under a minute |
literature |
python3 fetch_sources.py |
python3 analysis.py --out derived-rerun |
download a little over a minute plus transfer time, 49.9 MB. Rebuild 30 to 40 s |
minting |
python3 fetch.py |
python3 analysis.py |
download at least 8 minutes because of the spacing of the requests, 21.6 MB. Rebuild about 10 s |
analysis.py overwrites the shipped files in derived/, except with
--out DIR in the literature and Snowflake analyses. To keep the shipped
files, copy them first with cp -r derived derived.paper. Each
raw/README.md also describes the pinned versions, the manual route for each
source, what a new download cannot reproduce and what happens when an input
is missing. Each derived/README.md says how its outputs differ from the
paper's when the inputs differ.
The times below are from the reference workstation of Appendix E, a 20-core Linux machine.
| step | time |
|---|---|
python3 exact.py |
about 1 s |
python3 extra_values.py |
about 3 s |
python3 paper/figures/make_figures.py --outdir figures_check |
under a minute |
python3 exact_experiments.py (R1 to R13) |
302 s of wall time on 20 cores |
python3 run_experiments.py (E1 to E10) |
about one hour on one core (3,356 s) |
the four rebuilds of data/ |
about 4 minutes in all |
- The repository makes no new measurements. The fetch scripts contact only public dataset APIs, file hosts, publishers and documentation sites. They never resolve, probe or connect to a blocked name, a platform tenant, a proxy, a broker or a domain front, and they use no account, API key or approval. Requests to one host are paced at least 1.5 to 3 s apart, and the minting analysis spaces its crt.sh lookups 20 s apart.
- No human subjects are involved. The Tor and Snowflake statistics are published aggregates. Some downloaded inputs name people, for example the commenters of public GitHub and GitLab issue threads (user names), the authors of the four Snowflake commit patches (names and e-mail addresses) and the contributors that Tor's release notes credit, a few with e-mail addresses. The analyses identify issue threads by URL and date, comments by position and time, and commits by hash, never by author, and no downloaded input is redistributed.
- Russia's blocking registry lists the names, URLs and addresses under
blocking orders, some of them for illegal content. Read
data/burn_units/raw/README.mdbefore running the burn-units fetch stepszi,zi_historyandantifilter. They stream the registry files, keep only host names under the 19 platform suffixes, with their decision dates for the registry dumps, record the hashes of the full files and delete them. Never open, resolve or publish the full files or the extracts. The shipped files report registry content only as counts and dates per platform. - Do not redistribute the downloads. Their licenses are in each
SOURCES.mdandraw/manifest.json.
LICENSE gives the terms. The code is under the MIT License. The result
files, figures, derived data, manifests and documentation are under CC BY 4.0,
except the eight files derived from OONI data, which are under CC BY-NC-SA
4.0 as OONI's license requires. Quoted passages of third-party sources keep
their sources' rights.
PAPER_MAP.md follows the paper section by section. For each number or
statement it names the file, the key or column and the command that prints
it, most of them from the shipped files without any download.