From 9015a93294cd192771ff378f22d4a2d6783d796a Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 31 Jul 2026 18:47:12 +0800 Subject: [PATCH 01/10] feat: add ML-DSA-65 as a second transaction signature scheme 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) --- Cargo.lock | 6 +- Cargo.toml | 6 +- client/cli/src/arg_enums.rs | 4 +- client/cli/src/commands/utils.rs | 3 + client/litep2p/src/crypto/dilithium.rs | 2 +- docs/RUNTIME_SURFACE.md | 4 +- primitives/dilithium-crypto/src/lib.rs | 8 +- primitives/dilithium-crypto/src/pair.rs | 267 ++++------ .../dilithium-crypto/src/scheme_macro.rs | 501 ++++++++++++++++++ primitives/dilithium-crypto/src/traits.rs | 234 ++------ primitives/dilithium-crypto/src/types.rs | 162 ++---- runtime/src/lib.rs | 7 +- runtime/tests/transactions/integration.rs | 119 ++++- runtime/tests/transactions/ml_dsa_65.rs | 115 ++++ runtime/tests/transactions/mod.rs | 1 + 15 files changed, 914 insertions(+), 525 deletions(-) create mode 100644 primitives/dilithium-crypto/src/scheme_macro.rs create mode 100644 runtime/tests/transactions/ml_dsa_65.rs diff --git a/Cargo.lock b/Cargo.lock index 78a9aaa6d..4fe272896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7581,8 +7581,7 @@ checksum = "55e32db7b3a5d70086f82f198ecfc021f17cb7ec70a416512dba3488f4a116c1" [[package]] name = "qp-rusty-crystals-dilithium" version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f6eb664c03395b3892f7f18d4f08bff0e256a967bb0146598a0f771d5e99ba" +source = "git+https://github.com/Quantus-Network/qp-rusty-crystals?rev=6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1#6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1" dependencies = [ "zeroize", ] @@ -7590,8 +7589,7 @@ dependencies = [ [[package]] name = "qp-rusty-crystals-hdwallet" version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9a9d975e8bfa2b6726f9668f32bd134a2556ccf65f4d336fed941658e4db67" +source = "git+https://github.com/Quantus-Network/qp-rusty-crystals?rev=6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1#6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1" dependencies = [ "bip39", "getrandom 0.2.17", diff --git a/Cargo.toml b/Cargo.toml index 69154a1aa..716a3e9df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -233,8 +233,10 @@ sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = f qp-plonky2 = { version = "1.5.4", default-features = false } qp-plonky2-verifier = { version = "1.5.4", default-features = false } qp-poseidon-core = { version = "3.0.2", default-features = false } -qp-rusty-crystals-dilithium = { version = "3.0.1", default-features = false } -qp-rusty-crystals-hdwallet = { version = "3.0.1" } +# Git pin: crates.io 3.0.1 ships only ML-DSA-87; the ml-dsa-44/65/87 feature split +# lives on qp-rusty-crystals main and is not published yet. +qp-rusty-crystals-dilithium = { git = "https://github.com/Quantus-Network/qp-rusty-crystals", rev = "6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1", default-features = false, features = ["ml-dsa-65", "ml-dsa-87"] } +qp-rusty-crystals-hdwallet = { git = "https://github.com/Quantus-Network/qp-rusty-crystals", rev = "6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1", features = ["ml-dsa-65"] } qp-wormhole-aggregator = { version = "3.1.0", default-features = false } qp-wormhole-circuit = { version = "3.1.0", default-features = false } qp-wormhole-circuit-builder = { version = "3.1.0", default-features = false } diff --git a/client/cli/src/arg_enums.rs b/client/cli/src/arg_enums.rs index 4a0bfa035..3bff11d62 100644 --- a/client/cli/src/arg_enums.rs +++ b/client/cli/src/arg_enums.rs @@ -122,8 +122,10 @@ pub enum NodeKeyType { #[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] #[value(rename_all = "kebab-case")] pub enum CryptoScheme { - /// Use dilithium. + /// Use dilithium (ML-DSA-87). Dilithium, + /// Use dilithium ML-DSA-65. + Dilithium65, } /// The type of the output format. diff --git a/client/cli/src/commands/utils.rs b/client/cli/src/commands/utils.rs index fa2bb1084..2703d4066 100644 --- a/client/cli/src/commands/utils.rs +++ b/client/cli/src/commands/utils.rs @@ -293,6 +293,9 @@ macro_rules! with_crypto_scheme { $crate::CryptoScheme::Dilithium => { $method::($($params),*) } + $crate::CryptoScheme::Dilithium65 => { + $method::($($params),*) + } } }; } diff --git a/client/litep2p/src/crypto/dilithium.rs b/client/litep2p/src/crypto/dilithium.rs index 682c62aa9..fa43e7b21 100644 --- a/client/litep2p/src/crypto/dilithium.rs +++ b/client/litep2p/src/crypto/dilithium.rs @@ -121,7 +121,7 @@ impl Keypair { /// Get the public key of this keypair. pub fn public(&self) -> PublicKey { - PublicKey(self.derive_internal().public) + PublicKey(self.derive_internal().public().clone()) } /// Get the secret key (seed) of this keypair. diff --git a/docs/RUNTIME_SURFACE.md b/docs/RUNTIME_SURFACE.md index 6e58cc136..fb203a47b 100644 --- a/docs/RUNTIME_SURFACE.md +++ b/docs/RUNTIME_SURFACE.md @@ -11,7 +11,7 @@ genesis logic, and the workspace primitive crates pulled in. - **Build:** `no_std` WASM via `substrate-wasm-builder` (`runtime/build.rs`); native `std` build for the node/client - **Block time target:** 12s (`TARGET_BLOCK_TIME_MS = 12_000`) - **Consensus:** QPoW (quantum-resistant Proof of Work, Poseidon2-based) -- **Signatures:** Dilithium (ML-DSA-87) post-quantum signature scheme +- **Signatures:** Dilithium post-quantum signature schemes (ML-DSA-87 and ML-DSA-65) - **SS58 prefix:** 189 --- @@ -281,7 +281,7 @@ Related transaction-payment RPC surface (patched for WASM + node builds): | Crate | Path | Role in runtime | | --- | --- | --- | -| `qp-dilithium-crypto` | `primitives/dilithium-crypto` | ML-DSA-87 post-quantum signatures; `DilithiumSignatureScheme` = the chain's `Signature`/`AccountId`. | +| `qp-dilithium-crypto` | `primitives/dilithium-crypto` | ML-DSA-87/ML-DSA-65 post-quantum signatures; `DilithiumSignatureScheme` = the chain's `Signature`/`AccountId`. | | `qp-header` | `primitives/header` | Custom block `Header` (Poseidon block hash + Blake2 state trie); `ZkTreeRootProvider` trait. | | `qp-high-security` | `primitives/high-security` | `HighSecurityInspector` trait shared by multisig, reversible-transfers, tx-extensions (breaks circular dep). | | `qp-scheduler` | `primitives/scheduler` | `BlockNumberOrTimestamp`, `DispatchTime`, `ScheduleNamed` trait for delayed dispatch. | diff --git a/primitives/dilithium-crypto/src/lib.rs b/primitives/dilithium-crypto/src/lib.rs index 31d4befb3..05f95e1d0 100644 --- a/primitives/dilithium-crypto/src/lib.rs +++ b/primitives/dilithium-crypto/src/lib.rs @@ -3,6 +3,7 @@ extern crate alloc; pub mod pair; +mod scheme_macro; pub mod traits; pub mod types; @@ -13,8 +14,9 @@ pub const SECRET_KEY_BYTES: usize = ml_dsa_87::SECRETKEYBYTES; pub const SIGNATURE_BYTES: usize = ml_dsa_87::SIGNBYTES; pub use pair::{create_keypair, crystal_alice, crystal_charlie, dilithium_bob, generate}; -pub use traits::verify; pub use types::{ - DilithiumPair, DilithiumPublic, DilithiumSignature, DilithiumSignatureScheme, - DilithiumSignatureWithPublic, DilithiumSigner, WrappedPublicBytes, WrappedSignatureBytes, + verify, verify_ml_dsa_65, Dilithium65CryptoTag, Dilithium65Pair, Dilithium65Public, + Dilithium65Signature, Dilithium65SignatureWithPublic, DilithiumCryptoTag, DilithiumPair, + DilithiumPublic, DilithiumSignature, DilithiumSignatureScheme, DilithiumSignatureWithPublic, + DilithiumSigner, WrappedPublicBytes, WrappedSignatureBytes, }; diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 853326869..e6157f940 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -1,20 +1,10 @@ -use crate::{DilithiumSignatureScheme, DilithiumSignatureWithPublic, DilithiumSigner}; - -use super::types::{DilithiumPair, DilithiumPublic}; -use alloc::vec::Vec; +use super::types::DilithiumPair; use qp_rusty_crystals_dilithium::{ ml_dsa_87::{Keypair, PublicKey, SecretKey}, params::SEEDBYTES, SensitiveBytes32, }; -use sp_core::{ - crypto::{DeriveError, DeriveJunction, SecretStringError}, - ByteArray, Pair, -}; -use sp_runtime::{ - traits::{IdentifyAccount, Verify}, - AccountId32, -}; +use sp_core::Pair; pub fn crystal_alice() -> DilithiumPair { let seed = [0u8; 32]; @@ -29,161 +19,6 @@ pub fn crystal_charlie() -> DilithiumPair { DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") } -impl IdentifyAccount for DilithiumPair { - type AccountId = AccountId32; - fn into_account(self) -> AccountId32 { - self.public().into_account() - } -} - -impl Pair for DilithiumPair { - type Public = DilithiumPublic; - type Seed = [u8; 32]; - type Signature = DilithiumSignatureWithPublic; - type ProofOfPossession = DilithiumSignatureWithPublic; - - // Dilithium doesn't support path-based derivation - use hdwallet in qp-rusty-crystals instead. - // However, an empty path is a valid no-op that returns self unchanged. - fn derive>( - &self, - path_iter: Iter, - seed: Option<::Seed>, - ) -> Result<(Self, Option<::Seed>), DeriveError> { - // Check if any junctions are present - empty path is a no-op - let mut path_iter = path_iter.peekable(); - if path_iter.peek().is_none() { - return Ok((self.clone(), seed)); - } - - log::error!( - "Pair::derive() is not supported for Dilithium. \ - Use qp_rusty_crystals_hdwallet::derive_key_from_mnemonic() for HD key derivation." - ); - // SoftKeyInPath is the only error available for this trait definition. - Err(DeriveError::SoftKeyInPath) - } - - fn from_seed_slice(seed: &[u8]) -> Result { - DilithiumPair::from_seed(seed).map_err(|_| SecretStringError::InvalidSeed) - } - - #[cfg(feature = "full_crypto")] - fn sign(&self, message: &[u8]) -> DilithiumSignatureWithPublic { - // Create keypair struct - - use crate::types::DilithiumSignature; - let keypair = create_keypair(&self.public, &self.secret).expect("Failed to create keypair"); - - // Sign the message - let signature = keypair.sign(message, None, None).expect("Signing should not fail"); - - let signature = - DilithiumSignature::try_from(signature.as_ref()).expect("Wrap doesn't fail"); - - DilithiumSignatureWithPublic::new(signature, self.public()) - } - - fn verify>( - sig: &DilithiumSignatureWithPublic, - message: M, - pubkey: &DilithiumPublic, - ) -> bool { - let sig_scheme = DilithiumSignatureScheme::Dilithium(sig.clone()); - let signer = DilithiumSigner::Dilithium(pubkey.clone()); - sig_scheme.verify(message.as_ref(), &signer.into_account()) - } - - fn public(&self) -> Self::Public { - DilithiumPublic::from_slice(&self.public).expect("Valid public key bytes") - } - - fn to_raw_vec(&self) -> Vec { - // this is modeled after sr25519 which returns the private key for this method - self.secret.to_vec() - } - - // NOTE: This method does not parse all secret uris correctly, like - // "mnemonic///password///account" This was supported in standard substrate, if there is - // demand, we can support it in the future - fn from_string(s: &str, password_override: Option<&str>) -> Result { - let res = Self::from_phrase(s, password_override) - .map_err(|_| SecretStringError::InvalidPhrase)?; - Ok(res.0) - } - - #[cfg(feature = "std")] - fn from_phrase( - phrase: &str, - password: Option<&str>, - ) -> Result<(Self, Self::Seed), SecretStringError> { - use qp_rusty_crystals_hdwallet::{derive_key_from_mnemonic, mnemonic_to_seed}; - // Default derivation path for Quantus: m/44'/189189'/0'/0'/0' - const DEFAULT_PATH: &str = "m/44'/189189'/0'/0'/0'"; - let keypair = derive_key_from_mnemonic(phrase, password, DEFAULT_PATH) - .map_err(|_| SecretStringError::InvalidPhrase)?; - let pair = DilithiumPair::from_keypair(keypair); - let seed_bytes = mnemonic_to_seed(phrase.to_string(), password) - .map_err(|_| SecretStringError::InvalidPhrase)?; - let mut seed = [0u8; 32]; - seed.copy_from_slice(&seed_bytes[..32]); - Ok((pair, seed)) - } - - #[cfg(feature = "std")] - fn from_string_with_seed( - s: &str, - password: Option<&str>, - ) -> Result<(Self, Option), SecretStringError> { - use qp_rusty_crystals_hdwallet::derive_key_from_mnemonic; - // Default derivation path for Quantus: m/44'/189189'/0'/0'/0' - const DEFAULT_PATH: &str = "m/44'/189189'/0'/0'/0'"; - // For Dilithium, we use the string directly as entropy for key generation - // We combine the string with the password if provided - let keypair = derive_key_from_mnemonic(s, password, DEFAULT_PATH) - .map_err(|_| SecretStringError::InvalidPhrase)?; - let pair = DilithiumPair::from_keypair(keypair); - - // Return the pair with no seed since Dilithium doesn't use traditional seed-based - // generation - Ok((pair, None)) - } -} - -#[cfg(feature = "std")] -impl DilithiumPublic { - /// Attempt to parse a Dilithium public key from a string and return it with the - /// associated SS58 address format (version). - /// - /// This inherent method is provided to avoid relying solely on the generic Ss58Codec - /// behavior which expects SS58-encoded keys of the same length as the public key. - /// For Dilithium, we primarily support hex-encoded public keys (0x-prefixed) here. - /// - /// Note: SS58 AccountId32 addresses are not convertible back into Dilithium public - /// keys. The CLI already includes a fallback to parse AccountId32 addresses when - /// this function returns an error. - pub fn from_string_with_version( - s: &str, - ) -> Result<(Self, sp_core::crypto::Ss58AddressFormat), sp_core::crypto::PublicError> { - use sp_core::crypto::{default_ss58_version, PublicError}; - // Accept 0x-prefixed hex of the raw Dilithium public key bytes. - let maybe_hex = s.strip_prefix("0x").unwrap_or(s); - // Expect exact hex length for a Dilithium public key - let expected_hex_len = ::LEN * 2; - if maybe_hex.len() == expected_hex_len && maybe_hex.chars().all(|c| c.is_ascii_hexdigit()) { - let mut bytes = vec![0u8; ::LEN]; - for (i, chunk) in maybe_hex.as_bytes().chunks(2).enumerate() { - let h = (chunk[0] as char).to_digit(16).ok_or(PublicError::InvalidFormat)? as u8; - let l = (chunk[1] as char).to_digit(16).ok_or(PublicError::InvalidFormat)? as u8; - bytes[i] = (h << 4) | l; - } - let pk = ::from_slice(&bytes).map_err(|_| PublicError::BadLength)?; - return Ok((pk, default_ss58_version())); - } - // Not a supported Dilithium public key representation here. - Err(PublicError::InvalidFormat) - } -} - /// Generates a new Dilithium ML-DSA-87 keypair /// /// # Arguments @@ -228,13 +63,16 @@ pub fn create_keypair( let public = PublicKey::from_bytes(public_key).map_err(|_| crate::types::Error::InvalidPublicKey)?; - let keypair = Keypair { secret, public }; + // from_parts also validates that the public key corresponds to the secret. + let keypair = Keypair::from_parts(secret, public).map_err(|_| crate::types::Error::InvalidPublicKey)?; Ok(keypair) } #[cfg(test)] mod tests { use super::*; + use crate::{Dilithium65Pair, DilithiumSignatureWithPublic}; + use sp_core::ByteArray; fn setup() { // Initialize the logger once per test run @@ -335,4 +173,97 @@ mod tests { let result = DilithiumPair::from_raw(pair2.public().as_ref(), pair1.secret_bytes()); assert!(result.is_err(), "Mismatched public/secret should be rejected"); } + + #[test] + fn test_ml_dsa_65_sign_and_verify() { + let seed = [0u8; 32]; + let pair = Dilithium65Pair::from_seed_slice(&seed).expect("Failed to create pair"); + let message = b"Something"; + let signature = pair.sign(message); + let public = pair.public(); + + assert!(Dilithium65Pair::verify(&signature, message, &public)); + } + + #[test] + fn test_ml_dsa_65_wrong_message_fails() { + let seed = [0u8; 32]; + let pair = Dilithium65Pair::from_seed(&seed).expect("Failed to create pair"); + let signature = pair.sign(b"Hello, world!"); + let public = pair.public(); + + assert!( + !Dilithium65Pair::verify(&signature, b"Goodbye, world!", &public), + "Signature should not verify with wrong message" + ); + } + + #[test] + fn test_ml_dsa_65_wrong_signature_fails() { + let seed = [0u8; 32]; + let pair = Dilithium65Pair::from_seed(&seed).expect("Failed to create pair"); + let message = b"Hello, world!"; + + let mut signature = pair.sign(message); + let signature_bytes = signature.as_mut(); + if let Some(byte) = signature_bytes.get_mut(0) { + *byte ^= 1; + } + let false_signature = + crate::Dilithium65SignatureWithPublic::from_slice(signature_bytes) + .expect("Failed to create signature"); + let public = pair.public(); + + assert!( + !Dilithium65Pair::verify(&false_signature, message, &public), + "Corrupted signature should not verify" + ); + } + + #[test] + fn test_ml_dsa_65_from_raw_mismatched_keys_fails() { + let pair1 = Dilithium65Pair::from_seed(&[0u8; 32]).expect("Failed to create pair1"); + let pair2 = Dilithium65Pair::from_seed(&[1u8; 32]).expect("Failed to create pair2"); + let result = Dilithium65Pair::from_raw(pair2.public().as_ref(), pair1.secret_bytes()); + assert!(result.is_err(), "Mismatched public/secret should be rejected"); + } + + #[test] + fn test_schemes_produce_different_accounts() { + let seed = [0u8; 32]; + let pair87 = DilithiumPair::from_seed(&seed).expect("Failed to create 87 pair"); + let pair65 = Dilithium65Pair::from_seed(&seed).expect("Failed to create 65 pair"); + + assert_ne!( + pair87.public().as_ref().len(), + pair65.public().as_ref().len(), + "ML-DSA-87 and ML-DSA-65 public keys must differ in size" + ); + } + + #[test] + fn test_from_phrase_works_for_both_schemes() { + // Well-known test mnemonic; proves HD derivation is wired up for both parameter sets + // (this is the path the CLI key commands use). + let phrase = "legal winner thank year wave sausage worth useful legal winner thank yellow"; + + let (pair87, _) = DilithiumPair::from_phrase(phrase, None).expect("87 from_phrase failed"); + let (pair87_again, _) = + DilithiumPair::from_phrase(phrase, None).expect("87 from_phrase failed"); + assert_eq!(pair87.public(), pair87_again.public(), "87 derivation must be deterministic"); + + let (pair65, _) = + Dilithium65Pair::from_phrase(phrase, None).expect("65 from_phrase failed"); + let (pair65_again, _) = + Dilithium65Pair::from_phrase(phrase, None).expect("65 from_phrase failed"); + assert_eq!(pair65.public(), pair65_again.public(), "65 derivation must be deterministic"); + + // Same mnemonic, different parameter sets -> different key material. + assert_ne!(pair87.public().as_ref(), pair65.public().as_ref()); + + // The derived 65 pair must produce valid signatures. + let message = b"mnemonic-derived key"; + let signature = pair65.sign(message); + assert!(Dilithium65Pair::verify(&signature, message, &pair65.public())); + } } diff --git a/primitives/dilithium-crypto/src/scheme_macro.rs b/primitives/dilithium-crypto/src/scheme_macro.rs new file mode 100644 index 000000000..2f627980d --- /dev/null +++ b/primitives/dilithium-crypto/src/scheme_macro.rs @@ -0,0 +1,501 @@ +/// Generates a complete Dilithium signature scheme (tag, pair, public/signature +/// aliases, signature-with-public type, trait impls and a raw verify function) +/// for one `qp_rusty_crystals_dilithium` parameter-set module (e.g. `ml_dsa_87`). +/// +/// All variants feed the shared `DilithiumSignatureScheme` / `DilithiumSigner` +/// enums through the `variant` enum case name. +macro_rules! define_dilithium_scheme { + ( + variant = $variant:ident, + tag = $tag:ident, + pair = $pair:ident, + public = $public:ident, + signature = $signature:ident, + sig_with_public = $sig_with_public:ident, + module = $module:ident, + verify_fn = $verify_fn:ident + $(,)? + ) => { + #[derive( + Clone, + Eq, + PartialEq, + Debug, + Hash, + codec::Encode, + codec::Decode, + scale_info::TypeInfo, + Ord, + PartialOrd, + codec::DecodeWithMemTracking, + )] + pub struct $tag; + + /// Dilithium cryptographic key pair + /// + /// Contains both secret and public key material for this parameter set. + #[derive(Clone, Eq, PartialEq)] + pub struct $pair { + pub(crate) secret: [u8; qp_rusty_crystals_dilithium::$module::SECRETKEYBYTES], + pub(crate) public: [u8; qp_rusty_crystals_dilithium::$module::PUBLICKEYBYTES], + } + + impl alloc::fmt::Debug for $pair { + fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { + f.debug_struct(stringify!($pair)) + .field("public", &self.public) + .finish_non_exhaustive() + } + } + + pub type $public = $crate::types::WrappedPublicBytes< + { qp_rusty_crystals_dilithium::$module::PUBLICKEYBYTES }, + $tag, + >; + pub type $signature = $crate::types::WrappedSignatureBytes< + { qp_rusty_crystals_dilithium::$module::SIGNBYTES }, + $tag, + >; + + /// Combined signature and public key structure. + /// + /// Required because the runtime address is a hash of the public key, so the + /// signature must carry the public key along. Byte layout: + /// `[signature_bytes][public_key_bytes]`. + #[derive( + Clone, + Eq, + PartialEq, + Hash, + codec::Encode, + codec::Decode, + scale_info::TypeInfo, + codec::MaxEncodedLen, + Ord, + PartialOrd, + codec::DecodeWithMemTracking, + )] + pub struct $sig_with_public { + /// Raw bytes containing both signature and public key + pub bytes: [u8; $sig_with_public::TOTAL_LEN], + } + + impl $sig_with_public { + const SIGNATURE_LEN: usize = <$signature as sp_core::ByteArray>::LEN; + const PUBLIC_LEN: usize = <$public as sp_core::ByteArray>::LEN; + pub const TOTAL_LEN: usize = Self::SIGNATURE_LEN + Self::PUBLIC_LEN; + + /// Creates a new combined signature and public key structure + pub fn new(signature: $signature, public: $public) -> Self { + let mut bytes = [0u8; ::LEN]; + bytes[..Self::SIGNATURE_LEN].copy_from_slice(signature.as_ref()); + bytes[Self::SIGNATURE_LEN..].copy_from_slice(public.as_ref()); + Self { bytes } + } + + /// Extracts the signature portion + pub fn signature(&self) -> $signature { + <$signature as sp_core::ByteArray>::from_slice(&self.bytes[..Self::SIGNATURE_LEN]) + .expect("Invalid signature") + } + + /// Extracts the public key portion + pub fn public(&self) -> $public { + <$public as sp_core::ByteArray>::from_slice(&self.bytes[Self::SIGNATURE_LEN..]) + .expect("Invalid public key") + } + + /// Returns the raw bytes + pub fn to_bytes(&self) -> [u8; Self::TOTAL_LEN] { + self.bytes + } + + /// Creates from raw bytes containing signature and public key + /// + /// # Errors + /// Returns `Error::InvalidLength` if the byte array is not the expected length + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::TOTAL_LEN { + return Err($crate::types::Error::InvalidLength); + } + + let signature = <$signature as sp_core::ByteArray>::from_slice( + &bytes[..Self::SIGNATURE_LEN], + ) + .map_err(|_| $crate::types::Error::InvalidLength)?; + let public = <$public as sp_core::ByteArray>::from_slice( + &bytes[Self::SIGNATURE_LEN..], + ) + .map_err(|_| $crate::types::Error::InvalidLength)?; + + Ok(Self::new(signature, public)) + } + } + + impl alloc::fmt::Debug for $sig_with_public { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { + write!( + f, + "{} {{ signature: {:?}, public: {:?} }}", + stringify!($sig_with_public), + self.signature(), + self.public() + ) + } + + #[cfg(not(feature = "std"))] + fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { + write!(f, stringify!($sig_with_public)) + } + } + + impl From<$sig_with_public> for $crate::types::DilithiumSignatureScheme { + fn from(x: $sig_with_public) -> Self { + Self::$variant(x) + } + } + + impl TryFrom<$crate::types::DilithiumSignatureScheme> for $sig_with_public { + type Error = (); + fn try_from(m: $crate::types::DilithiumSignatureScheme) -> Result { + match m { + $crate::types::DilithiumSignatureScheme::$variant(sig_with_public) => + Ok(sig_with_public), + _ => Err(()), + } + } + } + + impl AsMut<[u8]> for $sig_with_public { + fn as_mut(&mut self) -> &mut [u8] { + self.bytes.as_mut() + } + } + + impl TryFrom<&[u8]> for $sig_with_public { + type Error = (); + fn try_from(data: &[u8]) -> Result { + if data.len() != Self::TOTAL_LEN { + return Err(()); + } + let (sig_bytes, pub_bytes) = data.split_at(Self::SIGNATURE_LEN); + let signature = + <$signature as sp_core::ByteArray>::from_slice(sig_bytes).map_err(|_| ())?; + let public = <$public as sp_core::ByteArray>::from_slice(pub_bytes).map_err(|_| ())?; + Ok(Self::new(signature, public)) + } + } + + impl sp_core::ByteArray for $sig_with_public { + const LEN: usize = Self::TOTAL_LEN; + + fn to_raw_vec(&self) -> alloc::vec::Vec { + self.to_bytes().to_vec() + } + + fn from_slice(data: &[u8]) -> Result { + if data.len() != Self::LEN { + return Err(()); + } + let bytes = <[u8; Self::LEN]>::try_from(data).map_err(|_| ())?; + Self::from_bytes(&bytes).map_err(|_| ()) + } + + fn as_slice(&self) -> &[u8] { + self.bytes.as_slice() + } + } + + impl AsRef<[u8; ::LEN]> for $sig_with_public { + fn as_ref(&self) -> &[u8; ::LEN] { + &self.bytes + } + } + + impl AsRef<[u8]> for $sig_with_public { + fn as_ref(&self) -> &[u8] { + &self.bytes + } + } + + impl sp_core::crypto::Signature for $sig_with_public {} + + impl sp_runtime::CryptoType for $sig_with_public { + type Pair = $pair; + } + + impl sp_runtime::CryptoType for $crate::types::WrappedPublicBytes { + type Pair = $pair; + } + + impl sp_runtime::CryptoType for $crate::types::WrappedSignatureBytes { + type Pair = $pair; + } + + impl sp_runtime::traits::IdentifyAccount for $public { + type AccountId = sp_runtime::AccountId32; + fn into_account(self) -> Self::AccountId { + // Use injective encoding for account ID derivation (collision-resistant for security) + let public_bytes: &[u8] = self.as_ref(); + sp_runtime::AccountId32::new(qp_poseidon_core::hash_bytes(public_bytes)) + } + } + + impl From<$public> for sp_runtime::AccountId32 { + fn from(public: $public) -> Self { + sp_runtime::traits::IdentifyAccount::into_account(public) + } + } + + impl From<$public> for $crate::types::DilithiumSigner { + fn from(x: $public) -> Self { + Self::$variant(x) + } + } + + impl sp_runtime::CryptoType for $pair { + type Pair = Self; + } + + impl sp_runtime::traits::IdentifyAccount for $pair { + type AccountId = sp_runtime::AccountId32; + fn into_account(self) -> sp_runtime::AccountId32 { + sp_runtime::AccountId32::new(qp_poseidon_core::hash_bytes(&self.public)) + } + } + + impl $pair { + pub fn from_seed(seed: &[u8]) -> Result { + if seed.len() < qp_rusty_crystals_dilithium::params::SEEDBYTES { + return Err($crate::types::Error::InsufficientEntropy { + required: qp_rusty_crystals_dilithium::params::SEEDBYTES, + actual: seed.len(), + }); + } + let mut entropy_array = [0u8; 32]; + entropy_array.copy_from_slice(&seed[..32]); + let keypair = qp_rusty_crystals_dilithium::$module::Keypair::generate( + qp_rusty_crystals_dilithium::SensitiveBytes32::from(&mut entropy_array), + ); + Ok(Self { + secret: *keypair.secret().to_bytes(), + public: keypair.public().to_bytes(), + }) + } + + pub fn from_keypair(keypair: qp_rusty_crystals_dilithium::$module::Keypair) -> Self { + Self { + secret: *keypair.secret().to_bytes(), + public: keypair.public().to_bytes(), + } + } + + /// Create a pair from raw public and secret key bytes. + /// Use when reconstructing a pair from stored/serialized key material (e.g. wallet + /// restore). + /// + /// Rejects mismatched or corrupted key pairs: `Keypair::from_parts` re-derives the + /// public key from the secret and requires an exact match, so a pair that would + /// cause non-obvious signature verification failures downstream is unrepresentable. + pub fn from_raw(public: &[u8], secret: &[u8]) -> Result { + let secret = qp_rusty_crystals_dilithium::$module::SecretKey::from_bytes(secret) + .map_err(|_| $crate::types::Error::InvalidSecretKey)?; + let public = qp_rusty_crystals_dilithium::$module::PublicKey::from_bytes(public) + .map_err(|_| $crate::types::Error::InvalidPublicKey)?; + let keypair = qp_rusty_crystals_dilithium::$module::Keypair::from_parts(secret, public) + .map_err(|_| $crate::types::Error::InvalidPublicKey)?; + Ok(Self { + secret: *keypair.secret().to_bytes(), + public: keypair.public().to_bytes(), + }) + } + + pub fn secret_bytes(&self) -> &[u8] { + &self.secret + } + + pub fn public_bytes(&self) -> &[u8] { + &self.public + } + } + + impl sp_core::Pair for $pair { + type Public = $public; + type Seed = [u8; 32]; + type Signature = $sig_with_public; + type ProofOfPossession = $sig_with_public; + + // Dilithium doesn't support path-based derivation - use hdwallet in + // qp-rusty-crystals instead. An empty path is a valid no-op returning self unchanged. + fn derive>( + &self, + path_iter: Iter, + seed: Option, + ) -> Result<(Self, Option), sp_core::crypto::DeriveError> { + let mut path_iter = path_iter.peekable(); + if path_iter.peek().is_none() { + return Ok((self.clone(), seed)); + } + + log::error!( + "Pair::derive() is not supported for Dilithium. \ + Use qp_rusty_crystals_hdwallet::derive_key_from_mnemonic() for HD key derivation." + ); + // SoftKeyInPath is the only error available for this trait definition. + Err(sp_core::crypto::DeriveError::SoftKeyInPath) + } + + fn from_seed_slice(seed: &[u8]) -> Result { + Self::from_seed(seed).map_err(|_| sp_core::crypto::SecretStringError::InvalidSeed) + } + + #[cfg(feature = "full_crypto")] + fn sign(&self, message: &[u8]) -> $sig_with_public { + let secret = qp_rusty_crystals_dilithium::$module::SecretKey::from_bytes( + &self.secret, + ) + .expect("Failed to parse secret key"); + + let signature = secret.sign(message, None, None).expect("Signing should not fail"); + let signature_bytes: &[u8] = signature.as_ref(); + let signature = <$signature as TryFrom<&[u8]>>::try_from(signature_bytes) + .expect("Wrap doesn't fail"); + + $sig_with_public::new(signature, ::public(self)) + } + + fn verify>( + sig: &$sig_with_public, + message: M, + pubkey: &$public, + ) -> bool { + let sig_scheme = $crate::types::DilithiumSignatureScheme::$variant(sig.clone()); + let signer = $crate::types::DilithiumSigner::$variant(pubkey.clone()); + let account = sp_runtime::traits::IdentifyAccount::into_account(signer); + sp_runtime::traits::Verify::verify(&sig_scheme, message.as_ref(), &account) + } + + fn public(&self) -> Self::Public { + <$public as sp_core::ByteArray>::from_slice(&self.public) + .expect("Valid public key bytes") + } + + fn to_raw_vec(&self) -> alloc::vec::Vec { + // this is modeled after sr25519 which returns the private key for this method + self.secret.to_vec() + } + + // NOTE: This method does not parse all secret uris correctly, like + // "mnemonic///password///account" This was supported in standard substrate, if + // there is demand, we can support it in the future + fn from_string( + s: &str, + password_override: Option<&str>, + ) -> Result { + let res = ::from_phrase(s, password_override) + .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; + Ok(res.0) + } + + #[cfg(feature = "std")] + fn from_phrase( + phrase: &str, + password: Option<&str>, + ) -> Result<(Self, Self::Seed), sp_core::crypto::SecretStringError> { + // Default derivation path for Quantus: m/44'/189189'/0'/0'/0' + const DEFAULT_PATH: &str = "m/44'/189189'/0'/0'/0'"; + let keypair = qp_rusty_crystals_hdwallet::$module::derive_key_from_mnemonic( + phrase, + password, + DEFAULT_PATH, + ) + .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; + let pair = Self::from_keypair(keypair); + let seed_bytes = qp_rusty_crystals_hdwallet::mnemonic_to_seed( + phrase.to_string(), + password, + ) + .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; + let mut seed = [0u8; 32]; + seed.copy_from_slice(&seed_bytes[..32]); + Ok((pair, seed)) + } + + #[cfg(feature = "std")] + fn from_string_with_seed( + s: &str, + password: Option<&str>, + ) -> Result<(Self, Option), sp_core::crypto::SecretStringError> { + // Default derivation path for Quantus: m/44'/189189'/0'/0'/0' + const DEFAULT_PATH: &str = "m/44'/189189'/0'/0'/0'"; + let keypair = qp_rusty_crystals_hdwallet::$module::derive_key_from_mnemonic( + s, + password, + DEFAULT_PATH, + ) + .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; + let pair = Self::from_keypair(keypair); + + // Return the pair with no seed since Dilithium doesn't use traditional + // seed-based generation + Ok((pair, None)) + } + } + + #[cfg(feature = "std")] + impl $public { + /// Attempt to parse a Dilithium public key from a string and return it with the + /// associated SS58 address format (version). + /// + /// We primarily support hex-encoded (0x-prefixed) public keys here; SS58 + /// AccountId32 addresses are not convertible back into Dilithium public keys (the + /// CLI falls back to parsing AccountId32 when this returns an error). + pub fn from_string_with_version( + s: &str, + ) -> Result<(Self, sp_core::crypto::Ss58AddressFormat), sp_core::crypto::PublicError> + { + use sp_core::crypto::{default_ss58_version, PublicError}; + // Accept 0x-prefixed hex of the raw Dilithium public key bytes. + let maybe_hex = s.strip_prefix("0x").unwrap_or(s); + // Expect exact hex length for a Dilithium public key + let expected_hex_len = ::LEN * 2; + if maybe_hex.len() == expected_hex_len && + maybe_hex.chars().all(|c| c.is_ascii_hexdigit()) + { + let mut bytes = alloc::vec![0u8; ::LEN]; + for (i, chunk) in maybe_hex.as_bytes().chunks(2).enumerate() { + let h = (chunk[0] as char) + .to_digit(16) + .ok_or(PublicError::InvalidFormat)? as u8; + let l = (chunk[1] as char) + .to_digit(16) + .ok_or(PublicError::InvalidFormat)? as u8; + bytes[i] = (h << 4) | l; + } + let pk = ::from_slice(&bytes) + .map_err(|_| PublicError::BadLength)?; + return Ok((pk, default_ss58_version())); + } + // Not a supported Dilithium public key representation here. + Err(PublicError::InvalidFormat) + } + } + + /// Verifies a Dilithium signature for this parameter set. + /// + /// Returns `true` if the signature is valid for `msg` under `pub_key`, + /// `false` otherwise (including malformed public keys). + pub fn $verify_fn(pub_key: &[u8], msg: &[u8], sig: &[u8]) -> bool { + match qp_rusty_crystals_dilithium::$module::PublicKey::from_bytes(pub_key) { + Ok(pk) => pk.verify(msg, sig, None), + Err(e) => { + log::warn!("public key failed to deserialize {:?}", e); + false + }, + } + } + }; +} + +pub(crate) use define_dilithium_scheme; diff --git a/primitives/dilithium-crypto/src/traits.rs b/primitives/dilithium-crypto/src/traits.rs index 329f47320..d48d54ce6 100644 --- a/primitives/dilithium-crypto/src/traits.rs +++ b/primitives/dilithium-crypto/src/traits.rs @@ -1,9 +1,5 @@ -use super::types::{ - DilithiumPair, DilithiumPublic, DilithiumSignatureScheme, DilithiumSigner, Error, - WrappedPublicBytes, WrappedSignatureBytes, -}; +use super::types::{DilithiumSignatureScheme, DilithiumSigner, WrappedPublicBytes, WrappedSignatureBytes}; -use crate::{DilithiumSignature, DilithiumSignatureWithPublic}; use alloc::vec::Vec; use qp_poseidon_core::hash_bytes; use sp_core::{ @@ -12,43 +8,9 @@ use sp_core::{ }; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - AccountId32, CryptoType, + AccountId32, }; -/// Verifies a Dilithium ML-DSA-87 signature -/// -/// This function performs signature verification using the Dilithium post-quantum -/// cryptographic signature scheme (ML-DSA-87). It validates that the given signature -/// was created by the holder of the private key corresponding to the public key. -/// -/// # Arguments -/// * `pub_key` - The public key bytes (must be valid Dilithium public key) -/// * `msg` - The message that was signed -/// * `sig` - The signature bytes to verify -/// -/// # Returns -/// `true` if the signature is valid and verification succeeds, `false` otherwise -/// -/// # Examples -/// ```ignore -/// use qp_dilithium_crypto::verify; -/// -/// let valid = verify(&public_key_bytes, &message, &signature_bytes); -/// if valid { -/// println!("Signature is valid!"); -/// } -/// ``` -pub fn verify(pub_key: &[u8], msg: &[u8], sig: &[u8]) -> bool { - use qp_rusty_crystals_dilithium::ml_dsa_87::PublicKey; - match PublicKey::from_bytes(pub_key) { - Ok(pk) => pk.verify(msg, sig, None), - Err(e) => { - log::warn!("public key failed to deserialize {:?}", e); - false - }, - } -} - // // Trait implementations for WrappedPublicBytes // @@ -86,10 +48,10 @@ impl ByteArray for WrappedPublicBytes { self.0.as_slice().to_vec() } } -impl CryptoType for WrappedPublicBytes { - type Pair = DilithiumPair; +impl Public for WrappedPublicBytes where + Self: sp_runtime::CryptoType +{ } -impl Public for WrappedPublicBytes {} impl Default for WrappedPublicBytes { fn default() -> Self { @@ -110,14 +72,6 @@ impl alloc::fmt::Debug for WrappedPublicBytes } } -impl IdentifyAccount for DilithiumPublic { - type AccountId = AccountId32; - fn into_account(self) -> Self::AccountId { - // Use injective encoding for account ID derivation (collision-resistant for security) - AccountId32::new(hash_bytes(self.0.as_slice())) - } -} - pub struct WormholeAddress(pub H256); // AccountID32 for a wormhole address is the same as the address itself @@ -164,10 +118,10 @@ impl ByteArray for WrappedSignatureBytes { self.0.as_slice().to_vec() } } -impl CryptoType for WrappedSignatureBytes { - type Pair = DilithiumPair; +impl Signature for WrappedSignatureBytes where + Self: sp_runtime::CryptoType +{ } -impl Signature for WrappedSignatureBytes {} impl Default for WrappedSignatureBytes { fn default() -> Self { @@ -189,14 +143,6 @@ impl alloc::fmt::Debug for WrappedSignatureBytes::AccountId, ) -> bool { - let Self::Dilithium(sig_public) = self; - let account = sig_public.public().clone().into_account(); - if account != *signer { - return false; + match self { + Self::Dilithium(sig_public) => { + let account = sig_public.public().clone().into_account(); + if account != *signer { + return false; + } + crate::verify(sig_public.public().as_ref(), msg.get(), sig_public.signature().as_ref()) + }, + Self::Dilithium65(sig_public) => { + let account = sig_public.public().clone().into_account(); + if account != *signer { + return false; + } + crate::verify_ml_dsa_65( + sig_public.public().as_ref(), + msg.get(), + sig_public.signature().as_ref(), + ) + }, } - verify(sig_public.public().as_ref(), msg.get(), sig_public.signature().as_ref()) } } // // Trait implementations for DilithiumSigner // -impl From for DilithiumSigner { - fn from(x: DilithiumPublic) -> Self { - Self::Dilithium(x) - } -} impl IdentifyAccount for DilithiumSigner { type AccountId = AccountId32; fn into_account(self) -> AccountId32 { // Use injective encoding for account ID derivation (collision-resistant for security) - let Self::Dilithium(who) = self; - hash_bytes(who.as_ref()).into() - } -} - -impl From for AccountId32 { - fn from(public: DilithiumPublic) -> Self { - public.into_account() - } -} - -// -// Implementation methods for DilithiumPair -// - -impl DilithiumPair { - pub fn from_seed(seed: &[u8]) -> Result { - let keypair = crate::pair::generate(seed)?; - Ok(DilithiumPair { secret: keypair.secret.to_bytes(), public: keypair.public.to_bytes() }) - } - - pub fn from_keypair(keypair: qp_rusty_crystals_dilithium::ml_dsa_87::Keypair) -> Self { - DilithiumPair { secret: keypair.secret.to_bytes(), public: keypair.public.to_bytes() } - } - - /// Create DilithiumPair from raw public and secret key bytes. - /// Use when reconstructing a pair from stored/serialized key material (e.g. wallet restore). - /// - /// Verifies that the public key corresponds to the secret by signing a test message and - /// verifying it. Rejects mismatched or corrupted key pairs that would otherwise cause - /// non-obvious signature verification failures downstream. - pub fn from_raw(public: &[u8], secret: &[u8]) -> Result { - let keypair = crate::pair::create_keypair(public, secret)?; - // Verify public corresponds to secret (create_keypair only deserializes, does not validate) - const VALIDATION_MSG: &[u8] = b"qp_dilithium_crypto::from_raw_validation"; - let sig = keypair.sign(VALIDATION_MSG, None, None).map_err(|_| Error::InvalidSecretKey)?; - if !keypair.verify(VALIDATION_MSG, sig.as_ref(), None) { - return Err(Error::InvalidPublicKey); + match self { + Self::Dilithium(who) => hash_bytes(who.as_ref()).into(), + Self::Dilithium65(who) => hash_bytes(who.as_ref()).into(), } - Ok(DilithiumPair { secret: keypair.secret.to_bytes(), public: keypair.public.to_bytes() }) } - - pub fn secret_bytes(&self) -> &[u8] { - &self.secret - } - - pub fn public_bytes(&self) -> &[u8] { - &self.public - } -} - -impl alloc::fmt::Debug for DilithiumSignatureWithPublic { - #[cfg(feature = "std")] - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - write!( - f, - "DilithiumSignatureWithPublic {{ signature: {:?}, public: {:?} }}", - self.signature(), - self.public() - ) - } - - #[cfg(not(feature = "std"))] - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - write!(f, "DilithiumSignatureWithPublic") - } -} - -impl From for DilithiumSignatureScheme { - fn from(x: DilithiumSignatureWithPublic) -> Self { - Self::Dilithium(x) - } -} - -impl TryFrom for DilithiumSignatureWithPublic { - type Error = (); - fn try_from(m: DilithiumSignatureScheme) -> Result { - let DilithiumSignatureScheme::Dilithium(sig_with_public) = m; - Ok(sig_with_public) - } -} - -impl AsMut<[u8]> for DilithiumSignatureWithPublic { - fn as_mut(&mut self) -> &mut [u8] { - self.bytes.as_mut() - } -} -impl TryFrom<&[u8]> for DilithiumSignatureWithPublic { - type Error = (); - fn try_from(data: &[u8]) -> Result { - if data.len() != Self::TOTAL_LEN { - return Err(()); - } - let (sig_bytes, pub_bytes) = data.split_at(::LEN); - let signature = DilithiumSignature::from_slice(sig_bytes).map_err(|_| ())?; - let public = DilithiumPublic::from_slice(pub_bytes).map_err(|_| ())?; - Ok(Self::new(signature, public)) - } -} - -impl ByteArray for DilithiumSignatureWithPublic { - const LEN: usize = Self::TOTAL_LEN; - - fn to_raw_vec(&self) -> Vec { - self.to_bytes().to_vec() - } - - fn from_slice(data: &[u8]) -> Result { - if data.len() != Self::LEN { - return Err(()); - } - let bytes = <[u8; Self::LEN]>::try_from(data).map_err(|_| ())?; - Self::from_bytes(&bytes).map_err(|_| ()) - } - - fn as_slice(&self) -> &[u8] { - self.bytes.as_slice() - } -} -impl AsRef<[u8; Self::LEN]> for DilithiumSignatureWithPublic { - fn as_ref(&self) -> &[u8; Self::LEN] { - &self.bytes - } -} - -impl AsRef<[u8]> for DilithiumSignatureWithPublic { - fn as_ref(&self) -> &[u8] { - &self.bytes - } -} -impl Signature for DilithiumSignatureWithPublic {} - -impl CryptoType for DilithiumSignatureWithPublic { - type Pair = DilithiumPair; } diff --git a/primitives/dilithium-crypto/src/types.rs b/primitives/dilithium-crypto/src/types.rs index 354232ea6..6c230adad 100644 --- a/primitives/dilithium-crypto/src/types.rs +++ b/primitives/dilithium-crypto/src/types.rs @@ -1,50 +1,20 @@ +use crate::scheme_macro::define_dilithium_scheme; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; -use qp_rusty_crystals_dilithium::ml_dsa_87::{PUBLICKEYBYTES, SECRETKEYBYTES}; use scale_info::TypeInfo; use sp_core::{ crypto::{PublicBytes, SignatureBytes}, - ByteArray, RuntimeDebug, + RuntimeDebug, }; /// Resonance Crypto Types /// -/// Currently implementing the Dilithum cryprographic scheme for post quantum security +/// Implementing the Dilithium cryptographic schemes for post quantum security: +/// ML-DSA-87 and ML-DSA-65. /// -/// It is modeled after the Substrate MultiSignature and Signature types such as sr25519. +/// Modeled after the Substrate MultiSignature and Signature types such as sr25519. /// -/// For traits implemented see traits.rs - -#[derive( - Clone, - Eq, - PartialEq, - Debug, - Hash, - Encode, - Decode, - TypeInfo, - Ord, - PartialOrd, - DecodeWithMemTracking, -)] -pub struct DilithiumCryptoTag; - -/// Dilithium cryptographic key pair -/// -/// Contains both secret and public key material for Dilithium ML-DSA-87 operations -#[derive(Clone, Eq, PartialEq)] -pub struct DilithiumPair { - pub(crate) secret: [u8; SECRETKEYBYTES], - pub(crate) public: [u8; PUBLICKEYBYTES], -} - -impl alloc::fmt::Debug for DilithiumPair { - fn fmt(&self, f: &mut alloc::fmt::Formatter) -> alloc::fmt::Result { - f.debug_struct("DilithiumPair") - .field("public", &self.public) - .finish_non_exhaustive() - } -} +/// For trait implementations shared by all schemes see traits.rs; per-scheme +/// implementations are generated by `define_dilithium_scheme!`. /// Wrapper around Substrate's PublicBytes to provide Dilithium-specific implementations /// @@ -84,13 +54,11 @@ pub struct WrappedPublicBytes(pub PublicBytes )] pub struct WrappedSignatureBytes(pub SignatureBytes); -pub type DilithiumPublic = WrappedPublicBytes<{ crate::PUB_KEY_BYTES }, DilithiumCryptoTag>; -pub type DilithiumSignature = WrappedSignatureBytes<{ crate::SIGNATURE_BYTES }, DilithiumCryptoTag>; - /// Dilithium signature scheme - drop-in replacement for MultiSignature /// -/// Currently supports only Dilithium, but structured as an enum to allow -/// for future signature schemes to be added easily. +/// Supports ML-DSA-87 (`Dilithium`) and ML-DSA-65 (`Dilithium65`). New variants +/// must only be appended: existing variant indices are part of the extrinsic +/// wire format. #[derive( Eq, PartialEq, @@ -104,11 +72,12 @@ pub type DilithiumSignature = WrappedSignatureBytes<{ crate::SIGNATURE_BYTES }, )] pub enum DilithiumSignatureScheme { Dilithium(DilithiumSignatureWithPublic), + Dilithium65(Dilithium65SignatureWithPublic), } /// Dilithium signer - replacement for MultiSigner /// -/// Identifies the signer of a transaction using Dilithium public key +/// Identifies the signer of a transaction using a Dilithium public key #[derive( Eq, PartialEq, @@ -123,6 +92,7 @@ pub enum DilithiumSignatureScheme { )] pub enum DilithiumSigner { Dilithium(DilithiumPublic), + Dilithium65(Dilithium65Public), } #[derive(Debug)] @@ -143,94 +113,24 @@ pub enum Error { InvalidPublicKey, } -/// Combined signature and public key structure for Dilithium -/// -/// This structure contains both the signature and the public key in a single -/// byte array, which is required for certain Substrate operations. The layout -/// is: [signature_bytes][public_key_bytes]. -#[derive( - Clone, - Eq, - PartialEq, - Hash, - Encode, - Decode, - TypeInfo, - MaxEncodedLen, - Ord, - PartialOrd, - DecodeWithMemTracking, -)] -pub struct DilithiumSignatureWithPublic { - /// Raw bytes containing both signature and public key - pub bytes: [u8; DilithiumSignatureWithPublic::TOTAL_LEN], +define_dilithium_scheme! { + variant = Dilithium, + tag = DilithiumCryptoTag, + pair = DilithiumPair, + public = DilithiumPublic, + signature = DilithiumSignature, + sig_with_public = DilithiumSignatureWithPublic, + module = ml_dsa_87, + verify_fn = verify, } -impl DilithiumSignatureWithPublic { - const SIGNATURE_LEN: usize = ::LEN; - const PUBLIC_LEN: usize = ::LEN; - pub const TOTAL_LEN: usize = Self::SIGNATURE_LEN + Self::PUBLIC_LEN; - - /// Creates a new combined signature and public key structure - /// - /// # Arguments - /// * `signature` - The Dilithium signature - /// * `public` - The Dilithium public key - /// - /// # Returns - /// A new `DilithiumSignatureWithPublic` instance - pub fn new(signature: DilithiumSignature, public: DilithiumPublic) -> Self { - let mut bytes = [0u8; Self::LEN]; - bytes[..Self::SIGNATURE_LEN].copy_from_slice(signature.as_ref()); - bytes[Self::SIGNATURE_LEN..].copy_from_slice(public.as_ref()); - Self { bytes } - } - - /// Extracts the signature portion - /// - /// # Returns - /// The `DilithiumSignature` contained in this structure - pub fn signature(&self) -> DilithiumSignature { - DilithiumSignature::from_slice(&self.bytes[..Self::SIGNATURE_LEN]) - .expect("Invalid signature") - } - - /// Extracts the public key portion - /// - /// # Returns - /// The `DilithiumPublic` key contained in this structure - pub fn public(&self) -> DilithiumPublic { - DilithiumPublic::from_slice(&self.bytes[Self::SIGNATURE_LEN..]).expect("Invalid public key") - } - - /// Returns the raw bytes - /// - /// # Returns - /// A copy of the internal byte array - pub fn to_bytes(&self) -> [u8; Self::TOTAL_LEN] { - self.bytes - } - - /// Creates a `DilithiumSignatureWithPublic` from raw bytes - /// - /// # Arguments - /// * `bytes` - Raw bytes containing signature and public key - /// - /// # Returns - /// `Ok(DilithiumSignatureWithPublic)` on success, `Err(Error)` if the bytes are invalid - /// - /// # Errors - /// Returns `Error::InvalidLength` if the byte array is not the expected length - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() != Self::TOTAL_LEN { - return Err(Error::InvalidLength); - } - - let signature = DilithiumSignature::from_slice(&bytes[..Self::SIGNATURE_LEN]) - .map_err(|_| Error::InvalidLength)?; - let public = DilithiumPublic::from_slice(&bytes[Self::SIGNATURE_LEN..]) - .map_err(|_| Error::InvalidLength)?; - - Ok(Self::new(signature, public)) - } +define_dilithium_scheme! { + variant = Dilithium65, + tag = Dilithium65CryptoTag, + pair = Dilithium65Pair, + public = Dilithium65Public, + signature = Dilithium65Signature, + sig_with_public = Dilithium65SignatureWithPublic, + module = ml_dsa_65, + verify_fn = verify_ml_dsa_65, } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a3db788ec..b4ff9fbda 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -9,7 +9,10 @@ pub mod apis; mod benchmarks; pub mod configs; -pub use qp_dilithium_crypto::{DilithiumPublic, DilithiumSignature, DilithiumSignatureScheme}; +pub use qp_dilithium_crypto::{ + Dilithium65Pair, Dilithium65Public, Dilithium65Signature, Dilithium65SignatureWithPublic, + DilithiumPublic, DilithiumSignature, DilithiumSignatureScheme, +}; use alloc::vec::Vec; use sp_core::U512; @@ -73,7 +76,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 137, + spec_version: 138, impl_version: 1, apis: apis::RUNTIME_API_VERSIONS, transaction_version: 3, diff --git a/runtime/tests/transactions/integration.rs b/runtime/tests/transactions/integration.rs index 53e46234b..69e7717ed 100644 --- a/runtime/tests/transactions/integration.rs +++ b/runtime/tests/transactions/integration.rs @@ -1,6 +1,8 @@ use codec::{Decode, Encode}; -use qp_dilithium_crypto::{DilithiumSignatureScheme, DilithiumSignatureWithPublic, PUB_KEY_BYTES}; -use sp_core::ByteArray; +use qp_dilithium_crypto::{ + Dilithium65Pair, DilithiumSignatureScheme, DilithiumSignatureWithPublic, PUB_KEY_BYTES, +}; +use sp_core::{ByteArray, Pair}; use sp_runtime::{ generic::{Preamble, UncheckedExtrinsic}, traits::Verify, @@ -46,7 +48,7 @@ mod tests { // Generate a keypair let entropy = [0u8; 32]; // Fixed entropy of all zeros let keypair = qp_dilithium_crypto::generate(&entropy).expect("Failed to generate keypair"); - let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public.to_bytes(); + let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public().to_bytes(); println!("Gen Public Key (hex): {:?}", format_hex_truncated(&pk_bytes)); @@ -111,7 +113,9 @@ mod tests { println!("Decoded Address: {:?}", decoded_address); println!("Decoded Extra: ()"); - let DilithiumSignatureScheme::Dilithium(sig_public) = decoded_signature.clone(); + let DilithiumSignatureScheme::Dilithium(sig_public) = decoded_signature.clone() else { + panic!("Expected Dilithium (ML-DSA-87) signature variant") + }; let sig = sig_public.signature(); let sig_bytes = sig.as_slice(); println!("Decoded Signature: {:?}", format_hex_truncated(sig_bytes)); @@ -150,7 +154,7 @@ mod tests { // Generate a keypair let entropy = [0u8; 32]; // Fixed entropy of all zeros let keypair = qp_dilithium_crypto::generate(&entropy).expect("Failed to generate keypair"); - let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public.to_bytes(); + let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public().to_bytes(); // Use injective encoding for account ID derivation (collision-resistant) let account_id = hash_bytes(&pk_bytes).into(); let id = Address::Id(account_id); @@ -212,7 +216,7 @@ mod tests { // Generate a keypair let entropy = [0u8; 32]; // Fixed entropy of all zeros let keypair = qp_dilithium_crypto::generate(&entropy).expect("Failed to generate keypair"); - let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public.to_bytes(); + let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public().to_bytes(); // Create and sign a payload let payload: RuntimeCall = 77; @@ -273,7 +277,7 @@ mod tests { // Generate a keypair let entropy = [0u8; 32]; // Fixed entropy of all zeros let keypair = qp_dilithium_crypto::generate(&entropy).expect("Failed to generate keypair"); - let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public.to_bytes(); + let pk_bytes: [u8; PUB_KEY_BYTES] = keypair.public().to_bytes(); // Create and sign a payload let payload: RuntimeCall = 42; @@ -321,4 +325,105 @@ mod tests { let is_valid = signature.verify(&msg_decoded[..], &decoded_account_id); assert!(!is_valid, "Signature verification worked with wrong payload"); } + + // + // Integration test for ML-DSA-65 signatures + // Tests the valid round-trip case + // + #[test] + fn test_dilithium65_extrinsic() { + setup(); + + let pair = Dilithium65Pair::from_seed_slice(&[0u8; 32]).expect("Failed to create pair"); + let public = pair.public(); + let account_id: AccountId32 = hash_bytes(public.as_ref()).into(); + let id = Address::Id(account_id); + let signed_extra: SignedExtra = (); + + let payload: RuntimeCall = 42; + let msg = payload.encode(); + let sig_with_public = pair.sign(&msg); + + let extrinsic = UncheckedExtrinsic::new_signed( + payload, + id, + DilithiumSignatureScheme::Dilithium65(sig_with_public), + signed_extra, + ); + + let encoded = extrinsic.encode(); + + let decoded: UncheckedExtrinsic< + MultiAddress, + RuntimeCall, + DilithiumSignatureScheme, + (), + > = UncheckedExtrinsic::decode(&mut &encoded[..]).expect("Decoding failed"); + + assert_eq!(decoded.function, payload, "Decoded function does not match original payload"); + + let Preamble::Signed(address, signature, _) = decoded.preamble else { + unreachable!("Test assumes Preamble::Signed") + }; + let Address::Id(decoded_account_id) = address else { + unreachable!("Test assumes Address::Id") + }; + let msg_decoded = decoded.function.encode(); + + let is_valid = signature.verify(&msg_decoded[..], &decoded_account_id); + assert!( + is_valid, + "ML-DSA-65 signature verification failed for AccountId: {:?}", + decoded_account_id + ); + } + + #[test] + fn test_dilithium65_extrinsic_fail_signature() { + setup(); + + let pair = Dilithium65Pair::from_seed_slice(&[0u8; 32]).expect("Failed to create pair"); + let public = pair.public(); + let account_id: AccountId32 = hash_bytes(public.as_ref()).into(); + let id = Address::Id(account_id); + let signed_extra: SignedExtra = (); + + let payload: RuntimeCall = 99; + let msg = payload.encode(); + + // Sign payload with a different key + let pair2 = Dilithium65Pair::from_seed_slice(&[1u8; 32]).expect("Failed to create pair"); + let wrong_sig = pair2.sign(&msg); + // Recombine the wrong signature with the first pair's public key + let sig_with_public = qp_dilithium_crypto::Dilithium65SignatureWithPublic::new( + wrong_sig.signature(), + public.clone(), + ); + + let extrinsic = UncheckedExtrinsic::new_signed( + payload, + id, + DilithiumSignatureScheme::Dilithium65(sig_with_public), + signed_extra, + ); + + let encoded = extrinsic.encode(); + let decoded: UncheckedExtrinsic< + MultiAddress, + RuntimeCall, + DilithiumSignatureScheme, + (), + > = UncheckedExtrinsic::decode(&mut &encoded[..]).expect("Decoding failed"); + + let Preamble::Signed(address, signature, _) = decoded.preamble else { + unreachable!("Test assumes Preamble::Signed") + }; + let Address::Id(decoded_account_id) = address else { + unreachable!("Test assumes Address::Id") + }; + let msg_decoded = decoded.function.encode(); + + let is_valid = signature.verify(&msg_decoded[..], &decoded_account_id); + assert!(!is_valid, "ML-DSA-65 signature verification unexpectedly succeeded"); + } } diff --git a/runtime/tests/transactions/ml_dsa_65.rs b/runtime/tests/transactions/ml_dsa_65.rs new file mode 100644 index 000000000..9f5b85a7a --- /dev/null +++ b/runtime/tests/transactions/ml_dsa_65.rs @@ -0,0 +1,115 @@ +//! End-to-end tests: extrinsics signed with ML-DSA-65 through the full runtime +//! transaction pipeline (`Executive::apply_extrinsic` with all `TxExtension`s). + +use codec::Encode; +use frame_support::{assert_ok, traits::Currency}; +use qp_dilithium_crypto::Dilithium65Pair; +use quantus_runtime::{ + transaction_extensions::{ReversibleTransactionExtension, WormholeProofRecorderExtension}, + Balances, BalancesCall, Executive, Runtime, RuntimeCall, Signature, SignedPayload, System, + TxExtension, UncheckedExtrinsic, UNIT, VERSION, +}; +use sp_core::Pair; +use sp_runtime::{generic::Era, traits::IdentifyAccount, AccountId32, MultiAddress}; + +fn test_ext(account: &AccountId32) -> sp_io::TestExternalities { + use quantus_runtime::BuildStorage; + + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + Balances::make_free_balance_be(account, 1000 * UNIT); + System::set_block_number(1); + }); + ext +} + +/// Build a `transfer_keep_alive` extrinsic signed by `pair`, claiming `sender` +/// as the transaction origin. +fn signed_transfer( + pair: &Dilithium65Pair, + sender: AccountId32, + dest: AccountId32, + value: u128, + nonce: u32, +) -> UncheckedExtrinsic { + let genesis_hash = System::block_hash(0); + let call: RuntimeCall = + BalancesCall::transfer_keep_alive { dest: MultiAddress::Id(dest), value }.into(); + + let tx_ext: TxExtension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(Era::immortal()), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + ReversibleTransactionExtension::::new(), + WormholeProofRecorderExtension::::new(), + ); + + let raw_payload = SignedPayload::from_raw( + call.clone(), + tx_ext.clone(), + ( + (), + VERSION.spec_version, + VERSION.transaction_version, + genesis_hash, + genesis_hash, + (), + (), + (), + None, + (), + (), + ), + ); + let signature = raw_payload.using_encoded(|e| pair.sign(e)); + + UncheckedExtrinsic::new_signed( + call, + MultiAddress::Id(sender), + Signature::Dilithium65(signature), + tx_ext, + ) +} + +#[test] +fn test_ml_dsa_65_extrinsic_accepted_by_runtime() { + let pair = Dilithium65Pair::from_seed_slice(&[42u8; 32]).expect("valid seed"); + let account = pair.public().into_account(); + let mut ext = test_ext(&account); + + ext.execute_with(|| { + let dest = AccountId32::new([9u8; 32]); + let xt = signed_transfer(&pair, account.clone(), dest.clone(), 10 * UNIT, 0); + + let outcome = Executive::apply_extrinsic(xt) + .expect("ML-DSA-65 signed extrinsic should pass validation"); + assert_ok!(outcome); + assert_eq!(Balances::free_balance(&dest), 10 * UNIT); + }); +} + +#[test] +fn test_ml_dsa_65_extrinsic_wrong_signer_rejected() { + let pair = Dilithium65Pair::from_seed_slice(&[42u8; 32]).expect("valid seed"); + let account = pair.public().into_account(); + let mut ext = test_ext(&account); + + ext.execute_with(|| { + // The payload is signed by `pair`, but the extrinsic claims a different sender. + let impostor = AccountId32::new([7u8; 32]); + let dest = AccountId32::new([9u8; 32]); + let xt = signed_transfer(&pair, impostor, dest, 10 * UNIT, 0); + + assert!( + Executive::apply_extrinsic(xt).is_err(), + "extrinsic with mismatched signer must be rejected" + ); + }); +} diff --git a/runtime/tests/transactions/mod.rs b/runtime/tests/transactions/mod.rs index 63705e014..00c1d89e5 100644 --- a/runtime/tests/transactions/mod.rs +++ b/runtime/tests/transactions/mod.rs @@ -1,3 +1,4 @@ pub mod integration; +pub mod ml_dsa_65; pub mod recovery; pub mod reversible_integration; From cbc2c41ca94d716b65daa658ed0f2f136c92e983 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 31 Jul 2026 18:56:43 +0800 Subject: [PATCH 02/10] fix test --- primitives/dilithium-crypto/src/pair.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index e6157f940..4b61a2f35 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -230,14 +230,18 @@ mod tests { #[test] fn test_schemes_produce_different_accounts() { + use sp_runtime::traits::IdentifyAccount; + let seed = [0u8; 32]; let pair87 = DilithiumPair::from_seed(&seed).expect("Failed to create 87 pair"); let pair65 = Dilithium65Pair::from_seed(&seed).expect("Failed to create 65 pair"); + // The security property: the same seed must not yield the same on-chain + // account under the two schemes. assert_ne!( - pair87.public().as_ref().len(), - pair65.public().as_ref().len(), - "ML-DSA-87 and ML-DSA-65 public keys must differ in size" + pair87.public().into_account(), + pair65.public().into_account(), + "ML-DSA-87 and ML-DSA-65 keys must derive different AccountIds" ); } From 13e1d925fcf35851607a42f038f5d724261f4c59 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 31 Jul 2026 18:57:57 +0800 Subject: [PATCH 03/10] formatting --- primitives/dilithium-crypto/src/pair.rs | 8 ++++---- primitives/dilithium-crypto/src/traits.rs | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 4b61a2f35..e326e6714 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -64,7 +64,8 @@ pub fn create_keypair( PublicKey::from_bytes(public_key).map_err(|_| crate::types::Error::InvalidPublicKey)?; // from_parts also validates that the public key corresponds to the secret. - let keypair = Keypair::from_parts(secret, public).map_err(|_| crate::types::Error::InvalidPublicKey)?; + let keypair = + Keypair::from_parts(secret, public).map_err(|_| crate::types::Error::InvalidPublicKey)?; Ok(keypair) } @@ -209,9 +210,8 @@ mod tests { if let Some(byte) = signature_bytes.get_mut(0) { *byte ^= 1; } - let false_signature = - crate::Dilithium65SignatureWithPublic::from_slice(signature_bytes) - .expect("Failed to create signature"); + let false_signature = crate::Dilithium65SignatureWithPublic::from_slice(signature_bytes) + .expect("Failed to create signature"); let public = pair.public(); assert!( diff --git a/primitives/dilithium-crypto/src/traits.rs b/primitives/dilithium-crypto/src/traits.rs index d48d54ce6..6980e2ab4 100644 --- a/primitives/dilithium-crypto/src/traits.rs +++ b/primitives/dilithium-crypto/src/traits.rs @@ -1,4 +1,6 @@ -use super::types::{DilithiumSignatureScheme, DilithiumSigner, WrappedPublicBytes, WrappedSignatureBytes}; +use super::types::{ + DilithiumSignatureScheme, DilithiumSigner, WrappedPublicBytes, WrappedSignatureBytes, +}; use alloc::vec::Vec; use qp_poseidon_core::hash_bytes; @@ -161,7 +163,11 @@ impl Verify for DilithiumSignatureScheme { if account != *signer { return false; } - crate::verify(sig_public.public().as_ref(), msg.get(), sig_public.signature().as_ref()) + crate::verify( + sig_public.public().as_ref(), + msg.get(), + sig_public.signature().as_ref(), + ) }, Self::Dilithium65(sig_public) => { let account = sig_public.public().clone().into_account(); From 8c651734ab68ff5180722476b3640ac396486e28 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 31 Jul 2026 19:43:01 +0800 Subject: [PATCH 04/10] refactor: rename Dilithium variant/types to Dilithium87 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) --- client/cli/src/arg_enums.rs | 5 ++- client/cli/src/commands/inspect_key.rs | 4 +- client/cli/src/commands/utils.rs | 4 +- client/cli/src/commands/verify.rs | 2 +- client/cli/src/params/mod.rs | 2 +- node/src/benchmarking.rs | 4 +- node/src/command.rs | 10 ++--- primitives/dilithium-crypto/src/lib.rs | 8 ++-- primitives/dilithium-crypto/src/pair.rs | 50 +++++++++++------------ primitives/dilithium-crypto/src/traits.rs | 6 +-- primitives/dilithium-crypto/src/types.rs | 20 ++++----- runtime/src/lib.rs | 2 +- runtime/tests/transactions/integration.rs | 40 +++++++++--------- 13 files changed, 79 insertions(+), 78 deletions(-) diff --git a/client/cli/src/arg_enums.rs b/client/cli/src/arg_enums.rs index 3bff11d62..2723a0ce3 100644 --- a/client/cli/src/arg_enums.rs +++ b/client/cli/src/arg_enums.rs @@ -122,8 +122,9 @@ pub enum NodeKeyType { #[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] #[value(rename_all = "kebab-case")] pub enum CryptoScheme { - /// Use dilithium (ML-DSA-87). - Dilithium, + /// Use dilithium ML-DSA-87. + #[value(alias = "dilithium")] + Dilithium87, /// Use dilithium ML-DSA-65. Dilithium65, } diff --git a/client/cli/src/commands/inspect_key.rs b/client/cli/src/commands/inspect_key.rs index c2aff16f3..98dc4098b 100644 --- a/client/cli/src/commands/inspect_key.rs +++ b/client/cli/src/commands/inspect_key.rs @@ -151,7 +151,7 @@ fn expect_public_from_phrase( #[cfg(test)] mod tests { use super::*; - use qp_dilithium_crypto::DilithiumPair; + use qp_dilithium_crypto::Dilithium87Pair; use sp_core::crypto::{ByteArray, Pair, Ss58AddressFormat}; use sp_runtime::traits::IdentifyAccount; @@ -205,7 +205,7 @@ mod tests { let uri = SecretUri::from_str(seed_phrase).expect("Valid URI"); let password: Option<&str> = uri.password.as_ref().map(|s| s.expose_secret().as_ref()); - let pair: DilithiumPair = + let pair: Dilithium87Pair = Pair::from_string(uri.phrase.expose_secret().as_str(), password).expect("Valid"); let public = pair.public(); let public_hex = array_bytes::bytes2hex("0x", public.as_slice()); diff --git a/client/cli/src/commands/utils.rs b/client/cli/src/commands/utils.rs index 2703d4066..6b45e86ae 100644 --- a/client/cli/src/commands/utils.rs +++ b/client/cli/src/commands/utils.rs @@ -290,8 +290,8 @@ macro_rules! with_crypto_scheme { $method:ident<$($generics:ty),*>( $( $params:expr ),* $(,)?) $(,)? ) => { match $scheme { - $crate::CryptoScheme::Dilithium => { - $method::($($params),*) + $crate::CryptoScheme::Dilithium87 => { + $method::($($params),*) } $crate::CryptoScheme::Dilithium65 => { $method::($($params),*) diff --git a/client/cli/src/commands/verify.rs b/client/cli/src/commands/verify.rs index 5043e605c..13f764839 100644 --- a/client/cli/src/commands/verify.rs +++ b/client/cli/src/commands/verify.rs @@ -160,7 +160,7 @@ mod test { fn sign_then_verify_roundtrip() { // Derive public key from mnemonic let mnemonic_pair = - utils::pair_from_suri::(MNEMONIC, None) + utils::pair_from_suri::(MNEMONIC, None) .expect("Must derive pair from mnemonic"); let public_hex = format!("0x{}", hex::encode(mnemonic_pair.public().as_ref())); // Sign via the sign command diff --git a/client/cli/src/params/mod.rs b/client/cli/src/params/mod.rs index b41e16f6d..a742b4fce 100644 --- a/client/cli/src/params/mod.rs +++ b/client/cli/src/params/mod.rs @@ -136,7 +136,7 @@ impl BlockNumberOrHash { #[derive(Debug, Clone, Args)] pub struct CryptoSchemeFlag { /// cryptography scheme - #[arg(long, value_name = "SCHEME", value_enum, ignore_case = true, default_value_t = CryptoScheme::Dilithium)] + #[arg(long, value_name = "SCHEME", value_enum, ignore_case = true, default_value_t = CryptoScheme::Dilithium87)] pub scheme: CryptoScheme, } diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 2dd9a8ef8..cd1e76cee 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -98,7 +98,7 @@ impl frame_benchmarking_cli::ExtrinsicBuilder for TransferKeepAliveBuilder { /// Note: Should only be used for benchmarking. pub fn create_benchmark_extrinsic( client: &FullClient, - sender: qp_dilithium_crypto::DilithiumPair, + sender: qp_dilithium_crypto::Dilithium87Pair, call: runtime::RuntimeCall, nonce: u32, ) -> runtime::UncheckedExtrinsic { @@ -154,7 +154,7 @@ pub fn create_benchmark_extrinsic( runtime::UncheckedExtrinsic::new_signed( call, sender.public().into_account().into(), - runtime::Signature::Dilithium(signature), + runtime::Signature::Dilithium87(signature), tx_ext, ) } diff --git a/node/src/command.rs b/node/src/command.rs index af7367187..1cff2a15b 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -7,7 +7,7 @@ use crate::{ }; #[cfg(feature = "runtime-benchmarks")] use frame_benchmarking_cli::{BenchmarkCmd, ExtrinsicFactory, SUBSTRATE_REFERENCE_HARDWARE}; -use qp_dilithium_crypto::{traits::WormholeAddress, DilithiumPair}; +use qp_dilithium_crypto::{traits::WormholeAddress, Dilithium87Pair}; use qp_rusty_crystals_hdwallet::{ derive_key_from_mnemonic, derive_wormhole_from_mnemonic, generate_mnemonic, mnemonic_to_seed, wormhole::WormholePair, SensitiveBytes32, QUANTUS_DILITHIUM_CHAIN_ID, @@ -127,7 +127,7 @@ pub fn generate_quantus_key( eprintln!("Error deriving from mnemonic: {:?}", e); sc_cli::Error::Input("Failed to derive from mnemonic".into()) })?; - let dilithium_pair = DilithiumPair::from_keypair(keypair); + let dilithium_pair = Dilithium87Pair::from_keypair(keypair); let account_id = AccountId32::from(dilithium_pair.public()); return Ok(QuantusKeyDetails { address: account_id @@ -188,7 +188,7 @@ pub fn generate_quantus_key( })?; let mut new_words = new_words; new_words.zeroize(); - let dilithium_pair = DilithiumPair::from_keypair(keypair); + let dilithium_pair = Dilithium87Pair::from_keypair(keypair); let account_id = AccountId32::from(dilithium_pair.public()); return Ok(QuantusKeyDetails { address: account_id @@ -203,8 +203,8 @@ pub fn generate_quantus_key( } }; - let dilithium_pair = DilithiumPair::from_seed(&seed_for_pair).map_err(|e| { - eprintln!("Error creating DilithiumPair: {:?}", e); + let dilithium_pair = Dilithium87Pair::from_seed(&seed_for_pair).map_err(|e| { + eprintln!("Error creating Dilithium87Pair: {:?}", e); sc_cli::Error::Input("Failed to create keypair".into()) })?; diff --git a/primitives/dilithium-crypto/src/lib.rs b/primitives/dilithium-crypto/src/lib.rs index 05f95e1d0..e8efa3981 100644 --- a/primitives/dilithium-crypto/src/lib.rs +++ b/primitives/dilithium-crypto/src/lib.rs @@ -15,8 +15,8 @@ pub const SIGNATURE_BYTES: usize = ml_dsa_87::SIGNBYTES; pub use pair::{create_keypair, crystal_alice, crystal_charlie, dilithium_bob, generate}; pub use types::{ - verify, verify_ml_dsa_65, Dilithium65CryptoTag, Dilithium65Pair, Dilithium65Public, - Dilithium65Signature, Dilithium65SignatureWithPublic, DilithiumCryptoTag, DilithiumPair, - DilithiumPublic, DilithiumSignature, DilithiumSignatureScheme, DilithiumSignatureWithPublic, - DilithiumSigner, WrappedPublicBytes, WrappedSignatureBytes, + verify_ml_dsa_65, verify_ml_dsa_87, Dilithium65CryptoTag, Dilithium65Pair, Dilithium65Public, + Dilithium65Signature, Dilithium65SignatureWithPublic, Dilithium87CryptoTag, Dilithium87Pair, + Dilithium87Public, Dilithium87Signature, Dilithium87SignatureWithPublic, + DilithiumSignatureScheme, DilithiumSigner, WrappedPublicBytes, WrappedSignatureBytes, }; diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index e326e6714..058110455 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -1,4 +1,4 @@ -use super::types::DilithiumPair; +use super::types::Dilithium87Pair; use qp_rusty_crystals_dilithium::{ ml_dsa_87::{Keypair, PublicKey, SecretKey}, params::SEEDBYTES, @@ -6,17 +6,17 @@ use qp_rusty_crystals_dilithium::{ }; use sp_core::Pair; -pub fn crystal_alice() -> DilithiumPair { +pub fn crystal_alice() -> Dilithium87Pair { let seed = [0u8; 32]; - DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") + Dilithium87Pair::from_seed_slice(&seed).expect("Always succeeds") } -pub fn dilithium_bob() -> DilithiumPair { +pub fn dilithium_bob() -> Dilithium87Pair { let seed = [1u8; 32]; - DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") + Dilithium87Pair::from_seed_slice(&seed).expect("Always succeeds") } -pub fn crystal_charlie() -> DilithiumPair { +pub fn crystal_charlie() -> Dilithium87Pair { let seed = [2u8; 32]; - DilithiumPair::from_seed_slice(&seed).expect("Always succeeds") + Dilithium87Pair::from_seed_slice(&seed).expect("Always succeeds") } /// Generates a new Dilithium ML-DSA-87 keypair @@ -72,7 +72,7 @@ pub fn create_keypair( #[cfg(test)] mod tests { use super::*; - use crate::{Dilithium65Pair, DilithiumSignatureWithPublic}; + use crate::{Dilithium65Pair, Dilithium87SignatureWithPublic}; use sp_core::ByteArray; fn setup() { @@ -87,13 +87,13 @@ mod tests { let seed = vec![0u8; 32]; - let pair = DilithiumPair::from_seed_slice(&seed).expect("Failed to create pair"); + let pair = Dilithium87Pair::from_seed_slice(&seed).expect("Failed to create pair"); let message = b"Something"; let signature = pair.sign(message); let public = pair.public(); - let result = DilithiumPair::verify(&signature, message, &public); + let result = Dilithium87Pair::verify(&signature, message, &public); assert!(result, "Signature should verify"); } @@ -101,7 +101,7 @@ mod tests { #[test] fn test_sign_different_message_fails() { let seed = [0u8; 32]; - let pair = DilithiumPair::from_seed(&seed).expect("Failed to create pair"); + let pair = Dilithium87Pair::from_seed(&seed).expect("Failed to create pair"); let message = b"Hello, world!"; let wrong_message = b"Goodbye, world!"; @@ -109,7 +109,7 @@ mod tests { let public = pair.public(); assert!( - !DilithiumPair::verify(&signature, wrong_message, &public), + !Dilithium87Pair::verify(&signature, wrong_message, &public), "Signature should not verify with wrong message" ); } @@ -117,7 +117,7 @@ mod tests { #[test] fn test_wrong_signature_fails() { let seed = [0u8; 32]; - let pair = DilithiumPair::from_seed(&seed).expect("Failed to create pair"); + let pair = Dilithium87Pair::from_seed(&seed).expect("Failed to create pair"); let message = b"Hello, world!"; let mut signature = pair.sign(message); @@ -126,12 +126,12 @@ mod tests { if let Some(byte) = signature_bytes.get_mut(0) { *byte ^= 1; } - let false_signature = DilithiumSignatureWithPublic::from_slice(signature_bytes) + let false_signature = Dilithium87SignatureWithPublic::from_slice(signature_bytes) .expect("Failed to create signature"); let public = pair.public(); assert!( - !DilithiumPair::verify(&false_signature, message, &public), + !Dilithium87Pair::verify(&false_signature, message, &public), "Corrupted signature should not verify" ); } @@ -140,8 +140,8 @@ mod tests { fn test_different_seed_different_public() { let seed1 = vec![0u8; 32]; let seed2 = vec![1u8; 32]; - let pair1 = DilithiumPair::from_seed(&seed1).expect("Failed to create pair"); - let pair2 = DilithiumPair::from_seed(&seed2).expect("Failed to create pair"); + let pair1 = Dilithium87Pair::from_seed(&seed1).expect("Failed to create pair"); + let pair2 = Dilithium87Pair::from_seed(&seed2).expect("Failed to create pair"); let pub1 = pair1.public(); let pub2 = pair2.public(); @@ -156,11 +156,11 @@ mod tests { #[test] fn test_from_raw_matching_keys_succeeds() { let seed = [0u8; 32]; - let pair = DilithiumPair::from_seed(&seed).expect("Failed to create pair"); + let pair = Dilithium87Pair::from_seed(&seed).expect("Failed to create pair"); let public = pair.public().as_ref().to_vec(); let secret = pair.secret_bytes().to_vec(); let restored = - DilithiumPair::from_raw(&public, &secret).expect("Matching keys should succeed"); + Dilithium87Pair::from_raw(&public, &secret).expect("Matching keys should succeed"); assert_eq!(restored.public().as_ref(), pair.public().as_ref()); } @@ -168,10 +168,10 @@ mod tests { fn test_from_raw_mismatched_keys_fails() { let seed1 = [0u8; 32]; let seed2 = [1u8; 32]; - let pair1 = DilithiumPair::from_seed(&seed1).expect("Failed to create pair1"); - let pair2 = DilithiumPair::from_seed(&seed2).expect("Failed to create pair2"); + let pair1 = Dilithium87Pair::from_seed(&seed1).expect("Failed to create pair1"); + let pair2 = Dilithium87Pair::from_seed(&seed2).expect("Failed to create pair2"); // Swap: pair1's secret with pair2's public - should fail validation - let result = DilithiumPair::from_raw(pair2.public().as_ref(), pair1.secret_bytes()); + let result = Dilithium87Pair::from_raw(pair2.public().as_ref(), pair1.secret_bytes()); assert!(result.is_err(), "Mismatched public/secret should be rejected"); } @@ -233,7 +233,7 @@ mod tests { use sp_runtime::traits::IdentifyAccount; let seed = [0u8; 32]; - let pair87 = DilithiumPair::from_seed(&seed).expect("Failed to create 87 pair"); + let pair87 = Dilithium87Pair::from_seed(&seed).expect("Failed to create 87 pair"); let pair65 = Dilithium65Pair::from_seed(&seed).expect("Failed to create 65 pair"); // The security property: the same seed must not yield the same on-chain @@ -251,9 +251,9 @@ mod tests { // (this is the path the CLI key commands use). let phrase = "legal winner thank year wave sausage worth useful legal winner thank yellow"; - let (pair87, _) = DilithiumPair::from_phrase(phrase, None).expect("87 from_phrase failed"); + let (pair87, _) = Dilithium87Pair::from_phrase(phrase, None).expect("87 from_phrase failed"); let (pair87_again, _) = - DilithiumPair::from_phrase(phrase, None).expect("87 from_phrase failed"); + Dilithium87Pair::from_phrase(phrase, None).expect("87 from_phrase failed"); assert_eq!(pair87.public(), pair87_again.public(), "87 derivation must be deterministic"); let (pair65, _) = diff --git a/primitives/dilithium-crypto/src/traits.rs b/primitives/dilithium-crypto/src/traits.rs index 6980e2ab4..4ef79aa3f 100644 --- a/primitives/dilithium-crypto/src/traits.rs +++ b/primitives/dilithium-crypto/src/traits.rs @@ -158,12 +158,12 @@ impl Verify for DilithiumSignatureScheme { signer: &::AccountId, ) -> bool { match self { - Self::Dilithium(sig_public) => { + Self::Dilithium87(sig_public) => { let account = sig_public.public().clone().into_account(); if account != *signer { return false; } - crate::verify( + crate::verify_ml_dsa_87( sig_public.public().as_ref(), msg.get(), sig_public.signature().as_ref(), @@ -194,7 +194,7 @@ impl IdentifyAccount for DilithiumSigner { fn into_account(self) -> AccountId32 { // Use injective encoding for account ID derivation (collision-resistant for security) match self { - Self::Dilithium(who) => hash_bytes(who.as_ref()).into(), + Self::Dilithium87(who) => hash_bytes(who.as_ref()).into(), Self::Dilithium65(who) => hash_bytes(who.as_ref()).into(), } } diff --git a/primitives/dilithium-crypto/src/types.rs b/primitives/dilithium-crypto/src/types.rs index 6c230adad..be88217ed 100644 --- a/primitives/dilithium-crypto/src/types.rs +++ b/primitives/dilithium-crypto/src/types.rs @@ -56,7 +56,7 @@ pub struct WrappedSignatureBytes(pub SignatureBytes(pub SignatureBytes String { #[cfg(test)] mod tests { - use qp_dilithium_crypto::{DilithiumPublic, DilithiumSignature}; + use qp_dilithium_crypto::{Dilithium87Public, Dilithium87Signature}; use qp_poseidon_core::hash_bytes; use super::*; @@ -60,7 +60,7 @@ mod tests { println!("Gen Signature (hex): {:?}", format_hex_truncated(&sig_bytes)); let signature = - DilithiumSignature::from_slice(&sig_bytes).expect("Signature length mismatch"); + Dilithium87Signature::from_slice(&sig_bytes).expect("Signature length mismatch"); let bytes: &[u8] = signature.as_ref(); // or signature.as_slice() println!("Gen Signature bytes: {:?}", format_hex_truncated(bytes)); @@ -73,15 +73,15 @@ mod tests { println!("Payload AccountId: {:?}", &id); let signed_extra: SignedExtra = (); - let sig_with_public = DilithiumSignatureWithPublic::new( + let sig_with_public = Dilithium87SignatureWithPublic::new( signature, - DilithiumPublic::from_slice(&pk_bytes).unwrap(), + Dilithium87Public::from_slice(&pk_bytes).unwrap(), ); let extrinsic = UncheckedExtrinsic::new_signed( payload, id, - DilithiumSignatureScheme::Dilithium(sig_with_public), + DilithiumSignatureScheme::Dilithium87(sig_with_public), signed_extra, ); @@ -113,8 +113,8 @@ mod tests { println!("Decoded Address: {:?}", decoded_address); println!("Decoded Extra: ()"); - let DilithiumSignatureScheme::Dilithium(sig_public) = decoded_signature.clone() else { - panic!("Expected Dilithium (ML-DSA-87) signature variant") + let DilithiumSignatureScheme::Dilithium87(sig_public) = decoded_signature.clone() else { + panic!("Expected Dilithium87 (ML-DSA-87) signature variant") }; let sig = sig_public.signature(); let sig_bytes = sig.as_slice(); @@ -169,19 +169,19 @@ mod tests { let keypair2 = qp_dilithium_crypto::generate(&entropy2).expect("Failed to generate keypair"); let sig_bytes_wrong_key = keypair2.sign(&msg, None, None).expect("Failed to sign message"); - let signature_wrong_key = DilithiumSignature::try_from(&sig_bytes_wrong_key[..]) + let signature_wrong_key = Dilithium87Signature::try_from(&sig_bytes_wrong_key[..]) .expect("Signature length mismatch"); - let sig_with_public = DilithiumSignatureWithPublic::new( + let sig_with_public = Dilithium87SignatureWithPublic::new( signature_wrong_key, - DilithiumPublic::from_slice(&pk_bytes).unwrap(), + Dilithium87Public::from_slice(&pk_bytes).unwrap(), ); // Create transaction with invalid signature let extrinsic = UncheckedExtrinsic::new_signed( payload, id, - DilithiumSignatureScheme::Dilithium(sig_with_public), + DilithiumSignatureScheme::Dilithium87(sig_with_public), signed_extra, ); @@ -223,7 +223,7 @@ mod tests { let msg = payload.encode(); let sig_bytes = keypair.sign(&msg, None, None).expect("Failed to sign message"); let signature = - DilithiumSignature::try_from(&sig_bytes[..]).expect("Signature length mismatch"); + Dilithium87Signature::try_from(&sig_bytes[..]).expect("Signature length mismatch"); // Create a second account // Use injective encoding for account ID derivation (collision-resistant) @@ -231,16 +231,16 @@ mod tests { let id_2 = Address::Id(account_id_2); let signed_extra: SignedExtra = (); - let sig_with_public = DilithiumSignatureWithPublic::new( + let sig_with_public = Dilithium87SignatureWithPublic::new( signature, - DilithiumPublic::from_slice(&pk_bytes).unwrap(), + Dilithium87Public::from_slice(&pk_bytes).unwrap(), ); // Create transaction with wrong account ID. let extrinsic = UncheckedExtrinsic::new_signed( payload, id_2, - DilithiumSignatureScheme::Dilithium(sig_with_public), // correct signature! + DilithiumSignatureScheme::Dilithium87(sig_with_public), // correct signature! signed_extra, ); @@ -284,7 +284,7 @@ mod tests { let msg = payload.encode(); let sig_bytes = keypair.sign(&msg, None, None).expect("Failed to sign message"); let signature = - DilithiumSignature::from_slice(&sig_bytes).expect("Signature length mismatch"); + Dilithium87Signature::from_slice(&sig_bytes).expect("Signature length mismatch"); // Use injective encoding for account ID derivation (collision-resistant) let account_id = hash_bytes(&pk_bytes).into(); @@ -294,15 +294,15 @@ mod tests { // Create transaction with wrong payload. Should fail. let wrong_payload: RuntimeCall = 40; - let sig_with_public = DilithiumSignatureWithPublic::new( + let sig_with_public = Dilithium87SignatureWithPublic::new( signature, - DilithiumPublic::from_slice(&pk_bytes).unwrap(), + Dilithium87Public::from_slice(&pk_bytes).unwrap(), ); let extrinsic = UncheckedExtrinsic::new_signed( wrong_payload, id, - DilithiumSignatureScheme::Dilithium(sig_with_public), + DilithiumSignatureScheme::Dilithium87(sig_with_public), signed_extra, ); From 31dc125445b7e1b55cc9b0c8e258e0c53d848ed2 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 31 Jul 2026 19:47:52 +0800 Subject: [PATCH 05/10] style: formatting after Dilithium87 rename --- primitives/dilithium-crypto/src/pair.rs | 3 ++- runtime/tests/transactions/integration.rs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 058110455..12dfc99d7 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -251,7 +251,8 @@ mod tests { // (this is the path the CLI key commands use). let phrase = "legal winner thank year wave sausage worth useful legal winner thank yellow"; - let (pair87, _) = Dilithium87Pair::from_phrase(phrase, None).expect("87 from_phrase failed"); + let (pair87, _) = + Dilithium87Pair::from_phrase(phrase, None).expect("87 from_phrase failed"); let (pair87_again, _) = Dilithium87Pair::from_phrase(phrase, None).expect("87 from_phrase failed"); assert_eq!(pair87.public(), pair87_again.public(), "87 derivation must be deterministic"); diff --git a/runtime/tests/transactions/integration.rs b/runtime/tests/transactions/integration.rs index e012cd327..81b723402 100644 --- a/runtime/tests/transactions/integration.rs +++ b/runtime/tests/transactions/integration.rs @@ -1,6 +1,6 @@ use codec::{Decode, Encode}; use qp_dilithium_crypto::{ - Dilithium65Pair, DilithiumSignatureScheme, Dilithium87SignatureWithPublic, PUB_KEY_BYTES, + Dilithium65Pair, Dilithium87SignatureWithPublic, DilithiumSignatureScheme, PUB_KEY_BYTES, }; use sp_core::{ByteArray, Pair}; use sp_runtime::{ @@ -113,7 +113,8 @@ mod tests { println!("Decoded Address: {:?}", decoded_address); println!("Decoded Extra: ()"); - let DilithiumSignatureScheme::Dilithium87(sig_public) = decoded_signature.clone() else { + let DilithiumSignatureScheme::Dilithium87(sig_public) = decoded_signature.clone() + else { panic!("Expected Dilithium87 (ML-DSA-87) signature variant") }; let sig = sig_public.signature(); From 8806d928a78356d32cb2a671512ce3d631a7906e Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 31 Jul 2026 20:29:01 +0800 Subject: [PATCH 06/10] update to 4.0.0 dilithium packages --- Cargo.lock | 12 +++++++----- Cargo.toml | 6 ++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fe272896..f10cdf153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,6 +896,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "unicode-normalization", + "zeroize", ] [[package]] @@ -7580,22 +7581,23 @@ checksum = "55e32db7b3a5d70086f82f198ecfc021f17cb7ec70a416512dba3488f4a116c1" [[package]] name = "qp-rusty-crystals-dilithium" -version = "3.0.1" -source = "git+https://github.com/Quantus-Network/qp-rusty-crystals?rev=6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1#6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0799fbe9bf179087eae23c9ba89c1f6c62228a4afdb999c5c0975a0f17c3c5bf" dependencies = [ "zeroize", ] [[package]] name = "qp-rusty-crystals-hdwallet" -version = "3.0.1" -source = "git+https://github.com/Quantus-Network/qp-rusty-crystals?rev=6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1#6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865ff92884da31f4a6a38751f393f24107b72a8610e61e26cda8add47479520" dependencies = [ "bip39", "getrandom 0.2.17", "hex", "hex-literal 0.4.1", - "hmac 0.12.1", "qp-poseidon-core", "qp-rusty-crystals-dilithium", "serde", diff --git a/Cargo.toml b/Cargo.toml index 716a3e9df..0a4491520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -233,10 +233,8 @@ sp-consensus-qpow = { path = "./primitives/consensus/qpow", default-features = f qp-plonky2 = { version = "1.5.4", default-features = false } qp-plonky2-verifier = { version = "1.5.4", default-features = false } qp-poseidon-core = { version = "3.0.2", default-features = false } -# Git pin: crates.io 3.0.1 ships only ML-DSA-87; the ml-dsa-44/65/87 feature split -# lives on qp-rusty-crystals main and is not published yet. -qp-rusty-crystals-dilithium = { git = "https://github.com/Quantus-Network/qp-rusty-crystals", rev = "6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1", default-features = false, features = ["ml-dsa-65", "ml-dsa-87"] } -qp-rusty-crystals-hdwallet = { git = "https://github.com/Quantus-Network/qp-rusty-crystals", rev = "6e2d91eb5e927df3e3fccbbc9c1557e8fdc570e1", features = ["ml-dsa-65"] } +qp-rusty-crystals-dilithium = { version = "4.0.0", default-features = false, features = ["ml-dsa-65", "ml-dsa-87"] } +qp-rusty-crystals-hdwallet = { version = "4.0.0", features = ["ml-dsa-65"] } qp-wormhole-aggregator = { version = "3.1.0", default-features = false } qp-wormhole-circuit = { version = "3.1.0", default-features = false } qp-wormhole-circuit-builder = { version = "3.1.0", default-features = false } From 4b40d37fc2ab4aae7c291a2ddfbe9cfb35dff3ed Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 31 Jul 2026 20:37:04 +0800 Subject: [PATCH 07/10] chore: switch qp-rusty-crystals deps from git pin to published 4.0.0 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 --- client/litep2p/src/crypto/dilithium.rs | 4 +-- node/src/command.rs | 34 +++++++++---------- primitives/dilithium-crypto/src/pair.rs | 4 +-- .../dilithium-crypto/src/scheme_macro.rs | 10 ++++-- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/client/litep2p/src/crypto/dilithium.rs b/client/litep2p/src/crypto/dilithium.rs index fa43e7b21..25a272cce 100644 --- a/client/litep2p/src/crypto/dilithium.rs +++ b/client/litep2p/src/crypto/dilithium.rs @@ -69,8 +69,8 @@ impl Keypair { /// Derive the internal keypair from the seed. fn derive_internal(&self) -> ml_dsa_87::Keypair { let mut seed_copy = self.seed; - let sensitive_seed = SensitiveBytes32::from(&mut seed_copy); - ml_dsa_87::Keypair::generate(sensitive_seed) + let mut sensitive_seed = SensitiveBytes32::from(&mut seed_copy); + ml_dsa_87::Keypair::generate(&mut sensitive_seed) } /// Convert the keypair into a byte array. diff --git a/node/src/command.rs b/node/src/command.rs index 1cff2a15b..5719b7a80 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -10,7 +10,7 @@ use frame_benchmarking_cli::{BenchmarkCmd, ExtrinsicFactory, SUBSTRATE_REFERENCE use qp_dilithium_crypto::{traits::WormholeAddress, Dilithium87Pair}; use qp_rusty_crystals_hdwallet::{ derive_key_from_mnemonic, derive_wormhole_from_mnemonic, generate_mnemonic, mnemonic_to_seed, - wormhole::WormholePair, SensitiveBytes32, QUANTUS_DILITHIUM_CHAIN_ID, + wormhole::WormholePair, SensitiveBytes32, SensitiveBytes64, QUANTUS_DILITHIUM_CHAIN_ID, QUANTUS_WORMHOLE_CHAIN_ID, }; use quantus_runtime::Block; @@ -112,14 +112,13 @@ pub fn generate_quantus_key( // Use provided mnemonic. The caller already knows it, so it is // deliberately NOT placed in `secret_phrase` for echoing back. if no_derivation { - // Get raw seed from mnemonic (mnemonic_to_seed zeroizes its owned copy). - let mut seed64 = - mnemonic_to_seed(words_phrase.to_string(), None).map_err(|e| { - eprintln!("Error processing provided words: {:?}", e); - sc_cli::Error::Input("Failed to process provided words".into()) - })?; - seed_for_pair = Zeroizing::new(seed64.to_vec()); - seed64.zeroize(); + // Get raw seed from mnemonic (writes into `seed64`, which zeroizes on drop). + let mut seed64 = SensitiveBytes64::zeroed(); + mnemonic_to_seed(words_phrase.to_string(), None, &mut seed64).map_err(|e| { + eprintln!("Error processing provided words: {:?}", e); + sc_cli::Error::Input("Failed to process provided words".into()) + })?; + seed_for_pair = Zeroizing::new(seed64.as_bytes().to_vec()); } else { println!("Deriving HD path: {}", path); let keypair = @@ -172,13 +171,13 @@ pub fn generate_quantus_key( words_to_print = Some(new_words.clone()); if no_derivation { - // Get raw seed from mnemonic (consumes and zeroizes `new_words`). - let mut seed64 = mnemonic_to_seed(new_words, None).map_err(|e| { + // Get raw seed from mnemonic (writes into `seed64`, which zeroizes on drop). + let mut seed64 = SensitiveBytes64::zeroed(); + mnemonic_to_seed(new_words, None, &mut seed64).map_err(|e| { eprintln!("Error converting mnemonic to seed: {:?}", e); sc_cli::Error::Input("Failed to convert mnemonic to seed".into()) })?; - seed_for_pair = Zeroizing::new(seed64.to_vec()); - seed64.zeroize(); + seed_for_pair = Zeroizing::new(seed64.as_bytes().to_vec()); } else { println!("Deriving HD path: {}", path); let keypair = @@ -244,14 +243,15 @@ pub fn generate_quantus_key( }; let wormhole_pair = if no_derivation { - let mut seed64 = mnemonic_to_seed(words_phrase.to_string(), None).map_err(|e| { + let mut seed64 = SensitiveBytes64::zeroed(); + mnemonic_to_seed(words_phrase.to_string(), None, &mut seed64).map_err(|e| { eprintln!("Error processing provided words: {:?}", e); sc_cli::Error::Input("Failed to process provided words".into()) })?; let mut seed32 = [0u8; 32]; - seed32.copy_from_slice(&seed64[..32]); - seed64.zeroize(); - WormholePair::generate_new(SensitiveBytes32::from(&mut seed32)) + seed32.copy_from_slice(&seed64.as_bytes()[..32]); + let mut sensitive_seed = SensitiveBytes32::from(&mut seed32); + WormholePair::generate_new(&mut sensitive_seed) } else { println!("Deriving wormhole HD path: {}", path); derive_wormhole_from_mnemonic(&words_phrase, None, &path).map_err(|e| { diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 12dfc99d7..f46a07da0 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -39,8 +39,8 @@ pub fn generate(entropy: &[u8]) -> Result { } let mut entropy_array = [0u8; 32]; entropy_array.copy_from_slice(&entropy[..32]); - let sensitive_entropy = SensitiveBytes32::from(&mut entropy_array); - Ok(Keypair::generate(sensitive_entropy)) + let mut sensitive_entropy = SensitiveBytes32::from(&mut entropy_array); + Ok(Keypair::generate(&mut sensitive_entropy)) } /// Creates a keypair from existing public and secret key bytes diff --git a/primitives/dilithium-crypto/src/scheme_macro.rs b/primitives/dilithium-crypto/src/scheme_macro.rs index 2f627980d..b6236d93c 100644 --- a/primitives/dilithium-crypto/src/scheme_macro.rs +++ b/primitives/dilithium-crypto/src/scheme_macro.rs @@ -275,8 +275,10 @@ macro_rules! define_dilithium_scheme { } let mut entropy_array = [0u8; 32]; entropy_array.copy_from_slice(&seed[..32]); + let mut sensitive_entropy = + qp_rusty_crystals_dilithium::SensitiveBytes32::from(&mut entropy_array); let keypair = qp_rusty_crystals_dilithium::$module::Keypair::generate( - qp_rusty_crystals_dilithium::SensitiveBytes32::from(&mut entropy_array), + &mut sensitive_entropy, ); Ok(Self { secret: *keypair.secret().to_bytes(), @@ -412,13 +414,15 @@ macro_rules! define_dilithium_scheme { ) .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; let pair = Self::from_keypair(keypair); - let seed_bytes = qp_rusty_crystals_hdwallet::mnemonic_to_seed( + let mut seed_bytes = qp_rusty_crystals_hdwallet::SensitiveBytes64::zeroed(); + qp_rusty_crystals_hdwallet::mnemonic_to_seed( phrase.to_string(), password, + &mut seed_bytes, ) .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; let mut seed = [0u8; 32]; - seed.copy_from_slice(&seed_bytes[..32]); + seed.copy_from_slice(&seed_bytes.as_bytes()[..32]); Ok((pair, seed)) } From f55a68fd0e46e147c90bcea69a52567d063dc780 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 3 Aug 2026 23:18:17 +0800 Subject: [PATCH 08/10] fix: restore main's pair changes dropped in merge 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 --- primitives/dilithium-crypto/src/pair.rs | 20 ++++----- .../dilithium-crypto/src/scheme_macro.rs | 44 +++++++++---------- primitives/dilithium-crypto/src/types.rs | 1 - 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 9af66b834..8db1182ea 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -161,8 +161,8 @@ mod tests { /// their account from it with `from_seed` if the mnemonic is lost. #[test] fn test_from_phrase_returned_seed_reconstructs_same_pair() { - let (pair, seed) = DilithiumPair::from_phrase(TEST_PHRASE, None).expect("valid phrase"); - let restored = DilithiumPair::from_seed(&seed).expect("valid seed"); + let (pair, seed) = Dilithium87Pair::from_phrase(TEST_PHRASE, None).expect("valid phrase"); + let restored = Dilithium87Pair::from_seed(&seed).expect("valid seed"); assert_eq!( pair.public_bytes(), restored.public_bytes(), @@ -175,8 +175,8 @@ mod tests { #[test] fn test_from_phrase_returned_seed_reconstructs_same_pair_with_password() { let password = Some("hunter2"); - let (pair, seed) = DilithiumPair::from_phrase(TEST_PHRASE, password).expect("valid phrase"); - let restored = DilithiumPair::from_seed(&seed).expect("valid seed"); + let (pair, seed) = Dilithium87Pair::from_phrase(TEST_PHRASE, password).expect("valid phrase"); + let restored = Dilithium87Pair::from_seed(&seed).expect("valid seed"); assert_eq!( pair.public_bytes(), restored.public_bytes(), @@ -188,9 +188,9 @@ mod tests { #[test] fn test_from_string_with_seed_returns_matching_seed() { let (pair, seed) = - DilithiumPair::from_string_with_seed(TEST_PHRASE, None).expect("valid phrase"); + Dilithium87Pair::from_string_with_seed(TEST_PHRASE, None).expect("valid phrase"); let seed = seed.expect("a faithful seed is available for mnemonic inputs"); - let restored = DilithiumPair::from_seed(&seed).expect("valid seed"); + let restored = Dilithium87Pair::from_seed(&seed).expect("valid seed"); assert_eq!(pair.public_bytes(), restored.public_bytes()); } @@ -204,8 +204,8 @@ mod tests { "m/44'/189189'/0'/0'/0'", ) .expect("valid phrase"); - let expected = DilithiumPair::from_keypair(keypair); - let (pair, _) = DilithiumPair::from_phrase(TEST_PHRASE, None).expect("valid phrase"); + let expected = Dilithium87Pair::from_keypair(keypair); + let (pair, _) = Dilithium87Pair::from_phrase(TEST_PHRASE, None).expect("valid phrase"); assert_eq!( pair.public_bytes(), expected.public_bytes(), @@ -217,7 +217,7 @@ mod tests { #[test] fn test_zeroize_clears_secret() { use zeroize::Zeroize; - let mut pair = DilithiumPair::from_seed(&[7u8; 32]).expect("valid seed"); + let mut pair = Dilithium87Pair::from_seed(&[7u8; 32]).expect("valid seed"); assert!(pair.secret_bytes().iter().any(|b| *b != 0)); pair.zeroize(); assert!(pair.secret_bytes().iter().all(|b| *b == 0)); @@ -227,7 +227,7 @@ mod tests { #[test] fn test_pair_zeroizes_on_drop() { fn assert_zeroize_on_drop() {} - assert_zeroize_on_drop::(); + assert_zeroize_on_drop::(); } #[test] diff --git a/primitives/dilithium-crypto/src/scheme_macro.rs b/primitives/dilithium-crypto/src/scheme_macro.rs index b6236d93c..e2a8bf89a 100644 --- a/primitives/dilithium-crypto/src/scheme_macro.rs +++ b/primitives/dilithium-crypto/src/scheme_macro.rs @@ -34,7 +34,10 @@ macro_rules! define_dilithium_scheme { /// Dilithium cryptographic key pair /// /// Contains both secret and public key material for this parameter set. - #[derive(Clone, Eq, PartialEq)] + /// + /// The secret key material is zeroized when an instance (including any clone) is + /// dropped, so released copies do not leave private-key bytes behind in memory. + #[derive(Clone, Eq, PartialEq, zeroize::Zeroize, zeroize::ZeroizeOnDrop)] pub struct $pair { pub(crate) secret: [u8; qp_rusty_crystals_dilithium::$module::SECRETKEYBYTES], pub(crate) public: [u8; qp_rusty_crystals_dilithium::$module::PUBLICKEYBYTES], @@ -407,13 +410,6 @@ macro_rules! define_dilithium_scheme { ) -> Result<(Self, Self::Seed), sp_core::crypto::SecretStringError> { // Default derivation path for Quantus: m/44'/189189'/0'/0'/0' const DEFAULT_PATH: &str = "m/44'/189189'/0'/0'/0'"; - let keypair = qp_rusty_crystals_hdwallet::$module::derive_key_from_mnemonic( - phrase, - password, - DEFAULT_PATH, - ) - .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; - let pair = Self::from_keypair(keypair); let mut seed_bytes = qp_rusty_crystals_hdwallet::SensitiveBytes64::zeroed(); qp_rusty_crystals_hdwallet::mnemonic_to_seed( phrase.to_string(), @@ -421,8 +417,23 @@ macro_rules! define_dilithium_scheme { &mut seed_bytes, ) .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; + // The returned seed must be the actual entropy of the returned pair, so + // that `from_seed(seed)` reconstructs the very same account (users back + // up the displayed seed as recovery material). That entropy is the + // 32-byte secret derived at the default HD path: + // `derive_key_from_mnemonic` internally runs `Keypair::generate` on + // exactly these bytes, which is also what `from_seed` does. + let mut xpriv = qp_rusty_crystals_hdwallet::hderive::ExtendedPrivKey::zeroed(); + qp_rusty_crystals_hdwallet::hderive::ExtendedPrivKey::derive( + seed_bytes.as_bytes(), + DEFAULT_PATH, + &mut xpriv, + ) + .map_err(|_| sp_core::crypto::SecretStringError::InvalidPath)?; let mut seed = [0u8; 32]; - seed.copy_from_slice(&seed_bytes.as_bytes()[..32]); + seed.copy_from_slice(xpriv.secret().as_bytes()); + let pair = Self::from_seed(&seed) + .map_err(|_| sp_core::crypto::SecretStringError::InvalidSeed)?; Ok((pair, seed)) } @@ -431,19 +442,8 @@ macro_rules! define_dilithium_scheme { s: &str, password: Option<&str>, ) -> Result<(Self, Option), sp_core::crypto::SecretStringError> { - // Default derivation path for Quantus: m/44'/189189'/0'/0'/0' - const DEFAULT_PATH: &str = "m/44'/189189'/0'/0'/0'"; - let keypair = qp_rusty_crystals_hdwallet::$module::derive_key_from_mnemonic( - s, - password, - DEFAULT_PATH, - ) - .map_err(|_| sp_core::crypto::SecretStringError::InvalidPhrase)?; - let pair = Self::from_keypair(keypair); - - // Return the pair with no seed since Dilithium doesn't use traditional - // seed-based generation - Ok((pair, None)) + let (pair, seed) = ::from_phrase(s, password)?; + Ok((pair, Some(seed))) } } diff --git a/primitives/dilithium-crypto/src/types.rs b/primitives/dilithium-crypto/src/types.rs index 150904375..be88217ed 100644 --- a/primitives/dilithium-crypto/src/types.rs +++ b/primitives/dilithium-crypto/src/types.rs @@ -5,7 +5,6 @@ use sp_core::{ crypto::{PublicBytes, SignatureBytes}, RuntimeDebug, }; -use zeroize::{Zeroize, ZeroizeOnDrop}; /// Resonance Crypto Types /// From b266adf859d59ffd8ac70743e41fb16daa5b1023 Mon Sep 17 00:00:00 2001 From: illuzen Date: Mon, 3 Aug 2026 23:30:26 +0800 Subject: [PATCH 09/10] fmt --- primitives/dilithium-crypto/src/pair.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/primitives/dilithium-crypto/src/pair.rs b/primitives/dilithium-crypto/src/pair.rs index 8db1182ea..640129645 100644 --- a/primitives/dilithium-crypto/src/pair.rs +++ b/primitives/dilithium-crypto/src/pair.rs @@ -175,7 +175,8 @@ mod tests { #[test] fn test_from_phrase_returned_seed_reconstructs_same_pair_with_password() { let password = Some("hunter2"); - let (pair, seed) = Dilithium87Pair::from_phrase(TEST_PHRASE, password).expect("valid phrase"); + let (pair, seed) = + Dilithium87Pair::from_phrase(TEST_PHRASE, password).expect("valid phrase"); let restored = Dilithium87Pair::from_seed(&seed).expect("valid seed"); assert_eq!( pair.public_bytes(), From 8f336e604a1f0b888717f5f8548911f37c7e1cd0 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 4 Aug 2026 10:44:14 +0800 Subject: [PATCH 10/10] fix: bump runtime spec version to 139 --- docs/RUNTIME_SURFACE.md | 2 +- runtime/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/RUNTIME_SURFACE.md b/docs/RUNTIME_SURFACE.md index 5638e23f8..85500d46d 100644 --- a/docs/RUNTIME_SURFACE.md +++ b/docs/RUNTIME_SURFACE.md @@ -7,7 +7,7 @@ the runtime, their dispatchable calls, the runtime APIs, transaction extensions, genesis logic, and the workspace primitive crates pulled in. - **Crate:** `quantus-runtime` (`runtime/`), version `0.7.1-q-day-2` -- **Spec:** `spec_name = quantus-runtime`, `spec_version = 138`, `transaction_version = 3`, `authoring_version = 1` +- **Spec:** `spec_name = quantus-runtime`, `spec_version = 139`, `transaction_version = 3`, `authoring_version = 1` - **Build:** `no_std` WASM via `substrate-wasm-builder` (`runtime/build.rs`); native `std` build for the node/client - **Block time target:** 12s (`TARGET_BLOCK_TIME_MS = 12_000`) - **Consensus:** QPoW (quantum-resistant Proof of Work, Poseidon2-based) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index fca467f5e..27f98949f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -76,7 +76,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 138, + spec_version: 139, impl_version: 1, apis: apis::RUNTIME_API_VERSIONS, transaction_version: 3,