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, refracting_light_128.digest

all match

Same 6 vectors, folded-128, unc_qtl_folded.digest

all match

Same 6 vectors, unfolded-512, unc_qtl_unfolded.digest

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 record() encoders and both folded digests agree

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 by n mod w.

  • divmod(a, 2^64) uses floor division: q = floor(a / 2^64), r = a − q·2^64, so 0 ≤ r < 2^64. In principle z can be negative, because the term d·(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.py recorded 0 negative z and 0 negative q. 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: refracting_light_spec_check.py, an independent re-implementation, 6 Oct 2026

Carry q is small: 0 ≤ q ≤ ~275, because z < 275·2^64

PROVEN bound on z; the q ≥ 0 side is MEASURED. Only about 9 bits flow from the high part of z into v each step, a structural property to examine in Phase 3

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

  1. Use signed 128-bit or larger intermediates: z can be negative and can exceed 2^64 (257·x alone needs 73 bits).

  2. Implement floor divmod, not truncation.

  3. Rotation amounts are taken mod the width. A rotation by 0 must be the identity (no x >> 64 undefined behaviour in C).

  4. 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 record ‖ 0x00 (300 random messages)

MEASURED

Every v3 digest differs from v2 (300 random messages)

MEASURED

Vectors

refracting-light-v3-test-vectors.json

Code

refracting_light_v3.py

Security

Still UNKNOWN. Phase 3 must be re-run on v3