The package could not be bundled. Every webpack consumer failed with:
Module not found: Error: Can't resolve 'quantus_crypto_bg.wasm'
in node_modules/@quantus/crypto/generated
wasm-bindgen's async `__wbg_init` contains
module_or_path = new URL('quantus_crypto_bg.wasm', import.meta.url);
and webpack resolves `new URL(..., import.meta.url)` statically, at build time,
whether or not the branch can run. The file is not in the package — the wasm
ships base64'd in bytes.js, which is the entire point of this package — so the
build failed on a code path we never call.
node never sees it, which is why ten Rust tests, twelve consumer assertions and a
browser probe all passed while the package was unusable in a bundler. It took a
real extension build to surface, and that is the useful lesson: this package's
consumers bundle, and nothing in its own test suite does.
So the dead init is removed after bindgen runs. Shipping a second copy of the
wasm to satisfy a path we do not use would be the wide fix; deleting generated
code we never call is the narrow one.
The stripper asserts the shape it expects and throws if wasm-bindgen changes it,
rather than silently no-opping — a build that quietly stopped stripping would
ship the broken package again. It also re-checks that no reference to the .wasm
filename survives.
Published as 0.1.1.
Refs quantus/wasm#1, quantus/extension#2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
@quantus/crypto
Quantus post-quantum crypto for the browser: ML-DSA-65 and ML-DSA-87 signatures, Poseidon2-over-Goldilocks account-id hashing, and hardened BIP44 key derivation.
Every function delegates to the crates the Quantus runtime itself uses —
qp-rusty-crystals-dilithium, qp-poseidon-core, qp-rusty-crystals-hdwallet —
rather than reimplementing them. A browser wallet that disagreed with the chain
about a key or a signature would produce perfectly well-formed output that the
chain rejects, and nothing on this side could tell.
Why a separate package from @polkadot/wasm-crypto
They cannot share a Cargo build. wasm-crypto is compiled with
nightly-2022-06-24 against a 2019-era dependency set; the ML-DSA crates use
inline const {} blocks that require Rust >= 1.79. Modernising the older build
would mean rewriting upstream's sr25519/ed25519 crypto, which is the thing most
worth leaving untouched so rebases onto upstream stay boring.
What is shared is the packaging: the WASM is zlib-compressed and base64'd into
the JS at build time, so nothing is fetched at runtime. That matters because the
consumer is an MV3 service worker under script-src 'self' 'wasm-unsafe-eval',
which can compile WASM but cannot usefully fetch it, and because callers like
pair.sign() are synchronous and have no await to give.
@polkadot/wasm-bridge is deliberately not used: its Bridge implements
wasm-bindgen 0.2.79's JS-heap ABI, while this crate builds with 0.2.128, which
uses externref tables. wasm-bindgen's own generated glue plus initSync is both
smaller and correct.
The only runtime dependency is fflate, for zlib inflate. Base64 decoding is
fifteen lines here rather than a dependency. Both were originally taken from
@polkadot/wasm-util, which turned out to cost more than it saved: its index
re-exports packageDetect, dragging in a @polkadot/util peer dependency for a
side effect we do not want, and being a workspace package it resolved through its
own repo's node_modules when this package was consumed by symlink from another
checkout — which is exactly how quantus/common consumes it during development.
Scheme selector
Scheme.MlDsa87 = 0, Scheme.MlDsa65 = 1 — these are the chain's own
DilithiumSignatureScheme variant indices, so the number threaded through this
API is the byte that ends up on the wire. New accounts use ML-DSA-65; ML-DSA-87
is legacy and must be supported but never chosen.
Signing context
ML-DSA hashes a context into the signature. Quantus extrinsics on spec >= 148 are
verified under QUANTUS_EXTRINSIC, earlier specs under the empty context. A
signature made under the wrong one is cryptographically valid, rejected by the
chain, and indistinguishable locally — so use contextForSpec(specVersion)
rather than picking one by hand. Nothing here guesses on your behalf.
Sizes come from the crate
sizes(scheme) returns the public/secret/signature lengths rather than exposing
constants to copy. They are consensus-critical — the runtime decodes a fixed-size
array with no compact length prefix — and a JS constant that drifted would
re-frame every byte after the signature while looking entirely healthy.
Building
yarn install-build-deps # downloads wasm-bindgen 0.2.128 and binaryen
./scripts/build-quantus.sh
The Rust toolchain is pinned in rust-toolchain.toml to the same channel the
chain builds its runtime with.
Tests
cargo test runs conformance tests whose expected values come from the quantus
CLI, not from this crate — the dev-genesis account ids, HD derivation at both
schemes' default paths, and context separation. A test that pinned our own output
would keep passing through exactly the drift they exist to catch.