A pseudorandom byte generator that treats its internal state as an N-dimensional coordinate space. Each of N axes holds a byte stream partitioned into 256-byte blocks; a keyed function selects which block each axis reads from, and a polynomial combines the N sampled bytes into one output byte, which then drives an output mixing layer.
This is not a cryptographic random number generator. Do not use it to generate keys, nonces, IVs, or anything else where an adversary is in the threat model. The reason is architectural, not a missing feature — see Security status.
Research code, honestly documented. It runs, it is reproducible, its behaviour matches its design, and its statistical properties have been measured against controls rather than asserted.
Four implementation defects and one configuration defect were found in an audit during August 2026. Four are fixed; one is inherent to the architecture and will not be fixed. All are recorded in KNOWN_ISSUES.md, including what the incorrect behaviour was, how it was found, and what it invalidated. That document is worth reading before the source.
seed file
│
├── SHA-512 ──► master entropy
│ ├── BLAKE2b (personalised per axis) ──► 2N axis keys
│ └── BLAKE2b + SHAKE256 ──────────────► output-layer seeds
│
└── SHA-512 ──► RNG seed ──► prime sampling ──► F1 ──► 2N initial values
│
SplitMix64 expansion
│
┌────────────────────────┴───────┐
N axis streams N index streams
└──────────► core layer ◄────────┘
│
for each output byte i:
b = i // 256 (block counter)
s = i % 256 (offset in block)
for each axis d:
blk = SHAKE256(key_d ‖ d ‖ chunk)[4b:4b+4]
v_d = axis_d[blk·256 + s]
core_out = F_final(v_0 … v_{N-1}) + index_byte
│
output mixing layer
(LCG + XOR accumulator + fmix64)
│
byte stream
Block selection is counter-addressable: block b of chunk c on axis d
depends only on (key_d, d, c, b). Any chunk can be produced without producing
the ones before it, and axes advance independently. This is what makes the
generator parallelisable and reproducible. It is also what rules out
cryptographic use.
git clone https://github.com/<user>/HDPRF.git
cd HDPRF
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtPython 3.10–3.12. Numba pins the supported NumPy range, so install from
requirements.txt rather than pulling latest.
python tools/make_data.py # generate data files
python src/main.py -c configs/default_config.json
python src/main.py -c configs/default_config.json --profiledata/example_prime.txt and data/example_seed.bin are not tracked in the
repository — the first is reproducible, and the second is entropy material that
should not be published. tools/make_data.py generates both.
| Key | Meaning |
|---|---|
dimension |
N — number of coordinate axes |
s1_length |
blocks per axis; each block is 256 bytes |
iteration_count |
number of intermediate mixing layers before output |
F1, F_layers.F2… |
polynomial coefficients, powers, constant per layer |
buffer_size |
width of the output-layer XOR accumulator |
decimal |
output length in bytes |
RandomnessExtractor |
none, sha256, sha512, or blake2b |
external_entropy_source |
path to the seed file; omit to use os.urandom |
Polynomial powers below 5 leak the input. See Diagnostic experiments — this is measured, not theoretical. Use odd powers of 5 or above, and an even constant.
python tools/verify_fixes.py # regression checks for the fixed defects
python tools/experiments.py # diagnostic experiments, writes plots to photo/
python tools/comparison.py A B # byte-level identity check between two outputsRun against a C++ implementation of this design (TestU01 1.2.3, WSL, 1330 GB consumed, ~2 h CPU):
Number of statistics: 160
All tests were passed
The pass is clean rather than marginal. Extracting all 254 reported p-values and testing them for uniformity gives Kolmogorov–Smirnov D = 0.0742, p = 0.117 — no evidence of departure from uniform. Three p-values fell below 0.01 and four above 0.99, against roughly 2.5 expected for each. The ten-bin histogram is flat.
Caveat: the exact C++ source used for this run has been lost. The implementation was a port of this design with the same output mixing layer, but it cannot currently be reproduced. It is reported here as history, not as a reproducible claim.
A BigCrush pass does not say which part of the generator earned it. The core layer and the output mixing layer were therefore separated by substituting the core's output, one line at a time, and re-testing.
Through the mixer — the core's output is replaced, the mixer is untouched:
| Core input | SmallCrush (15) | Crush (186) |
|---|---|---|
| Real HDPRF core | pass | 0 failures |
| Single SplitMix64 call | pass | 0 failures |
Weyl sequence i·φ >> 56 |
pass | 2 failures |
Ramp i & 0xFF |
13 failures | — |
| Constant zero | 13 failures | — |
Bypassing the mixer — raw core bytes are emitted directly:
| Raw output | SmallCrush (15) | Crush (186) |
|---|---|---|
| Real HDPRF core | pass | 0 failures, KS p = 0.309 |
| Single SplitMix64 call | pass | 0 failures, KS p = 0.135 |
| Weyl sequence | 15 failures | — |
Three findings:
The core layer is a working generator. Its raw output — with the mixer
removed entirely — passes all 186 Crush statistics with a uniform p-value
distribution. scomp_LinearComp and smarsa_MatrixRank, which target linear
structure, show nothing. The 43-axis architecture is producing randomness, not
decorating it.
The mixer cannot create entropy, but it is a powerful equaliser. Feed it a
constant or a period-256 ramp and it fails 13 of 15 SmallCrush statistics: its
state recursion s_{n+1} = A·s_n + c_n + B is affine, so a periodic c_n
produces a periodic state trajectory that fmix64 cannot rescue. But feed it a
Weyl sequence — which fails 15 of 15 raw — and it launders it into something
failing only 2 of 186. Both survivors are snpair_ClosePairs at t = 2, the
lattice detector; that is the exact fingerprint of a Weyl lattice, and it is all
that remains visible.
The core is statistically indistinguishable from a single SplitMix64 call. Across 186 statistics in both modes, no measurable difference. That is three multiplications and three shifts against 43 random-access gathers. The architecture is not producing a statistical advantage proportional to its cost.
tools/experiments.py writes three plots to photo/:
exp3 — with F1 powers all set to 1, the polynomial degenerates to a plain sum of six primes. Its range collapses to roughly 2^18, bits 19–63 are permanently zero, and chi-square against a uniform 256-bucket histogram is 2.55e6 against an expected 255.
exp4 — Pearson correlation between input and output is 1.000 at power 1 and 0.934 at power 3. The scatter plot at power 1 is a straight line: the output discloses the input directly. Powers of 5 and above scatter properly.
exp5 — differential uniformity is 256x the ideal at power 1 and 25x at power 2, approaching 1 only from power 3 upward.
Three independent measurements, one conclusion: powers below 5 are not usable.
The core makes one memory access per axis per output byte, at an address derived from the key:
v = axis[d][block_id * 256 + offset] # block_id <- SHAKE256(key_d, ...)Key-dependent table indexing is precisely the pattern cache-timing attacks target. Bernstein (2005) and Osvik–Shamir–Tromer (2006) recovered full AES keys from T-table access timing, remotely. Salsa20 and ChaCha20 were designed to avoid data-dependent memory access for this reason — they use only add, rotate and XOR, and are constant-time by construction.
Making HDPRF constant-time would mean reading every axis in full for every output byte: 43 x 262144 bytes per byte produced. That is not a tuning problem; it is a different algorithm.
HC-128 reached the eSTREAM software portfolio with key-dependent tables, so this is not categorically disqualifying — but HC-128's deployment has been minimal, and this is a substantial part of why.
Separately, the N-dimensional structure provides no cryptographic advantage. An
N-tuple of indices is mathematically equivalent to one wider counter; L^N is a
naming space, not a state space, and the entropy of the output is bounded by the
entropy of the seed regardless of N.
The low bit of a sum modulo 256 is the XOR of the operands' low bits, which is affine over GF(2). The ablation study found no trace of this in any statistical test, and that is expected: the 43 operands are themselves pseudorandom and unknown, so "XOR of 43 unknown pseudorandom bits" is still pseudorandom to a test that does not know them.
This makes the affine structure a cryptanalytic property, not a statistical one. An attacker who knows the coefficients can set up linear equations over the key; a statistical battery cannot. The two should not be conflated, and a clean BigCrush result says nothing about the first.
| Implementation | Throughput |
|---|---|
| C++ / OpenMP, multi-threaded | ~160 MB/s |
Python / Numba prange |
~1.1 MB/s |
For scale, ChaCha20 reaches 1–3 GB/s single-threaded. The cost here is N random-access gathers per output byte, and there is no plausible path to closing that gap.
An earlier benchmark chart compared HDPRF against ChaCha20, AES-128-CTR and Mersenne Twister. It should not be relied on. The thread count for HDPRF and the reference implementations used for the comparators were not recorded, and the ChaCha20 figure (~364 MB/s) is far below what an optimised implementation achieves — suggesting an unvectorised reference build. Comparing a multi-threaded OpenMP generator against that is not a meaningful comparison.
- Reproducible parallel simulation. Multi-dimensional addressing means a
tuple like
(particle, timestep, event_type)maps directly to a coordinate without being flattened into a counter first, and any dimension can be re-run independently. Philox and Threefry offer addressability, but with a flat counter. - High-dimensional box-counting. The
L^Npartition is a grid, and the machinery to build and query it in parallel already exists here.
Both are engineering conveniences rather than novel results. Neither is cryptography.
src/main.py pipeline, config handling, jump table manager
src/kernels.py Numba kernels, in a separate module so that
cache=True keys them under one stable name
configs/default_config.json reference configuration
tools/make_data.py generates the prime and seed files
tools/verify_fixes.py regression checks for the fixed defects
tools/experiments.py diagnostic experiments (3, 4, 5)
tools/comparison.py byte-level identity check between two outputs
photo/ diagnostic plots
KNOWN_ISSUES.md defect history and current limitations
MIT.