Refracting Light v2: formal specification¶
Revision: 1, 6 October 2026
Reference code: refracting_light_128.py (digest, folded), unc_qtl_unfolded.py (state_lanes, digest, unfolded)
Status: Experimental. No independent cryptanalysis. Not a replacement for SHA-256 or BLAKE3. The name “Refracting Light” is a label and does not describe a physical or quantum process.
Provenance check (fold trail)¶
On 6 Oct 2026 the current code was re-run against the recorded vectors in refracting-light-test-vectors.json and refracting-light-single-timestamp-hash.json:
Check |
Result |
|---|---|
6 message vectors, folded-128, |
all match |
Same 6 vectors, folded-128, |
all match |
Same 6 vectors, unfolded-512, |
all match |
Nonce vector (nonce 1566, 12 leading zero bits) |
matches |
140-byte timestamp record, unfolded root |
matches |
2,000 random messages, 0–39 bytes: both |
0 mismatches |
So the fold is real code that produces the recorded outputs. One bookkeeping gap: exp-1-folded-128-hunt.json records source hashes 3c021b34… / 70c7fbb5…, and the current files hash to 333367f5… / 794fdf0d…. The files were edited after that run (mtime 16:30 vs. 16:21). The recorded vectors still reproduce, so the round function looks unchanged, but this has not been proven by a diff.
Notation¶
All arithmetic is on unbounded signed integers unless reduced with
mod 2^64.rotl_w(x, n)rotates a w-bit value left byn mod w.divmod(a, 2^64)uses floor division:q = floor(a / 2^64),r = a − q·2^64, so0 ≤ r < 2^64. In principlezcan be negative, because the termd·(i+ℓ+1)can be as low as −16·(i+4). In practice the other terms are non-negative and dominate: in 556,672 lane-steps over 300 random messages,refracting_light_spec_check.pyrecorded 0 negativezand 0 negativeq. Ports should still use floor division so they stay correct in the edge case.⊕is XOR.‖is concatenation. Byte strings are big-endian unless stated otherwise.
1. Clamp (“outward”)¶
outward(x) = x − 16 if x ≤ 0
x + 15 if x > 0
This is an injective map from ℤ to ℤ \ {−15, …, 15}. Message bits become digits outward(0) = −16 and outward(1) = 16.
2. Record encoding (RAID-6-style shards)¶
For a message M of n bytes:
s = ceil(n / 4)
M' = M ‖ 0x00^(4s − n)
D_k = M'[k·s : (k+1)·s] k = 0..3
P[j] = D_0[j] ⊕ D_1[j] ⊕ D_2[j] ⊕ D_3[j]
Q[j] = 1·D_0[j] ⊕ 2·D_1[j] ⊕ 3·D_2[j] ⊕ 4·D_3[j] (multiply in GF(2^8), polynomial 0x11d)
record(M) = BE64(n) ‖ D_0 ‖ D_1 ‖ D_2 ‖ D_3 ‖ P ‖ Q
Its length is 8 + 6s bytes. The length prefix makes the encoding injective, so distinct messages give distinct records. P and Q are linear functions of M. They add redundancy, not secrecy.
3. State¶
There are four lanes, ℓ ∈ {0,1,2,3}. Each lane holds two 64-bit words x_ℓ and v_ℓ, for 512 bits in total.
x = (0, 1, 2, 3)
v = (1, 3, 5, 7)
i = 0 global step counter
4. Step function¶
The record is read bit by bit, most significant bit of each byte first. For each bit b:
d = outward(b) ∈ {−16, +16}
for each lane ℓ, with neighbour ν = (ℓ+1) mod 4, using the OLD state for every lane:
z = 257·x_ℓ + 17·v_ℓ + d·(i + ℓ + 1) + rotl_64(x_ν, 11 + 7ℓ)
(q, r) = divmod(outward(z), 2^64)
v' = (65537·v_ℓ + q + d + i + ℓ + 1 + v_ν) mod 2^64
v' = rotl_64(v', 13 + 8ℓ) ⊕ r
x'_ℓ = r ⊕ rotl_64(v', ((i + 11ℓ) mod 63) + 1)
v'_ℓ = v'
(x, v) ← (x', v') update all four lanes at once
i ← i + 1
A message of n bytes takes 8·(8 + 6·ceil(n/4)) steps. There is no finalization stage: the last message bit enters in the last step.
5. Output¶
L_ℓ = (x_ℓ << 64) | v_ℓ 128-bit lane
Unfolded = L_0 ‖ L_1 ‖ L_2 ‖ L_3 512 bits, 128 hex chars
Folded = rotl_128(L_0,0) ⊕ rotl_128(L_1,29) ⊕ rotl_128(L_2,61) ⊕ rotl_128(L_3,97) 128 bits, 32 hex chars
The fold is a surjective GF(2)-linear map from 512 bits to 128 bits, so its kernel has dimension 384 (PROVEN). Two different final states give the same folded digest exactly when their XOR lies in that kernel. Nobody has checked whether message pairs can steer the state into the kernel. That is the main open question for the folded view.
6. What is and is not established¶
Statement |
Status |
|---|---|
Spec above matches the reference code on all recorded vectors |
MEASURED: |
Carry |
PROVEN bound on z; the |
Record encoding is injective |
PROVEN (length prefix + data shards) |
Fold is linear with a 384-dimensional kernel |
PROVEN |
No full collisions in the bounded hunts run so far |
MEASURED, and expected for any 128-bit function at those sizes. This is not evidence of security. |
Collision resistance, preimage resistance, PRF behaviour |
UNKNOWN; requires Phase 3 analysis |
Suitable for signatures, Merkle roots, addresses |
NO, not until external cryptanalysis exists |
7. Porting notes¶
Use signed 128-bit or larger intermediates:
zcan be negative and can exceed 2^64 (257·x alone needs 73 bits).Implement floor
divmod, not truncation.Rotation amounts are taken mod the width. A rotation by 0 must be the identity (no
x >> 64undefined behaviour in C).Update all four lanes from the old state, not in place.
8. Version 3 (6 October 2026): finalization stage¶
v3 is identical to v2 except for one rule added between section 4 and section 5:
after the last record bit, run 8 more steps with input bit b = 0 (d = −16), i continuing
Equivalently, v3(M) = v2 core over record(M) ‖ 0x00, then the fold. The record is length-prefixed, so this stays injective.
Why: In v2, a difference entering in the last step reached only about 92 of 512 state bits, and its folded weight could be as low as 21–24 bits (a random value gives about 39). finalization_sizing.py showed full 512-bit mixing after 2 blank steps; 8 gives a 4× margin. Cost: 8 steps, against at least 112 per message.
v3 fact |
Status |
|---|---|
v3 with 0 final steps equals v2 on all recorded vectors |
MEASURED |
v3 equals the independent spec implementation run on |
MEASURED |
Every v3 digest differs from v2 (300 random messages) |
MEASURED |
Vectors |
|
Code |
|
Security |
Still UNKNOWN. Phase 3 must be re-run on v3 |