Skip to content

Wormhole as its own package: @quantus-network/wormhole (native addon, all 3 layers) - #6

Open
n13 wants to merge 5 commits into
mainfrom
feat/wormhole
Open

n13 wants to merge 5 commits into
mainfrom
feat/wormhole

Conversation

@n13

@n13 n13 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

What

The wormhole for JS as a separate, optional package; @quantus-network/wasm stays signing-only.

  • @quantus-network/wormhole (wormhole/native): a native Node.js addon (napi-rs) with a typed ESM API. Deposit keys (wormholeAddress, wormholeFromMnemonic, nullifier), decodeZkLeaf, merklePositions, amount quantisation and fee, proving for all three layers (proveLeaf, aggregatePrivateBatch, aggregatePublicBatch, all async and off the event loop), parse*/verify* for every layer with the verifier artifacts the runtime embeds, encodeVerifyPrivateBatch / encodeVerifyPublicBatch, and usedNullifierStorageKey / extrinsicHash for settlement checks with one state_getStorage call. Prebuilt binaries as optional dependencies @quantus-network/wormhole-<platform> for Linux x64 (glibc, musl), Linux arm64, macOS arm64/x64.
  • @quantus-network/wasm: unchanged API. README points at the companion package; the two meet at signTransfer(seed, { recipient: key.address }).
  • examples/consumer: a standalone project using both packages like an application (see below).
Layer In → out Measured (10 cores)
0 leaf 1 deposit → 2 exit slots 0.03–0.1 s
1 private batch 7 leaves → 14 slots, 7 nullifiers 3–5 s, ~2 GB
2 public batch 53 batches → 742 slots, 371 nullifiers 20–30 s, ~8 GB

Why native, why separate

wasm32 cannot address the 7 GB a public batch needs; wasm threads required a nightly toolchain, build-std, and custom worker_threads glue, and a private batch still took 18 s against 4 s natively. A separate package keeps the wasm package pure and free of a peer dependency, and lets the wormhole version track the chain's circuit releases on its own.

Layout

  • wormhole/core: pure Rust (keys, leaves, verify, extrinsics, proving). Layer 2 behind the public-batch feature; the build script generates the verifier artifacts with the same generator and QP_NUM_* sizing as pallet-wormhole and, with the feature, proves the layer-2 padding batch.
  • wormhole/native: napi bindings (src/lib.rs, thin: hex/SS58 at the boundary, Uint8Array for blobs) + the typed wrapper (js/index.mts) over the generated binding.js; npm/ holds the platform packages; README.md is the package's documentation.
  • .github/workflows/native.yml: builds the five targets, smoke-tests each, and publishes the platform packages and the package on a GitHub Release via Trusted Publishing.

Circuit crates stay pinned to what the runtime verifies against (qp-wormhole-* =4.3.0, qp-plonky2 =1.5.5). Rebased onto main after #4 (ML-DSA-65) and #8; squashed to one commit plus the example.

Tests

  • Rust (wormhole/core/tests, 12): the circuits repo's reference deposit is proved, aggregated and verified; the runtime's private_batch.hex / public_batch.hex verify with the embedded verifiers; extrinsics decode as Wormhole(20)::verify_private_batch(2) / verify_public_batch(3); leaf SCALE layout; Merkle sorting; storage-key layout. Layer-2 aggregation opt-in via WORMHOLE_TEST_PUBLIC_BATCH=1 (passes: 23 s, verified with the embedded public-batch verifier).
  • JS (test/wormhole.test.mjs, 9 + 1 opt-in): the same through the published API (imported as @quantus-network/wormhole via the file: dev dependency), plus storage keys and extrinsic hashes against @polkadot/util-crypto.
  • examples/consumer on quantus-node --dev: ML-DSA-87 and ML-DSA-65 accounts from one mnemonic; both funded; a transfer sent from each (7301-byte and 5343-byte extrinsics, both included); 5 DEV deposited from each into the wormhole; both deposits proved (38 ms, 27 ms) and aggregated into one private batch with two exits (2.6 s); verify_private_batch accepted; both nullifiers used; each account credited 4.99 DEV.

Follow-ups

  • Windows is not in the build matrix yet.

Example tests
image

@n13 n13 changed the title Add the wormhole entry point: proving on all cores, verification, settlement Wormhole: native addon for proving (all 3 layers), JS API in this package Sep 12, 2026
@n13 n13 changed the title Wormhole: native addon for proving (all 3 layers), JS API in this package Wormhole as its own package: @quantus-network/wormhole (native addon, all 3 layers) Sep 12, 2026
n13 added 2 commits September 12, 2026 20:04
…, settlement

The wormhole for JS as a separate, optional package; @quantus-network/wasm
stays signing-only and its README points to the companion package.

wormhole/core is pure Rust: deposit keys (address, HD derivation,
nullifier), ZK-leaf decoding and Merkle path preparation, leaf (layer 0)
and private-batch (layer 1) proving, public-batch (layer 2) proving
behind the public-batch feature, verification and parsing of all three
layers with the verifier artifacts the runtime embeds, and the unsigned
verify_private_batch / verify_public_batch extrinsics plus the
UsedNullifiers storage key. build.rs generates the artifacts with the
same generator and sizing as pallet-wormhole and, with the feature,
proves the layer-2 padding batch.

wormhole/native is the napi-rs addon over it with a typed ESM wrapper,
published as @quantus-network/wormhole with per-platform binaries as
optional dependencies. Proving runs off the event loop on every core.
Circuit crates are pinned to the versions the runtime verifies against.

Native rather than wasm: wasm32 cannot address the 7 GB a public batch
needs, wasm threads needed a nightly toolchain and custom worker glue,
and a private batch still took 18 s against 4 s natively.

Tests prove and aggregate the circuits repo's reference deposit, verify
the runtime's private_batch.hex / public_batch.hex fixtures, aggregate a
public batch (opt-in), decode the extrinsics as wormhole calls, and check
storage keys and hashes against polkadot.js. The Native addon workflow
builds Linux x64/arm64/musl and macOS arm64/x64 and publishes on release.
A standalone project depending on @quantus-network/wasm and
@quantus-network/wormhole (file: links here, npm for users). Against a
node it derives ML-DSA-87 and ML-DSA-65 accounts from one mnemonic,
funds both and sends from each so both schemes verify on chain,
deposits from each into the wormhole, proves both deposits, aggregates
them into one private batch with two exits, settles it, and checks the
nullifiers and balances. --offline runs the derivation, signing and
fixture-verification parts only.
n13 added 2 commits September 12, 2026 20:18
Tag wormhole releases wormhole-vX.Y.Z so publish.yml (wasm, vX.Y.Z) and
native.yml (wormhole) no longer both fire on every GitHub Release; each
workflow checks the tag prefix and native.yml verifies the tag against
wormhole/native/package.json.

napi prepublish already publishes the platform packages, so drop the
extra publish loop that would have failed on the second attempt.

wormhole/native/scripts/create-release.sh bumps package.json, the
platform packages and optionalDependencies, commits, tags, pushes and
creates the release; CREATE_RELEASE.md covers the npm Trusted Publishing
setup and the manual first publish each new package name needs.
They point at packages that do not exist on npm until the first release,
so npm install left them out of the lock file and npm ci then refused
to install ("Missing ... from lock file"), failing every CI job at
setup. napi prepublish rebuilds optionalDependencies from the targets
and writes them into package.json right before npm publish, so the
committed manifest does not need them.
@n13 n13 added the bot-review label Sep 12, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer model: GPT-6 Astra

Verdict (advisory): Request changes

Reviewed head 058db6f2f2957b6bfb1a5823446397c9c1d286d7 against base c459d40da40041935ef047d030f82efe959edee0.

  1. [P1] Replace the retired Intel macOS runner.github/workflows/native.yml:37-38. GitHub retired macos-13 in December 2025 (official notice). This matrix entry cannot run on a supported hosted runner; it is still queued in this PR's workflow while the other four platform builds have passed. Because publish has needs: build, it also blocks publication of every platform. Use a supported Intel image such as macos-15-intel, retaining the x64 build and smoke test.

  2. [P2] Reject invalid amounts before N-API coerces them to u32wormhole/native/js/index.mts:249-254. Amounts and fee rates pass directly into Rust u32 fields, so N-API silently truncates/wraps them. Reproduced through the public API: dequantize(-1) returns 42949672950000000000n, outputAfterFee(10000, 2 ** 32) returns 10000, and proveLeaf({ ...fixture, outputAmount1: NaN, volumeFeeBps: 4 }) succeeds. Aggregating that leaf produces a cryptographically valid private batch with the deposit's nullifier and zero total payout. A bad caller calculation can therefore become a spend proof for a different amount instead of an error. Add a shared finite-integer/u32-range guard before the native boundary, apply it to both output amounts and the other u32 parameters (including the amount helpers), and test NaN, fractions, negative values, and overflow.

Validation:

  • git diff --check, locked Cargo metadata, TypeScript compilation, release-script syntax, and native-package tarball contents passed.
  • cargo test --manifest-path wormhole/Cargo.toml --locked --release -p quantus-wormhole-core: 11 passed.
  • cargo build --manifest-path wormhole/Cargo.toml --locked --release -p quantus-wormhole-native: passed; the locally built addon also reproduced the amount-coercion issue.
  • WORMHOLE_TEST_PUBLIC_BATCH=1 node --test test/wormhole.test.mjs: 10 passed, none skipped, using the macOS arm64 CI artifact associated with this head.
  • Additional API probes confirmed the numeric-coercion issue; malformed/oversized proof blobs and an inconsistent header were rejected.

Live-chain settlement was not exercised; the root CI test job was still running at the final status check.

@n13 n13 removed the bot-review label Sep 12, 2026
…cos-13 runner

N-API converts JS numbers to u32 with ToUint32 semantics, so NaN became 0,
-1 became 4294967295 and 2**32 became 0. A caller's bad output amount
could turn into a valid proof for a different amount (NaN -> a zero
payout that still spends the nullifier). Every u32 crossing into the
addon is now checked to be an integer in range, and Merkle positions
to be 0-3, with tests for NaN, fractions, negatives and overflow.

GitHub retired macos-13 in December 2025; the x86_64-apple-darwin build
moves to macos-15-intel.
@n13 n13 added the bot-review label Sep 12, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer model: GPT-6 Astra

Verdict (advisory): Request changes

Reviewed head e66c84da520ca198dada13f604f40c85f88a131b against base c459d40da40041935ef047d030f82efe959edee0. The earlier numeric-coercion and retired-macOS-runner findings are addressed; one release-flow issue remains.

[P2] Push the release tag before creating the GitHub Releasewormhole/native/scripts/create-release.sh:23-25. With default Git settings, git tag "$tag" creates a lightweight tag, whereas git push --follow-tags only adds annotated tags to the refs being pushed. Consequently, this script pushes the version commit but leaves the new wormhole-vX.Y.Z tag local, and the following gh release create --verify-tag aborts because the tag is absent from GitHub. The documented release command therefore cannot trigger native-package publication. Create an annotated tag with an explicit message, or explicitly push the named tag before creating the release; the script should work without relying on a maintainer's tag-signing configuration.

Validation:

  • cargo test --manifest-path wormhole/Cargo.toml --locked --release -p quantus-wormhole-core: 11 passed.
  • cargo build --manifest-path wormhole/Cargo.toml --locked --release -p quantus-wormhole-native: passed on macOS arm64.
  • WORMHOLE_TEST_PUBLIC_BATCH=1 node --test test/wormhole.test.mjs, using that locally built addon: 11 passed, none skipped, including all three proving layers and the new numeric-input regression.
  • npm ci --ignore-scripts, native TypeScript compilation, locked Cargo metadata, native-package tarball contents, git diff --check, and shell/JS syntax checks passed.

Release behavior was assessed statically against the Git/gh command semantics; no release script, publication, or live-chain settlement was executed.

CI at the final head check: all three Linux builds and the Intel macOS build passed; the macOS arm64 build and root CI test job were still running.

@n13 n13 removed the bot-review label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant