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
455 lines
16 KiB
JavaScript
455 lines
16 KiB
JavaScript
/* @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 };
|