PORT_PLAN.md raw

Moxie Port Plan - gnarl-hamadryad

Source: ~7000 LOC non-test Go (excluding cayley, tests, benchmarks). Target: WASM via Moxie. Single-threaded, no reflection, no math/big.

Audit Summary

Clean (zero issues):

Mechanical transforms (search-and-replace + trivial rewrites):

Requires rewrite:

Port Order (dependency-driven)

Phase 0: Foundation types (no deps)

Files: ratio/ratio.go, epoch/epoch.go

FileLOCChanges
ratio.go198Remove fmt.Sprintf in String(). Replace bare int with int32. make([]T,n) -> literal.
epoch.go207Remove fmt.Sprintf in String(). Replace bare int with int32.

Trivial. These have zero crypto deps. Port first to validate the toolchain.

Phase 1: Field arithmetic (pure math, no stdlib deps)

Files from crypto/gnarl/:

FileLOCChanges
field.go616Drop 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.go11Merge into field.go.
field_amd64.go9Drop.
field_amd64.sasmDrop.
scalar.go213Eliminate `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.go277Eliminate `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.

Phase 2: NTT engines (pure math)

FileLOCChanges
ntt.go182Bare int -> int32. make -> literal.
ntt27.go370Same.
ring/ring.go284Bare int in Params.N -> int32. Propagate. make -> literal.
ring/ntt.go253Replace map[[2]uint32]*nttTables with fixed array of 3 entries (Falcon512, NewHope256, HE64). make -> literal. Bare int in nttTables fields -> int32.
ring/sample.go248make -> literal. encoding/binary -> inline. io.ReadFull -> read loop.

Phase 3: Hash functions

FileLOCChanges
hamadryad.go356Remove HashValue (uses any). Remove fmt. encoding/binary -> inline. crypto/rand.Read -> moxie CSPRNG. make -> literal.
gnarl_hash.go379Same pattern. Remove GnarlHashValue. crypto/rand -> moxie CSPRNG.
gnarlhashamd64.go18Drop entirely. WASM target uses generic path.
hash.go15Drop entirely. hashValue(any) and hashFingerprint are dead code (hashFingerprint already flagged unused).
ring/sis.go376encoding/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:

  1. Reimplement SHAKE256 from FIPS 202 spec (~300 LOC Keccak sponge)
  2. Check if moxie stdlib has it
  3. Use the Hamadryad hash as domain-separated PRF instead (architectural change)

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).

Phase 4: Gnarl signatures

FileLOCChanges
gnarl/gnarl.go408Eliminate `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.go112Pure 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.

Phase 5: Wire protocol and stream cipher

FileLOCChanges
ctr.go101Replace `chacha20` import. Reimplement ChaCha20 quarter-round from RFC 8439 (~120 LOC). The file already wraps it minimally. make -> literal.
gnarl_wire.go200encoding/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).

Phase 6: Ring-LWE KEM

FileLOCChanges
ring/kem.go341sha3 -> Phase 3 dep. crypto/subtle.ConstantTimeCompare -> inline loop. make -> literal. io -> keep (interface compat).

Phase 7: GPV signatures

FileLOCChanges
ring/gpv.go422sha3 -> Phase 3 dep. encoding/binary -> inline. make -> literal.
ring/gaussian.go308Float64 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.

Phase 8: Homomorphic encryption + MPC

FileLOCChanges
ring/he.go381sha3 dep. float64 in NoiseEstimate field (can be int32 noise bound instead). make -> literal.
ring/keyagg.go163sha3 dep. make -> literal.
ring/mpc.go114Thin wrapper. errors OK.
ring/malleable.go234sha3 dep. float64 in noise est. make -> literal.
ring/recognizer.go362make -> literal. Depends on he.go.

Phase 9: Params

FileLOCChanges
params.go111Uses ratio.Ratio. Bare int -> int32.

Critical Dependencies to Resolve Before Starting

  1. Does Moxie stdlib have `math/bits`? (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.
  1. Does Moxie stdlib have `math` (float64 ops)? WASM has native f64. If math.Exp/math.Pi are unavailable, precompute the Gaussian CDT table as integer constants.
  1. Does Moxie stdlib have `crypto/rand`? WASM needs crypto.getRandomValues() or equivalent. If not, the io.Reader pattern lets us inject any RNG source.
  1. SHAKE256 availability. If absent, implement Keccak-f[1600] sponge (~300 LOC). Used pervasively in ring/ for domain-separated hashing.
  1. `crypto/subtle` availability. If absent, one function: ConstantTimeCompare (~10 LOC).

Estimated Effort

PhaseLOC to portDifficultyNew code needed
0405trivial0
11117moderate~120 (native scalar arith)
21337easy~20 (cache array)
31144moderate~300 (SHAKE256 if needed)
4520easy0 (big.Int removal)
5301moderate~120 (ChaCha20 if needed)
6341easy~10 (subtle inline)
7730moderate~50 (float path)
81254easy0
9111trivial0
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.

File Mapping

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