Merge branch 'quantus-crypto' into quantus-codec
Some checks failed
Lock Threads / lock (push) Has been cancelled

This commit is contained in:
2026-09-16 18:14:06 +03:00
10 changed files with 782 additions and 8 deletions

View File

@@ -2,12 +2,36 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bip39"
version = "2.2.2"
@@ -42,6 +66,12 @@ version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -57,6 +87,18 @@ dependencies = [
"libc",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -77,6 +119,21 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "either"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "fixed-hash"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c"
dependencies = [
"static_assertions",
]
[[package]]
name = "futures-core"
version = "0.3.34"
@@ -124,6 +181,27 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"serde",
]
[[package]]
name = "hex"
version = "0.4.3"
@@ -145,6 +223,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -162,12 +249,28 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keccak-hash"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce2bd4c29270e724d3eaadf7bdc8700af4221fc0ed771b855eadcd1b98d52851"
dependencies = [
"primitive-types",
"tiny-keccak",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.8.3"
@@ -175,10 +278,86 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell"
version = "1.21.4"
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"critical-section",
"portable-atomic",
]
[[package]]
name = "pin-project-lite"
@@ -186,6 +365,43 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "plonky2_maybe_rayon"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1e554181dc95243b8d9948ae7bae5759c7fb2502fed28f671f95ef38079406"
[[package]]
name = "plonky2_util"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c32c137808ca984ab2458b612b7eb0462d853ee041a3136e83d54b96074c7610"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "primitive-types"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373"
dependencies = [
"fixed-hash",
"uint",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -195,6 +411,93 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "qp-plonky2"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd331d489a309f88e2d0e35a2b996932c7d92038b91ccc656a0a8e6b11b6977"
dependencies = [
"ahash",
"anyhow",
"critical-section",
"hashbrown",
"itertools",
"keccak-hash",
"log",
"num",
"once_cell",
"plonky2_maybe_rayon",
"plonky2_util",
"qp-plonky2-core",
"qp-plonky2-field",
"qp-plonky2-verifier",
"qp-poseidon-core",
"rand 0.10.1",
"serde",
"static_assertions",
"unroll",
]
[[package]]
name = "qp-plonky2-core"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b81a3a9fce99f7bd45b8578f8d9b6a33507d34c2eb2c47c969b464db1ad601d3"
dependencies = [
"ahash",
"anyhow",
"hashbrown",
"itertools",
"keccak-hash",
"log",
"num",
"plonky2_util",
"qp-plonky2-field",
"serde",
"static_assertions",
"unroll",
]
[[package]]
name = "qp-plonky2-field"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1630d418ddce9feba18d3364596711d07074851757301350de313eef0a31af4f"
dependencies = [
"anyhow",
"itertools",
"num",
"plonky2_util",
"rustc_version",
"serde",
"static_assertions",
"unroll",
]
[[package]]
name = "qp-plonky2-verifier"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "944da5dec21ee476d561f6c38caddf45e829f3cc5fccbc76f6ece03660378dbe"
dependencies = [
"ahash",
"anyhow",
"critical-section",
"hashbrown",
"itertools",
"keccak-hash",
"log",
"num",
"once_cell",
"plonky2_util",
"qp-plonky2-core",
"qp-plonky2-field",
"qp-poseidon-core",
"serde",
"static_assertions",
"unroll",
]
[[package]]
name = "qp-poseidon-core"
version = "3.1.0"
@@ -217,7 +520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ec6c3db4055c217a503c45d0c101cf3c10d4fc1e562f0588aa55dda4f60a4d"
dependencies = [
"bip39",
"getrandom",
"getrandom 0.2.17",
"hex",
"hex-literal",
"qp-poseidon-core",
@@ -230,6 +533,44 @@ dependencies = [
"zeroize",
]
[[package]]
name = "qp-wormhole-circuit"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55167daf965a3616b74184171148b94c64b3a48771d6433a1a43377a6e46c6e8"
dependencies = [
"anyhow",
"hex",
"qp-plonky2",
"qp-wormhole-inputs",
"qp-zk-circuits-common",
"zeroize",
]
[[package]]
name = "qp-wormhole-inputs"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c07863a2211a17b46319289c5ef22e5670a3fb8653386bb366c418193254793"
dependencies = [
"anyhow",
]
[[package]]
name = "qp-zk-circuits-common"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec45510701f160fdb730bdb622d6cdd0a62547e607c9b1ecb19558202797f81"
dependencies = [
"anyhow",
"qp-plonky2",
"qp-poseidon-core",
"qp-wormhole-inputs",
"rand 0.8.6",
"serde",
"serde_json",
]
[[package]]
name = "quantus_crypto"
version = "0.0.0"
@@ -237,6 +578,8 @@ dependencies = [
"qp-poseidon-core",
"qp-rusty-crystals-dilithium",
"qp-rusty-crystals-hdwallet",
"qp-wormhole-circuit",
"qp-zk-circuits-common",
"wasm-bindgen",
]
@@ -249,12 +592,79 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"getrandom 0.4.3",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
@@ -315,6 +725,23 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.119"
@@ -357,6 +784,15 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinyvec"
version = "1.13.2"
@@ -378,6 +814,18 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uint"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52"
dependencies = [
"byteorder",
"crunchy",
"hex",
"static_assertions",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -393,6 +841,16 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unroll"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "version_check"
version = "0.9.5"
@@ -450,6 +908,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "zerocopy"
version = "0.8.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zeroize"
version = "1.8.2"

View File

@@ -29,6 +29,13 @@ qp-rusty-crystals-dilithium = { version = "4.1.1", default-features = false, fea
qp-rusty-crystals-hdwallet = { version = "4.1.1", default-features = false, features = ["ml-dsa-65", "ml-dsa-87"] }
wasm-bindgen = "0.2"
[dev-dependencies]
# The chain's own nullifier, for known-answer tests only. It pulls in plonky2,
# which has no place in the shipped WASM: the port in rs/hdwallet.rs has to
# agree with it, and these tests are how that is shown rather than asserted.
qp-wormhole-circuit = { version = "=4.3.0", default-features = false, features = ["std"] }
qp-zk-circuits-common = { version = "=4.3.0" }
[profile.release]
codegen-units = 1
debug = false

View File

@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
"version": "0.2.0",
"version": "0.3.0",
"main": "index.js",
"dependencies": {
"fflate": "^0.8.2",

View File

@@ -1,7 +1,7 @@
// Copyright 2026 @quantus/crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ext_mldsa_derive, ext_mldsa_from_seed, ext_mldsa_is_scheme, ext_mldsa_sign, ext_mldsa_sizes, ext_mldsa_verify, ext_poseidon_hash, ext_wormhole_addresses } from './generated/quantus_crypto.js';
import { ext_mldsa_derive, ext_mldsa_from_seed, ext_mldsa_is_scheme, ext_mldsa_sign, ext_mldsa_sizes, ext_mldsa_verify, ext_poseidon_hash, ext_wormhole_addresses, ext_wormhole_nullifiers } from './generated/quantus_crypto.js';
import { initWasm } from './init.js';
import { Scheme } from './scheme.js';
@@ -123,6 +123,41 @@ export function wormholeAddresses (mnemonic: string, password: string, account:
return out;
}
/**
* Nullifiers for a run of wormhole addresses, one per deposit transfer count.
*
* A deposit to a wormhole address is spent when its nullifier is in
* `Wormhole::UsedNullifiers`. For each address `start..start + addresses` on
* `branch` of `account`, this returns the nullifiers for transfer counts
* `first..first + count`: `result[a][c]` belongs to address `start + a` and
* transfer count `first + c`.
*
* Check them locally, against a copy of the whole spent set. Never look one up
* by key or send it to a service: exits publish their nullifiers, so whoever
* sees yours can name your exits. At most 100,000 per call; the recovery phrase
* is stretched once per call.
*/
export function wormholeNullifiers (mnemonic: string, password: string, account: number, branch: WormholeBranch, start: number, addresses: number, first: number, count: number): Uint8Array[][] {
ready();
const flat = ext_wormhole_nullifiers(mnemonic, password, account, branch, start, addresses, BigInt(first), count);
const out: Uint8Array[][] = [];
for (let a = 0; a < addresses; a++) {
const row: Uint8Array[] = [];
for (let c = 0; c < count; c++) {
const at = (a * count + c) * 32;
row.push(flat.slice(at, at + 32));
}
out.push(row);
}
return out;
}
/**
* Sign under a FIPS 204 context.
*

View File

@@ -154,6 +154,34 @@ export function ext_poseidon_hash(data: Uint8Array): Uint8Array;
*/
export function ext_wormhole_addresses(mnemonic: string, password: string, account: number, change: number, start: number, count: number): Uint8Array;
/**
* Nullifiers for a run of wormhole addresses' deposits, by transfer count.
*
* A deposit to a wormhole address is spent when its nullifier is in
* `Wormhole::UsedNullifiers`. The nullifier is
*
* ```text
* poseidon2(poseidon2(salt("~nullif~") ‖ secret ‖ transfer_count))
* ```
*
* where `transfer_count` is the address's counter when the deposit landed. It
* needs the address's secret, which is why this takes the recovery phrase and
* why a nullifier should never be sent anywhere to be checked: exits publish
* nullifiers, so whoever sees yours can name your exits.
*
* For addresses `m/44'/189189189'/<account>'/<change>'/<index>'` with index in
* `start..start + addresses`, and transfer counts `first..first + count` for
* each. The BIP39 seed is stretched once for the whole run.
*
* Ported onto `qp-poseidon-core` rather than calling `qp-wormhole-circuit`,
* which would bring plonky2 into the WASM. The port is pinned to the circuit
* crate's own `Nullifier::from_preimage` by the tests.
*
* * returned vector is `addresses * count` 32-byte nullifiers: address by
* address, and by transfer count within each
*/
export function ext_wormhole_nullifiers(mnemonic: string, password: string, account: number, change: number, start: number, addresses: number, first: bigint, count: number): Uint8Array;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
@@ -166,6 +194,7 @@ export interface InitOutput {
readonly ext_mldsa_verify: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
readonly ext_poseidon_hash: (a: number, b: number) => [number, number];
readonly ext_wormhole_addresses: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number];
readonly ext_wormhole_nullifiers: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: bigint, j: number) => [number, number, number, number];
readonly __wbindgen_externrefs: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;

View File

@@ -263,6 +263,55 @@ export function ext_wormhole_addresses(mnemonic, password, account, change, star
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
return v3;
}
/**
* Nullifiers for a run of wormhole addresses' deposits, by transfer count.
*
* A deposit to a wormhole address is spent when its nullifier is in
* `Wormhole::UsedNullifiers`. The nullifier is
*
* ```text
* poseidon2(poseidon2(salt("~nullif~") ‖ secret ‖ transfer_count))
* ```
*
* where `transfer_count` is the address's counter when the deposit landed. It
* needs the address's secret, which is why this takes the recovery phrase and
* why a nullifier should never be sent anywhere to be checked: exits publish
* nullifiers, so whoever sees yours can name your exits.
*
* For addresses `m/44'/189189189'/<account>'/<change>'/<index>'` with index in
* `start..start + addresses`, and transfer counts `first..first + count` for
* each. The BIP39 seed is stretched once for the whole run.
*
* Ported onto `qp-poseidon-core` rather than calling `qp-wormhole-circuit`,
* which would bring plonky2 into the WASM. The port is pinned to the circuit
* crate's own `Nullifier::from_preimage` by the tests.
*
* * returned vector is `addresses * count` 32-byte nullifiers: address by
* address, and by transfer count within each
* @param {string} mnemonic
* @param {string} password
* @param {number} account
* @param {number} change
* @param {number} start
* @param {number} addresses
* @param {bigint} first
* @param {number} count
* @returns {Uint8Array}
*/
export function ext_wormhole_nullifiers(mnemonic, password, account, change, start, addresses, first, 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_nullifiers(ptr0, len0, ptr1, len1, account, change, start, addresses, first, 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,

View File

@@ -1,7 +1,7 @@
// Copyright 2026 @quantus/crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { accountFromPublicKey, isScheme, keypairFromMnemonic, keypairFromSeed, sign, signatureWithPublicKey, sizes, verify, WormholeBranch, wormholeAddresses } from './crypto.js';
export { accountFromPublicKey, isScheme, keypairFromMnemonic, keypairFromSeed, sign, signatureWithPublicKey, sizes, verify, WormholeBranch, wormholeAddresses, wormholeNullifiers } from './crypto.js';
export type { Keypair, Sizes } from './crypto.js';
export { initWasm, isReady } from './init.js';
export { contextForSpec, EXTRINSIC_CONTEXT, EXTRINSIC_MIN_SPEC, Scheme, SCHEME_NAME } from './scheme.js';

View File

@@ -133,3 +133,99 @@ pub fn wormhole_addresses(mnemonic: &str, password: &str, account: u32, change:
Ok(out)
}
/// Salt the chain's nullifier derivation starts from: `NULLIFIER_SALT` in
/// `qp-wormhole-circuit`.
const NULLIFIER_SALT: &str = "~nullif~";
/// The largest number of nullifiers one call computes.
pub const NULLIFIER_MAX_BATCH: u32 = 100_000;
/// Nullifiers for a run of wormhole addresses' deposits, by transfer count.
///
/// A deposit to a wormhole address is spent when its nullifier is in
/// `Wormhole::UsedNullifiers`. The nullifier is
///
/// ```text
/// poseidon2(poseidon2(salt("~nullif~") ‖ secret ‖ transfer_count))
/// ```
///
/// where `transfer_count` is the address's counter when the deposit landed. It
/// needs the address's secret, which is why this takes the recovery phrase and
/// why a nullifier should never be sent anywhere to be checked: exits publish
/// nullifiers, so whoever sees yours can name your exits.
///
/// For addresses `m/44'/189189189'/<account>'/<change>'/<index>'` with index in
/// `start..start + addresses`, and transfer counts `first..first + count` for
/// each. The BIP39 seed is stretched once for the whole run.
///
/// Ported onto `qp-poseidon-core` rather than calling `qp-wormhole-circuit`,
/// which would bring plonky2 into the WASM. The port is pinned to the circuit
/// crate's own `Nullifier::from_preimage` by the tests.
///
/// * returned vector is `addresses * count` 32-byte nullifiers: address by
/// address, and by transfer count within each
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn ext_wormhole_nullifiers(mnemonic: &str, password: &str, account: u32, change: u32, start: u32, addresses: u32, first: u64, count: u32) -> Result<Vec<u8>, JsError> {
wormhole_nullifiers(mnemonic, password, account, change, start, addresses, first, count).map_err(|e| JsError::new(&e))
}
/// The body of [`ext_wormhole_nullifiers`].
#[allow(clippy::too_many_arguments)]
pub fn wormhole_nullifiers(mnemonic: &str, password: &str, account: u32, change: u32, start: u32, addresses: u32, first: u64, count: u32) -> Result<Vec<u8>, String> {
use qp_rusty_crystals_hdwallet::{generate_wormhole_from_seed, mnemonic_to_seed, SensitiveBytes64};
let total = addresses as u64 * count as u64;
if total > NULLIFIER_MAX_BATCH as u64 {
return Err(format!("At most {NULLIFIER_MAX_BATCH} nullifiers per call, asked for {total}"));
}
if start.checked_add(addresses).is_none_or(|end| end > 0x8000_0000) {
return Err(format!("Wormhole address indices must stay below 2^31, asked for {start} + {addresses}"));
}
first.checked_add(count as u64).ok_or("Transfer counts overflow")?;
let password = if password.is_empty() {
None
} else {
Some(password)
};
let mut seed = SensitiveBytes64::zeroed();
mnemonic_to_seed(mnemonic.to_string(), password, &mut seed).map_err(alloc_error)?;
let mut out = Vec::with_capacity(total as usize * 32);
for index in start..start + addresses {
let path = format!("m/44'/189189189'/{account}'/{change}'/{index}'");
let pair = generate_wormhole_from_seed(&seed, &path).map_err(alloc_error)?;
for transfer_count in first..first + count as u64 {
out.extend_from_slice(&nullifier(pair.secret().as_bytes(), transfer_count));
}
}
Ok(out)
}
/// One nullifier from a wormhole secret and a transfer count.
pub fn nullifier(secret: &[u8; 32], transfer_count: u64) -> [u8; 32] {
use qp_poseidon_core::{
hash_twice,
serialization::{bytes_to_digest_lossy, string_to_felts, u64_to_felts},
};
let salt = string_to_felts(NULLIFIER_SALT);
let secret_felts = bytes_to_digest_lossy(secret);
let count_felts = u64_to_felts(transfer_count);
let mut preimage = Vec::with_capacity(salt.len() + secret_felts.len() + count_felts.len());
preimage.extend_from_slice(&salt);
preimage.extend_from_slice(&secret_felts);
preimage.extend_from_slice(&count_felts);
hash_twice(&preimage)
}

View File

@@ -232,3 +232,72 @@ fn wormhole_addresses_refuse_unbounded_requests() {
assert!(wormhole_addresses(MNEMONIC, "", 0, 0, 0x7fff_ffff, 2).is_err());
assert!(wormhole_addresses("not a mnemonic", "", 0, 0, 0, 1).is_err());
}
/// The port of the nullifier agrees with the chain's circuit crate.
///
/// `qp_wormhole_circuit::nullifier::Nullifier::from_preimage` is what the proof
/// commits to and what `Wormhole::UsedNullifiers` records, so it is the
/// reference. Secrets span the edge the lossy 8-bytes-per-felt encoding cares
/// about (limbs at and above the Goldilocks prime), and transfer counts span
/// both 32-bit limbs.
#[test]
fn nullifier_matches_the_circuit() {
use crate::hdwallet::nullifier;
use qp_wormhole_circuit::nullifier::Nullifier;
use qp_zk_circuits_common::utils::{digest_to_bytes, BytesDigest};
let secrets: Vec<[u8; 32]> = vec![
[0u8; 32],
[0xff; 32],
core::array::from_fn(|i| i as u8),
core::array::from_fn(|i| (i as u8).wrapping_mul(97).wrapping_add(13)),
// every limb is the Goldilocks prime 2^64 - 2^32 + 1, big-endian
[0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01].repeat(4).try_into().unwrap(),
];
let counts = [0u64, 1, 41_683, u32::MAX as u64, 1u64 << 32, u64::MAX];
let mut compared = 0;
for secret in &secrets {
for &count in &counts {
let digest = BytesDigest::try_from(*secret);
// The circuit's BytesDigest refuses a non-canonical limb; where it
// does, the chain can never produce that secret's nullifier either.
let Ok(digest) = digest else { continue };
let expected = digest_to_bytes(Nullifier::from_preimage(digest, count).hash);
assert_eq!(hex(&nullifier(secret, count)), hex(expected.as_ref()), "secret {} count {count}", hex(secret));
compared += 1;
}
}
// Skipping is for the non-canonical edge only; a test that compared
// nothing would pass just as well.
assert!(compared >= 18, "only {compared} cases compared");
}
#[test]
fn wormhole_nullifiers_follow_each_address_secret() {
use crate::hdwallet::{nullifier, wormhole_nullifiers};
use qp_rusty_crystals_hdwallet::derive_wormhole_from_mnemonic;
const MNEMONIC: &str = "orchard answer curve patient visual flower maze noise retreat penalty cage small earth domain scan pitch bottom crunch theme club client swap slice raven";
// addresses 2 and 3 on the change branch, transfer counts 5..8
let batch = wormhole_nullifiers(MNEMONIC, "", 0, 1, 2, 2, 5, 3).unwrap();
assert_eq!(batch.len(), 2 * 3 * 32);
for (a, index) in [2u32, 3].into_iter().enumerate() {
let pair = derive_wormhole_from_mnemonic(MNEMONIC, None, &format!("m/44'/189189189'/0'/1'/{index}'")).unwrap();
for c in 0..3usize {
let at = (a * 3 + c) * 32;
assert_eq!(batch[at..at + 32].to_vec(), nullifier(pair.secret().as_bytes(), 5 + c as u64).to_vec(), "address {index} count {}", 5 + c);
}
}
assert!(wormhole_nullifiers(MNEMONIC, "", 0, 0, 0, 40, 0, 2_501).is_err());
assert!(wormhole_nullifiers(MNEMONIC, "", 0, 0, 0, 1, u64::MAX, 2).is_err());
assert!(wormhole_nullifiers(MNEMONIC, "", 0, 0, 0x7fff_ffff, 2, 0, 1).is_err());
}

View File

@@ -13,7 +13,7 @@
import {
accountFromPublicKey, contextForSpec, EXTRINSIC_MIN_SPEC, initWasm,
isReady, keypairFromMnemonic, keypairFromSeed, Scheme, SCHEME_NAME,
sign, signatureWithPublicKey, sizes, verify, WormholeBranch, wormholeAddresses
sign, signatureWithPublicKey, sizes, verify, WormholeBranch, wormholeAddresses, wormholeNullifiers
} from '@quantus/crypto';
let fail = 0;
@@ -62,4 +62,15 @@ eq('wormhole receive 0 is the node test address',
'dfcfd6e59c75d208e84f54a887537bcf7b04265790ec79960bf49de123404d0e');
eq('change branch differs', Buffer.from(wormholeAddresses(NODE_PHRASE, '', 0, WormholeBranch.Change, 0, 1)[0]).toString('hex') !== Buffer.from(wh[0]).toString('hex'), true);
// nullifiers: shape, determinism, and the cost of a wallet account's precompute
const n = wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Receive, 0, 2, 7, 3);
eq('nullifier shape', `${n.length}x${n[0].length}x${n[0][0].length}`, '2x3x32');
eq('nullifiers differ by count', Buffer.from(n[0][0]).equals(Buffer.from(n[0][1])), false);
eq('nullifiers differ by address', Buffer.from(n[0][0]).equals(Buffer.from(n[1][0])), false);
eq('a sub-range agrees', Buffer.from(wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Receive, 1, 1, 8, 1)[0][0]).toString('hex'), Buffer.from(n[1][1]).toString('hex'));
const t0 = performance.now();
wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Receive, 0, 20, 0, 256);
wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Change, 0, 20, 0, 256);
console.log(` 40 addresses x 256 counts: ${Math.round(performance.now() - t0)} ms`);
process.exit(fail ? 1 : 0);