Source: ~7000 LOC non-test Go (excluding cayley, tests, benchmarks).
Target: WASM via Moxie. Single-threaded, no reflection, no math/big.
Clean (zero issues):
strings package+ concatenation, no fallthrough, no complex types, no uintptrerrors.New() calls use string literalsFoo() / FooFrom(rng io.Reader) pairsMechanical transforms (search-and-replace + trivial rewrites):
make([]T, n) -> []T{:n} / []T{:n:c} literal syntaxmake(map[...]) -> fixed array cache (only 3 known parameter sets)int in struct fields/params -> explicit int32encoding/binary.LittleEndian.PutUint64(...) etc -> inline byte ops.s file extension assembly -> drop (WASM target, use generic fallback)//go:build amd64) -> remove, WASM onlyRequires rewrite:
math/big elimination (3 files, ~200 LOC affected)chacha20, sha3 (reimplement or use moxie stdlib)fmt.Sprintf elimination (5 calls)crypto/sha256 -> native or moxie stdlibcrypto/subtle.ConstantTimeCompare -> inlineFiles: ratio/ratio.go, epoch/epoch.go
| File | LOC | Changes |
|---|---|---|
| ratio.go | 198 | Remove fmt.Sprintf in String(). Replace bare int with int32. make([]T,n) -> literal. |
| epoch.go | 207 | Remove fmt.Sprintf in String(). Replace bare int with int32. |
Trivial. These have zero crypto deps. Port first to validate the toolchain.
Files from crypto/gnarl/:
| File | LOC | Changes |
|---|---|---|
| field.go | 616 | Drop assembly stubs. Use montMulGeneric/montSquareGeneric only. math/bits -> inline (Moxie has bits? verify; if not, manual Add64/Mul64/Sub64). Remove unused fieldLen const. |
| field_generic.go | 11 | Merge into field.go. |
| field_amd64.go | 9 | Drop. |
| field_amd64.s | asm | Drop. |
| scalar.go | 213 | Eliminate `math/big`: replace scReduce with native 4-limb mod (Barrett or repeated subtraction - Q is 213 bits, value is at most 256 bits, ~2 subtractions max). Replace scMul with 4-limb schoolbook multiply + Barrett reduce. ~80 LOC rewrite. |
| torus.go | 277 | Eliminate `math/big`: remove bigToFe, feToBig, bigToScalar, scalarToBig, m4PowBig (all unused per diagnostics). What remains is pure limb ops. |
Key insight: scReduce currently does big.Int.Mod on a 256-bit value mod 213-bit Q. Since the input is at most 256 bits and Q is 213 bits, this is at most a few conditional subtractions, or one Barrett reduction. scMul does 4-limb multiply -> 8-limb intermediate -> Barrett reduce mod Q. Both are ~40 LOC each to implement natively.
| File | LOC | Changes |
|---|---|---|
| ntt.go | 182 | Bare int -> int32. make -> literal. |
| ntt27.go | 370 | Same. |
| ring/ring.go | 284 | Bare int in Params.N -> int32. Propagate. make -> literal. |
| ring/ntt.go | 253 | Replace map[[2]uint32]*nttTables with fixed array of 3 entries (Falcon512, NewHope256, HE64). make -> literal. Bare int in nttTables fields -> int32. |
| ring/sample.go | 248 | make -> literal. encoding/binary -> inline. io.ReadFull -> read loop. |
| File | LOC | Changes |
|---|---|---|
| hamadryad.go | 356 | Remove HashValue (uses any). Remove fmt. encoding/binary -> inline. crypto/rand.Read -> moxie CSPRNG. make -> literal. |
| gnarl_hash.go | 379 | Same pattern. Remove GnarlHashValue. crypto/rand -> moxie CSPRNG. |
| gnarlhashamd64.go | 18 | Drop entirely. WASM target uses generic path. |
| hash.go | 15 | Drop entirely. hashValue(any) and hashFingerprint are dead code (hashFingerprint already flagged unused). |
| ring/sis.go | 376 | encoding/binary -> inline. sha3.NewShake256 -> see Phase 3 deps below. make -> literal. |
Dep: SHA3/SHAKE256. Used by ring/sis.go, ring/gpv.go, ring/kem.go, ring/he.go, ring/keyagg.go, ring/malleable.go. Five files import sha3. Options:
Dep: SHA256. Used only in gnarl/gnarl.go:47 for one-time prime seed derivation during init(). Can be replaced with a hardcoded seed constant (the prime is deterministic).
| File | LOC | Changes |
|---|---|---|
| gnarl/gnarl.go | 408 | Eliminate `math/big` from `initPrime`: the prime P and Q are deterministic constants derived from a fixed seed. Precompute them and embed as pLimbs/qLimbs constants (already done - initPrime just verifies). Replace verifyConstants with compile-time assertion or remove. Remove Params() function (returns *big.Int). Replace crypto/sha256 call with hardcoded seed bytes. |
| gnarl/exp.go | 112 | Pure limb math. Remove unused tmVarExp. No changes needed. |
After Phase 1 scalar.go rewrite, gnarl.go's only remaining big.Int use is in initPrime (one-time verification) and Params() (export). Both removable.
| File | LOC | Changes |
|---|---|---|
| ctr.go | 101 | Replace `chacha20` import. Reimplement ChaCha20 quarter-round from RFC 8439 (~120 LOC). The file already wraps it minimally. make -> literal. |
| gnarl_wire.go | 200 | encoding/binary -> inline. errors OK. Depends on ctr.go for ChaCha20 and gnarl_hash.go for GMid. |
Dep: ChaCha20. Used in ctr.go only. One call: chacha20.NewUnauthenticatedCipher(key, nonce) + .XORKeyStream(). ChaCha20 is ~80 LOC from spec (8 quarter-rounds x 10 double-rounds + counter).
| File | LOC | Changes |
|---|---|---|
| ring/kem.go | 341 | sha3 -> Phase 3 dep. crypto/subtle.ConstantTimeCompare -> inline loop. make -> literal. io -> keep (interface compat). |
| File | LOC | Changes |
|---|---|---|
| ring/gpv.go | 422 | sha3 -> Phase 3 dep. encoding/binary -> inline. make -> literal. |
| ring/gaussian.go | 308 | Float64 rewrite. Uses math.Exp, math.Pi, math.Ceil, math.Floor, math.Round, math.Abs, math.Log2. Two options: (a) fixed-point CDT table precomputed at compile time, (b) check if Moxie has math float support for WASM. WASM has native f64 ops so float64 should work if the math package is available. |
| File | LOC | Changes |
|---|---|---|
| ring/he.go | 381 | sha3 dep. float64 in NoiseEstimate field (can be int32 noise bound instead). make -> literal. |
| ring/keyagg.go | 163 | sha3 dep. make -> literal. |
| ring/mpc.go | 114 | Thin wrapper. errors OK. |
| ring/malleable.go | 234 | sha3 dep. float64 in noise est. make -> literal. |
| ring/recognizer.go | 362 | make -> literal. Depends on he.go. |
| File | LOC | Changes |
|---|---|---|
| params.go | 111 | Uses ratio.Ratio. Bare int -> int32. |
Add64, Mul64, Sub64 used in Montgomery multiply.) If not, inline the ~10 functions needed. These are just (hi, lo) = a * b and (sum, carry) = a + b + c patterns.math.Exp/math.Pi are unavailable, precompute the Gaussian CDT table as integer constants.crypto.getRandomValues() or equivalent. If not, the io.Reader pattern lets us inject any RNG source.ConstantTimeCompare (~10 LOC).| Phase | LOC to port | Difficulty | New code needed |
|---|---|---|---|
| 0 | 405 | trivial | 0 |
| 1 | 1117 | moderate | ~120 (native scalar arith) |
| 2 | 1337 | easy | ~20 (cache array) |
| 3 | 1144 | moderate | ~300 (SHAKE256 if needed) |
| 4 | 520 | easy | 0 (big.Int removal) |
| 5 | 301 | moderate | ~120 (ChaCha20 if needed) |
| 6 | 341 | easy | ~10 (subtle inline) |
| 7 | 730 | moderate | ~50 (float path) |
| 8 | 1254 | easy | 0 |
| 9 | 111 | trivial | 0 |
| Total | ~7260 | ~620 new LOC |
The port is dominated by mechanical transforms. The only algorithmically interesting work is:
None of these are research problems - they're well-specified algorithms with test vectors.
moxie/
ratio/ratio.mx
epoch/epoch.mx
crypto/
hamadryad.mx <- hamadryad.go (no hash.go, no amd64)
gnarl_hash.mx <- gnarl_hash.go (no amd64)
gnarl_wire.mx <- gnarl_wire.go
ctr.mx <- ctr.go (+ inline chacha20)
ntt.mx <- ntt.go
ntt27.mx <- ntt27.go
params.mx <- params.go
gnarl/
field.mx <- field.go + field_generic.go (merged)
scalar.mx <- scalar.go (native 4-limb arith)
torus.mx <- torus.go (pruned)
exp.mx <- exp.go
gnarl.mx <- gnarl.go (no big.Int)
ring/
ring.mx <- ring.go
ntt.mx <- ntt.go (array cache)
sample.mx <- sample.go
kem.mx <- kem.go
gpv.mx <- gpv.go
gaussian.mx <- gaussian.go
he.mx <- he.go
keyagg.mx <- keyagg.go
mpc.mx <- mpc.go
malleable.mx <- malleable.go
recognizer.mx <- recognizer.go
sis.mx <- sis.go
shake256.mx <- new (if needed): Keccak sponge
chacha20.mx <- new (if needed): RFC 8439 ChaCha20