Files
wasm/packages/quantus-crypto/src/generated/quantus_crypto.js
T
grenadeandClaude Opus 5 0b1cfe35b3 feat(quantus-crypto): derive wormhole addresses
A wormhole address is not a key. A path under coin type 189189189 yields a
32-byte secret, and the address is poseidon(poseidon(salt || secret)). Funds
leave only through a ZK proof of that secret. The extension needs the
addresses to show a wallet's wormhole account; it has no use for the secrets.

wormholeAddresses(mnemonic, password, account, branch, start, count) returns
32-byte account ids for m/44'/189189189'/<account>'/<branch>'/<index>', the
mobile wallet's paths. The secrets and first hashes are derived and wiped
inside WASM.

The BIP39 seed is stretched once per call rather than once per address, since
a gap-limit window is dozens of addresses and each stretch is 2048 PBKDF2
rounds. A call is capped at 1000 addresses, and indices must stay below 2^31.

Pinned to the chain node's own vector
(node/src/tests/data/quantus_key_test_data.rs): TEST_MNEMONIC at
m/44'/189189189'/0'/0'/0' is TEST_WORMHOLE_ADDRESS, the same pair the mobile
wallet's SDK tests. Checked in Rust and through the packed package's consumer
test.

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

455 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* @ts-self-types="./quantus_crypto.d.ts" */
/**
* Derive a keypair from a BIP39 mnemonic at a hardened derivation path.
*
* Lattice keys have no public derivability, so there is no soft-junction
* equivalent and the crate rejects any unhardened path outright. The Quantus
* convention is:
*
* ```text
* m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
* ```
*
* with the account index at the third level and the *scheme* carried in the
* trailing index. That is unusual, and it is what `quantus-cli` and the mobile
* wallet already use — deriving anything else produces addresses no other
* Quantus tool can find.
*
* The seeding matters as much as the path. This goes mnemonic → 64-byte BIP39
* seed → HMAC-SHA512 chain keyed with the literal string `"Dilithium seed"`.
* Substrate's own `mnemonicToMiniSecret` is a *different* derivation and is the
* default reach in the polkadot-js codebase; using it here would yield a
* well-formed key for an account nobody owns.
*
* * mnemonic: BIP39 phrase, 12/15/18/21/24 words
* * password: BIP39 passphrase; empty string for none
* * path: hardened derivation path, e.g. `m/44'/189189'/0'/0'/1'`
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
*
* * returned vector is the secret key followed by the public key, as
* `ext_mldsa_from_seed` returns.
* @param {string} mnemonic
* @param {string} password
* @param {string} path
* @param {number} scheme
* @returns {Uint8Array}
*/
export function ext_mldsa_derive(mnemonic, password, path, scheme) {
const ptr0 = passStringToWasm0(mnemonic, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passStringToWasm0(path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len2 = WASM_VECTOR_LEN;
const ret = wasm.ext_mldsa_derive(ptr0, len0, ptr1, len1, ptr2, len2, scheme);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v4 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v4;
}
/**
* Generate a keypair from 32 bytes of entropy.
*
* This is FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step: the
* crate expands the seed as `SHAKE256(seed ‖ k ‖ ℓ)`, so the parameter set is
* absorbed into the expansion and the same 32 bytes yield independent keys per
* scheme. That is why the dev accounts (`[0u8; 32]`, `[1u8; 32]`, `[2u8; 32]`)
* and HD-derived accounts can share this one entry point.
*
* * seed: UIntArray with 32 elements
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
*
* * returned vector is the secret key followed by the public key, matching the
* ordering `ext_ed_from_seed` uses. Split it at the secret length from
* `ext_mldsa_sizes`.
* @param {Uint8Array} seed
* @param {number} scheme
* @returns {Uint8Array}
*/
export function ext_mldsa_from_seed(seed, scheme) {
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.ext_mldsa_from_seed(ptr0, len0, scheme);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v2;
}
/**
* Whether `scheme` names a parameter set this build supports.
*
* `dispatch!` falls back to ML-DSA-87 for anything unrecognised, which is the
* right default but a poor way to discover a typo. Callers that accept a scheme
* from storage or from a user should check here first.
* @param {number} scheme
* @returns {boolean}
*/
export function ext_mldsa_is_scheme(scheme) {
const ret = wasm.ext_mldsa_is_scheme(scheme);
return ret !== 0;
}
/**
* Sign a message under a FIPS 204 context.
*
* Signing is deterministic — no hedging randomness — because that is what the
* runtime does (`hedge: None`), and a wallet that hedged would produce a
* different signature each time for the same input, which makes the
* byte-for-byte agreement tests in quantus/wasm#2 impossible to write.
*
* `ctx` is domain separation and it is **not** optional in practice: extrinsics
* on spec >= 148 are verified under `QUANTUS_EXTRINSIC`, earlier specs under the
* empty context, and a signature made under the wrong one is valid, rejected by
* the chain, and indistinguishable locally. The caller chooses; this function
* does not guess.
*
* * secret: UIntArray, secret-key length for the scheme
* * public: UIntArray, public-key length for the scheme
* * message: arbitrary length UIntArray
* * ctx: UIntArray, at most 255 elements; empty for no context
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
*
* * returned vector is the signature alone. The runtime's wire format is
* `signature ‖ public`; concatenating is the caller's job because only the
* caller knows whether it wants the wire form or the bare signature.
* @param {Uint8Array} secret
* @param {Uint8Array} _public
* @param {Uint8Array} message
* @param {Uint8Array} ctx
* @param {number} scheme
* @returns {Uint8Array}
*/
export function ext_mldsa_sign(secret, _public, message, ctx, scheme) {
const ptr0 = passArray8ToWasm0(secret, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArray8ToWasm0(_public, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
const len2 = WASM_VECTOR_LEN;
const ptr3 = passArray8ToWasm0(ctx, wasm.__wbindgen_malloc);
const len3 = WASM_VECTOR_LEN;
const ret = wasm.ext_mldsa_sign(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, scheme);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v5 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v5;
}
/**
* Key and signature sizes for a parameter set, as
* `[public, secret, signature, signature_with_public]`.
*
* Exported so that nothing on the JS side has to hardcode 1952/4032/3309/5261 or
* 2592/4896/4627/7219. Those numbers are consensus-critical — the runtime reads a
* fixed-size array off the wire — and a JS constant that drifted from the crate
* would mis-frame every byte after the signature while looking entirely healthy.
* Ask the crate instead.
*
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
*
* * returned vector is four u32 lengths, little-endian, 16 bytes total.
* @param {number} scheme
* @returns {Uint8Array}
*/
export function ext_mldsa_sizes(scheme) {
const ret = wasm.ext_mldsa_sizes(scheme);
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v1;
}
/**
* Verify a signature against a message and public key under a context.
*
* * public: UIntArray, public-key length for the scheme
* * message: arbitrary length UIntArray
* * signature: UIntArray, signature length for the scheme
* * ctx: UIntArray, at most 255 elements; empty for no context
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
* @param {Uint8Array} _public
* @param {Uint8Array} message
* @param {Uint8Array} signature
* @param {Uint8Array} ctx
* @param {number} scheme
* @returns {boolean}
*/
export function ext_mldsa_verify(_public, message, signature, ctx, scheme) {
const ptr0 = passArray8ToWasm0(_public, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passArray8ToWasm0(signature, wasm.__wbindgen_malloc);
const len2 = WASM_VECTOR_LEN;
const ptr3 = passArray8ToWasm0(ctx, wasm.__wbindgen_malloc);
const len3 = WASM_VECTOR_LEN;
const ret = wasm.ext_mldsa_verify(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, scheme);
return ret !== 0;
}
/**
* Poseidon2-over-Goldilocks hash of arbitrary bytes.
*
* This is the account-id derivation. On Substrate an `AccountId32` *is* the
* public key; on Quantus it is `hash_bytes(public_key)`, which is why a Quantus
* signature has to carry its public key along — the address cannot give it back.
*
* `qp_poseidon_core::hash_bytes` is `IdentifyAccount for DilithiumSigner` in the
* runtime, so this is the same function the chain uses to decide who signed
* something, reached through the same crate rather than a port of it.
*
* * data: arbitrary length UIntArray
*
* * returned vector is 32 bytes.
* @param {Uint8Array} data
* @returns {Uint8Array}
*/
export function ext_poseidon_hash(data) {
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.ext_poseidon_hash(ptr0, len0);
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v2;
}
/**
* Derive consecutive wormhole addresses for one account and branch.
*
* A wormhole address is not a key. The path yields a 32-byte **secret**, and
* the address is `poseidon(poseidon(salt ‖ secret))`; funds leave it only
* through a ZK proof of knowing that secret. So this returns the addresses and
* nothing else. The secrets and the intermediate `first_hash` stay inside this
* call and are wiped on drop by the crate's sensitive types.
*
* Paths are `m/44'/189189189'/<account>'/<change>'/<index>'`, as the mobile
* wallet derives them: change `0` is the receive branch, `1` the change branch.
*
* The BIP39 seed is stretched once for the whole batch. Stretching per address
* is PBKDF2 with 2048 rounds each time, and a gap-limit window is dozens of
* addresses.
*
* * mnemonic: BIP39 phrase
* * password: BIP39 passphrase; empty string for none
* * account, change, start, count: which addresses; count <= WORMHOLE_MAX_BATCH
*
* * returned vector is `count` 32-byte account ids, concatenated
* @param {string} mnemonic
* @param {string} password
* @param {number} account
* @param {number} change
* @param {number} start
* @param {number} count
* @returns {Uint8Array}
*/
export function ext_wormhole_addresses(mnemonic, password, account, change, start, count) {
const ptr0 = passStringToWasm0(mnemonic, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.ext_wormhole_addresses(ptr0, len0, ptr1, len1, account, change, start, count);
if (ret[3]) {
throw takeFromExternrefTable0(ret[2]);
}
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v3;
}
function __wbg_get_imports() {
const import0 = {
__proto__: null,
__wbg_Error_67e7344beaa85059: function(arg0, arg1) {
const ret = Error(getStringFromWasm0(arg0, arg1));
return ret;
},
__wbindgen_init_externref_table: function() {
const table = wasm.__wbindgen_externrefs;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
},
};
return {
__proto__: null,
"./quantus_crypto_bg.js": import0,
};
}
function getArrayU8FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
}
function getStringFromWasm0(ptr, len) {
return decodeText(ptr >>> 0, len);
}
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1, 1) >>> 0;
getUint8ArrayMemory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = cachedTextEncoder.encodeInto(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function takeFromExternrefTable0(idx) {
const value = wasm.__wbindgen_externrefs.get(idx);
wasm.__externref_table_dealloc(idx);
return value;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
const MAX_SAFARI_DECODE_BYTES = 2146435072;
let numBytesDecoded = 0;
function decodeText(ptr, len) {
numBytesDecoded += len;
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
numBytesDecoded = len;
}
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
const cachedTextEncoder = new TextEncoder();
if (!('encodeInto' in cachedTextEncoder)) {
cachedTextEncoder.encodeInto = function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
};
}
let WASM_VECTOR_LEN = 0;
let wasmModule, wasmInstance, wasm;
function __wbg_finalize_init(instance, module) {
wasmInstance = instance;
wasm = instance.exports;
wasmModule = module;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (!module.ok) {
throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
}
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
const validResponse = expectedResponseType(module.type);
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else { throw e; }
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
function expectedResponseType(type) {
switch (type) {
case 'basic': case 'cors': case 'default': return true;
}
return false;
}
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (module !== undefined) {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };