Unified Lux Wallet shared core as a pnpm workspace. Umbrella @luxwallet/sdk re-exports independent modules; connect/SIWx stays in luxwallet/connect (depended on, not duplicated). Packages (packages/): - @luxwallet/chains REAL chain registry, one source of truth, emits chains.json for native. Seeds Lux C (96369/96368/96370), Zoo (200200/200201), Hanzo (36963/36964), Sparkle Pony (36911/36910), Pars (494949/7071) as EVM; X/P/Q/Z Lux families as non-EVM entries (builderStatus todo). - @luxwallet/rpc REAL gateway client: https://<gateway>/v1/rpc/<chainId>, default api.hanzo.ai + brand/per-chain override (getBootnodeRpcUrl semantics). - @luxwallet/crypto facade over luxfi/crypto C core (WASM/UniFFI). Exact CryptoBackend interface + FIPS sizes; impl stubbed (throws 'not yet bound'). NOT @noble/post-quantum. LLM.md documents WASM bind + the on-chain ML-DSA precompile wire format + shared KAT vectors. - @luxwallet/keyring account model + keystore. Keyring store + BIP-44 paths + AccountID framing REAL; crypto calls stubbed via @luxwallet/crypto. - @luxwallet/tx EVM unsigned EIP-1559 builder REAL (viem); P/X/UTXO/SVM typed stubs with builderStatus + LLM.md citing Lux tx types. - @luxwallet/sdk thin umbrella barrel (+ subpath exports). TS project references (tsc --build), vitest per package, react-native + browser/default export conditions. verify-clean gate: no @noble PQ, no GPL, crypto facade dependency-free. pnpm -r typecheck: 6/6 pass. pnpm -r test: 44 pass. pnpm verify: OK.
4.3 KiB
@luxwallet/crypto — implementation notes
This package is a facade. The interface in src/types.ts is final and
exact. The implementation is a stub that throws "not yet bound" until a
real backend is injected via setBackend. Read this before binding it.
(a) Bind luxfi/crypto — NOT @noble/post-quantum
The PQ primitives (ML-DSA-44/65/87, SLH-DSA, ML-KEM-768) and the classical ones MUST come from the luxfi/crypto C core, the same implementation the chain uses. One library, one implementation — wallet bytes are byte-identical to consensus bytes.
- Source:
~/work/lux/crypto. C ABI underbindings/cabi(main.goexposeslibluxcrypto), Rust underbindings/rust, an existing Node/koffi binding underbindings/typescript(packageluxcrypto, exportingmldsa65,mlkem768). - Web / extension: compile luxfi/crypto to WASM and wrap it to the
CryptoBackendshape, thensetBackend(wasmBackend). (The koffi binding inbindings/typescriptis Node-FFI only; the browser/extension path needs WASM.) - Native (iOS/Android): bind via UniFFI over the same C ABI; expose
the identical
CryptoBackend. See root LLM.md cross-language plan.
DO NOT add @noble/post-quantum (or any pure-JS PQ lib). verify-clean
fails the build if it appears here. Reasons: (1) we need bit-exact agreement
with the chain's FIPS-204 packing and context strings; (2) a second
implementation is a second thing to audit and a second place to drift.
(b) The on-chain ML-DSA precompile wire format
The signer's output must compose into the input the on-chain ML-DSA
verify precompile expects. The authoritative layout is in
~/work/lux/crypto/precompile/mldsa.go:
-
One precompile address per mode (no in-band mode byte):
0x…0110ML-DSA-44,0x…0111ML-DSA-65,0x…0112ML-DSA-87.
-
Input layout for each (from
mldsaVerify.Run):input = pubKey(pubKeySize) ‖ sig(sigSize) ‖ message(rest)i.e. public key first (fixed size), then signature (fixed size), then the variable-length message.
pubKeySize/sigSizeare the FIPS-204 packed sizes — ML-DSA-65 → pk 1952, sig 3309 (seeSizesintypes.ts, sourced fromluxfi/utxo/mldsafx/credential.go).
WARNING — the audit trap.
lux/wallet/pkgs/wallet/src/features/wallet/pqframed the precompile input asmode(1) ‖ pubkey ‖ msgLen(32 BE) ‖ sig ‖ msg. That is WRONG: the live precompile selects mode by address (no mode byte), expects pubkey then sig then msg (no length prefix), and rejects anything else. Do not port that module. Build inputs to matchmldsa.go.
For UTXO/credential verification (X/P chains) the shape is different
again: an MLDSACredential carries N signatures matching N OutputOwners,
where owners are full public keys (1952 B for ML-DSA-65), not 20-byte
hash addresses — see luxfi/utxo/mldsafx. That path is @luxwallet/tx's
concern (builderStatus todo); this package only produces raw signatures.
Signing context
The wallet account signer signs FIPS-204 "pure" with the Lux context string
LUX/WALLET/TX/V1 over a 48-byte (384-bit) digest (see
lux/sdk/wallet/account/pq_account.go). Match that context and digest size
exactly, or signatures verify locally but fail against chain expectations.
(c) Lock with shared KAT vectors
Generate vectors.json (known-answer tests) from the Go side
(luxfi/crypto) and commit it here. The TS/WASM and native backends must
reproduce every vector:
- ML-DSA-65: seed (xi) → (pk, sk); (sk, msg, ctx) → sig; (pk, msg, ctx, sig)
→ true. Include the
LUX/WALLET/TX/V1context cases. - SLH-DSA-192s, ML-KEM-768, secp256k1, ed25519: analogous.
- AccountID: (chainID, scheme, pubkey) → 48-byte cSHAKE-256 id, matching
lux/sdk/wallet/account/cshake.go(N="LUX_ACCOUNT_V1",S="LUX/WALLET/ACCOUNT_ID/V1").
A test here loads vectors.json and asserts the bound backend matches.
Until the WASM backend lands, that test is skipped (no backend bound).
Status
- Exact
CryptoBackendinterface + sizes/scheme constants. - Stub backend that throws
not yet bound;setBackendinjection. - WASM build of luxfi/crypto + browser/extension backend wrapper.
- UniFFI native backend.
vectors.jsonKAT from Go + conformance test.