2 Commits
Author SHA1 Message Date
grenadeandClaude Opus 5 4e5b479db4 feat(quantus-crypto): wormhole nullifiers
A deposit to a wormhole address is spent when its nullifier,
poseidon2(poseidon2(salt || secret || transfer_count)), is in
Wormhole::UsedNullifiers. Working out a wormhole balance means computing one
for each deposit. That needs the address's secret, which never leaves WASM.

wormholeNullifiers(mnemonic, password, account, branch, start, addresses,
first, count) returns them for a run of addresses and a run of transfer
counts. The BIP39 seed is stretched once per call; 40 addresses x 256 counts
takes 194 ms in node. A call is capped at 100,000 nullifiers.

This is ported onto qp-poseidon-core, not qp-wormhole-circuit, which would
bring plonky2 into the WASM. The circuit crate is a dev-dependency only, as
the reference: a known-answer test compares the port with
Nullifier::from_preimage across secrets at and above the Goldilocks-prime
limb edge and transfer counts across both 32-bit limbs, and asserts that
enough cases were actually compared rather than skipped.

The doc comments say to check nullifiers against a local copy of the spent
set, never by key: exits publish nullifiers, so a lookup names the exit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 18:13:57 +03:00
rob thijssenandClaude Opus 5 6eb04f63ac feat(quantus-crypto): ML-DSA, Poseidon2 and HD derivation as a separate crate
Wraps the chain's own crypto crates for the browser: qp-rusty-crystals-dilithium
(ML-DSA-65 and ML-DSA-87), qp-poseidon-core for the account-id hash, and
qp-rusty-crystals-hdwallet for BIP44 derivation. Nothing is reimplemented — a
browser wallet that disagreed with the chain about a key or a signature would
emit well-formed output the chain rejects, with nothing on this side able to
tell.

A separate crate rather than more files in wasm-crypto, because the two cannot
share a Cargo graph. wasm-crypto builds with nightly-2022-06-24 against a
2019-era dependency set; the ML-DSA crates use inline `const {}` blocks that
need Rust >= 1.79. Bumping the older one would mean rewriting upstream's
sr25519/ed25519 build, which is the thing most worth leaving alone so rebases
stay boring. wasm-crypto is untouched here.

The scheme selector is the chain's own signature-enum variant index (0 for
ML-DSA-87, 1 for ML-DSA-65), so the number threaded through this API is the
byte that ends up on the wire and there is no mapping to get backwards. Key and
signature sizes are exported rather than left for JS to hardcode: they are
consensus-critical and a drifted constant would mis-frame every byte after the
signature while looking healthy.

Logic is split from the #[wasm_bindgen] wrappers because JsError cannot be
constructed off-wasm, which made every error path untestable by cargo test —
and the error paths are what most needs testing.

Verified against the `quantus` CLI 2.2.2 as an independent oracle, not against
our own output: the three dev-genesis account ids, and HD derivation at both
schemes' default paths from the public Substrate dev phrase. Context separation
is pinned too — a signature made under QUANTUS_EXTRINSIC must not verify under
the empty context, which is what makes the spec-148 boundary detectable rather
than a silent chain rejection.

Refs quantus/wasm#1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-10 13:54:25 +03:00