feat: add ML-DSA-65 as a second transaction signature scheme - #635
Conversation
Users can now sign transactions with either ML-DSA-87 (unchanged) or ML-DSA-65. DilithiumSignatureScheme/DilithiumSigner gain a Dilithium65 variant, appended after Dilithium so the ML-DSA-87 wire format and variant index are unchanged. - Pin qp-rusty-crystals-dilithium/-hdwallet to upstream main rev 6e2d91eb (crates.io 3.0.1 has no ML-DSA-65; the ml-dsa-44/65/87 feature split is not published yet), enabling ml-dsa-65 + ml-dsa-87 - qp-dilithium-crypto: a single define_dilithium_scheme! macro now generates pair, public/signature types, SignatureWithPublic, trait impls and the verify fn for both parameter sets, replacing the hand-written ML-DSA-87-only code; Verify matches both variants - Adapt to new upstream API: private Keypair fields, validating Keypair::from_parts, SecretKey::sign (litep2p node keys included) - Runtime: re-export the 65 types, bump spec_version 137 -> 138 - CLI: --scheme dilithium65 for key/sign/verify commands - Tests: pair-level 65 sign/verify/failure cases, mnemonic HD derivation for both schemes, extrinsic encode/decode/verify round-trips, and end-to-end Executive::apply_extrinsic with an ML-DSA-65-signed transfer (accepted; wrong signer rejected)
Code ReviewReviewed by Claude Fable 5 (review requested by the author). OverviewThis PR adds ML-DSA-65 as a second transaction signature scheme alongside the existing ML-DSA-87, by appending a Correctness ✅
Code quality ✅
Test coverage ✅Unit tests (sign/verify, corrupted signature, wrong message, mismatched Findings (minor)
None of these block merging; only #1 is worth addressing soon since the test currently asserts less than its name claims. Verdict: ✅ ApproveWire-format compatibility for existing ML-DSA-87 transactions is preserved, the account-derivation and verification binding are sound for both parameter sets, the macro consolidation removes a real drift risk, and the E2E coverage through |
The ML-DSA-87 scheme now carries its strength in the name, matching Dilithium65. The enum variant stays in slot 0, so the extrinsic wire format is unchanged (SCALE encodes variants by index) and the enum is not used in any storage item. - DilithiumSignatureScheme::Dilithium -> Dilithium87 (same for DilithiumSigner); types DilithiumPair/Public/Signature/ SignatureWithPublic/CryptoTag -> Dilithium87*; verify -> verify_ml_dsa_87 - CLI: --scheme dilithium87 with a 'dilithium' alias so existing scripts keep working - Runtime metadata gains the new variant names; metadata-generated clients must regenerate (same release as the Dilithium65 addition)
Both qp-rusty-crystals-dilithium and qp-rusty-crystals-hdwallet 4.0.0 are now on crates.io with the ml-dsa-44/65/87 feature split, so the temporary git pin is no longer needed. Adapt to 4.0.0 API changes: - Keypair::generate / WormholePair::generate_new now take &mut SensitiveBytes32 - mnemonic_to_seed now writes into a caller-provided &mut SensitiveBytes64 (SensitiveBytes64::zeroed()) instead of returning the seed
The merge of main resolved the scheme macro in favor of the branch, silently dropping three changes from the V12 dilithium PR (#636) whose tests were kept: Zeroize/ZeroizeOnDrop on the pair, a from_phrase that returns a seed which reconstructs the same pair, and from_string_with_seed exposing that seed. Port all three into define_dilithium_scheme! (adapted to the hdwallet 4.0.0 API) and rename DilithiumPair to Dilithium87Pair in the merged-in tests. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Users can now sign transactions with either ML-DSA-87 (unchanged) or ML-DSA-65.
DilithiumSignatureScheme/DilithiumSignergain aDilithium65variant, appended afterDilithium, so the ML-DSA-87 wire format and SCALE variant index are unchanged.Changes
qp-rusty-crystals-dilithium/-hdwalletto upstreammainrev6e2d91eb— crates.io 3.0.1 ships only ML-DSA-87; theml-dsa-44/65/87feature split is not published yet. Featuresml-dsa-65+ml-dsa-87enabled. Once a release with ML-DSA-65 is published, this can move back to a versioned dep.qp-dilithium-crypto: a singledefine_dilithium_scheme!macro now generates pair, public/signature types,SignatureWithPublic, trait impls and the verify fn for both parameter sets (replacing the hand-written 87-only code, so the schemes can't drift).Verifymatches both variants; account derivation (Poseidon hash of pubkey) is identical for both.Keypairfields, validatingKeypair::from_parts(public/secret correspondence enforced),SecretKey::sign— adapted in primitives, litep2p node keys, and tests.spec_version137 → 138.--scheme dilithium65for key/sign/verify commands.Testing
qp-dilithium-crypto(12): 65 sign/verify, corrupted sig, wrong message, mismatchedfrom_raw, mnemonic HD derivation for both schemesquantus-runtime(28): 65 extrinsic encode/decode/verify round-trips + end-to-endExecutive::apply_extrinsicof an ML-DSA-65-signed transfer through the fullTxExtensionpipeline (accepted; wrong signer rejected)pallet-wormhole(100),sc-cli(51),quantus-node(34): all passcargo check --workspace --all-targetsclean; no_std wasm runtime buildsNote: 9 litep2p TCP/websocket transport tests fail locally on machines whose system DNS nameserver is unparseable (e.g.
fe80::…%en0) — they bypass theresolver_config_or_fallbackfrom #634 by building resolvers directly in test code. Pre-existing, unrelated; will be fixed in a follow-up PR.Notes for integrators
Dilithium65enum variant — downstream tooling (JS/SDK type definitions) should be updated.qp-dilithium-cryptogains a breaking API change (enum variant + new types) and should get a minor version bump at its next release perDILITHIUM_RELEASE_PROCESS.md.