1use codec::Encode;
6use hkdf::Hkdf;
7use rand_core::{OsRng, RngCore};
8use sha2::Sha512;
9use subtensor_hashed::mldsa;
10use zeroize::Zeroizing;
11
12use super::{crypto_err, falcon, is_pq_crypto, Keypair, KeypairInner};
13#[cfg(test)]
14use super::{CRYPTO_MLDSA65, CRYPTO_MLDSA65_FIXED};
15use crate::error::CoreError;
16
17const DERIVATION_SALT: &[u8] = b"bittensor/ms/v2/derive";
18const STATELESS_KEY_SEARCH_LIMIT: u64 = 256;
20pub const ROLE_TRANSACTION: u8 = 0;
21
22pub struct MldsaKeypair {
23 pub(super) descriptor: mldsa::Descriptor,
24 master_seed: Option<Zeroizing<[u8; 32]>>,
25 transaction_index: u64,
27 pub(super) generation: u64,
28}
29
30impl MldsaKeypair {
31 pub(super) fn from_seed(seed: &[u8], crypto_type: u8) -> Result<Self, CoreError> {
32 let profile = super::pq_profile(crypto_type)
33 .ok_or_else(|| crypto_err("unsupported post-quantum account profile"))?;
34 let master = Zeroizing::new(
35 <[u8; 32]>::try_from(seed)
36 .map_err(|_| crypto_err("post-quantum master seed must be exactly 32 bytes"))?,
37 );
38 let (transaction_index, initial_commitment) = if super::is_fixed_pq_crypto(crypto_type) {
39 Self::stateless_transaction_key_for_profile(
40 &master,
41 profile,
42 STATELESS_KEY_SEARCH_LIMIT,
43 )?
44 } else {
45 let initial = Self::derive_seed_for_profile(&master, profile, ROLE_TRANSACTION, 0)?;
46 let public = falcon::public_key(profile, &initial)?;
47 (0, falcon::commitment(profile, &public)?)
48 };
49 Ok(Self {
50 descriptor: mldsa::Descriptor {
51 version: mldsa::VERSION,
52 algorithm: profile,
53 initial_commitment,
54 },
55 master_seed: Some(master),
56 transaction_index,
57 generation: 0,
58 })
59 }
60
61 #[cfg(test)]
62 fn stateless_transaction_key(
63 master: &[u8; 32],
64 limit: u64,
65 ) -> Result<(u64, mldsa::Commitment), CoreError> {
66 Self::stateless_transaction_key_for_profile(master, mldsa::FIXED_ALGORITHM, limit)
67 }
68
69 fn stateless_transaction_key_for_profile(
70 master: &[u8; 32],
71 profile: u8,
72 limit: u64,
73 ) -> Result<(u64, mldsa::Commitment), CoreError> {
74 for index in 0..limit {
75 let seed = Self::derive_seed_for_profile(master, profile, ROLE_TRANSACTION, index)?;
76 let public = falcon::public_key(profile, &seed)?;
77 let initial_commitment = falcon::commitment(profile, &public)?;
78 let descriptor = mldsa::Descriptor {
79 version: mldsa::VERSION,
80 algorithm: profile,
81 initial_commitment,
82 };
83 if initial_commitment != [0; mldsa::COMMITMENT_LEN]
84 && mldsa::rejects_legacy_public_keys(&mldsa::account_id(&descriptor))
85 {
86 return Ok((index, initial_commitment));
87 }
88 }
89 Err(crypto_err(
90 "post-quantum account derivation exhausted its bounded key search",
91 ))
92 }
93
94 #[cfg(test)]
95 fn derive_seed(
96 master: &[u8; 32],
97 role: u8,
98 index: u64,
99 ) -> Result<Zeroizing<[u8; 32]>, CoreError> {
100 Self::derive_seed_for_profile(master, mldsa::ALGORITHM, role, index)
101 }
102
103 fn derive_seed_for_profile(
104 master: &[u8; 32],
105 profile: u8,
106 role: u8,
107 index: u64,
108 ) -> Result<Zeroizing<[u8; 32]>, CoreError> {
109 if role != ROLE_TRANSACTION {
110 return Err(crypto_err("unsupported post-quantum key role"));
111 }
112 let salt = if subtensor_hashed::falcon::is_account_profile(profile) {
113 b"bittensor/falcon/v1/derive".as_slice()
114 } else {
115 DERIVATION_SALT
116 };
117 let kdf = Hkdf::<Sha512>::new(Some(salt), master);
118 let mut seed = Zeroizing::new([0; 32]);
119 kdf.expand(
120 &(mldsa::VERSION, profile, role, index).encode(),
121 seed.as_mut(),
122 )
123 .map_err(|_| crypto_err("post-quantum signing-key derivation failed"))?;
124 Ok(seed)
125 }
126
127 pub(super) fn seed_at(&self, role: u8, index: u64) -> Result<Zeroizing<[u8; 32]>, CoreError> {
128 Self::derive_seed_for_profile(
129 self.master_seed()
130 .ok_or_else(|| crypto_err("post-quantum derivation requires a private wallet"))?,
131 self.descriptor.algorithm,
132 role,
133 if role == ROLE_TRANSACTION && !self.descriptor.is_rotating() {
134 self.transaction_index
135 } else {
136 index
137 },
138 )
139 }
140
141 pub(super) fn master_seed(&self) -> Option<&[u8; 32]> {
142 self.master_seed.as_deref()
143 }
144 pub(super) fn account_id(&self) -> [u8; 32] {
145 mldsa::account_id(&self.descriptor)
146 }
147}
148
149pub(super) fn randomness() -> Result<Zeroizing<[u8; 32]>, CoreError> {
150 let mut random = Zeroizing::new([0; 32]);
151 OsRng
152 .try_fill_bytes(random.as_mut())
153 .map_err(|_| crypto_err("post-quantum signing randomness unavailable"))?;
154 Ok(random)
155}
156
157pub fn verify_mldsa_authorization(
160 encoded: &[u8],
161 payload: &[u8],
162 descriptor: &[u8],
163 generation: u64,
164 expected_commitment: &[u8; 64],
165) -> Result<bool, CoreError> {
166 let descriptor = mldsa::Descriptor::from_bytes(descriptor)
167 .map_err(|_| crypto_err("invalid post-quantum descriptor"))?;
168 if subtensor_hashed::falcon::is_account_profile(descriptor.algorithm) {
169 let Ok(proof) = subtensor_hashed::falcon::SignerAddress::from_bytes(encoded) else {
170 return Ok(false);
171 };
172 return Ok(generation != u64::MAX
173 && (!descriptor.is_stateless() || generation == 0)
174 && proof.account == mldsa::account_id(&descriptor)
175 && proof.algorithm == descriptor.algorithm
176 && proof.generation == generation
177 && falcon::commitment(proof.algorithm, &proof.public_key)? == *expected_commitment
178 && descriptor.accepts_next_commitment(expected_commitment, &proof.next_commitment)
179 && proof.verify(payload));
180 }
181 let Ok(proof) = mldsa::SignerAddress::from_bytes(encoded) else {
182 return Ok(false);
183 };
184 Ok(generation != u64::MAX
185 && (!descriptor.is_stateless() || generation == 0)
186 && proof.account == mldsa::account_id(&descriptor)
187 && proof.algorithm == descriptor.algorithm
188 && proof.generation == generation
189 && mldsa::commitment(&proof.public_key) == *expected_commitment
190 && descriptor.accepts_next_commitment(expected_commitment, &proof.next_commitment)
191 && proof.verify(payload))
192}
193
194impl Keypair {
195 pub(super) fn mldsa(&self) -> Result<&MldsaKeypair, CoreError> {
196 match &self.inner {
197 KeypairInner::Mldsa(key) => Ok(key),
198 _ => Err(crypto_err(
199 "operation requires a post-quantum account descriptor",
200 )),
201 }
202 }
203
204 pub fn from_mldsa_descriptor(encoded: &[u8], ss58_format: u16) -> Result<Self, CoreError> {
205 let descriptor = mldsa::Descriptor::from_bytes(encoded)
206 .map_err(|_| crypto_err("invalid post-quantum account descriptor"))?;
207 if !descriptor.is_supported() {
208 return Err(crypto_err("unsupported post-quantum account descriptor"));
209 }
210 Ok(Self {
211 inner: KeypairInner::Mldsa(MldsaKeypair {
212 descriptor,
213 master_seed: None,
214 transaction_index: 0,
215 generation: 0,
216 }),
217 ss58_format,
218 mnemonic: None,
219 seed: None,
220 })
221 }
222
223 pub(super) fn mldsa_public_only(&self) -> Result<Self, CoreError> {
224 let key = self.mldsa()?;
225 Ok(Self {
226 inner: KeypairInner::Mldsa(MldsaKeypair {
227 descriptor: key.descriptor,
228 master_seed: None,
229 transaction_index: key.transaction_index,
230 generation: key.generation,
231 }),
232 ss58_format: self.ss58_format,
233 mnemonic: None,
234 seed: None,
235 })
236 }
237
238 pub(super) fn mldsa_at_generation(&self, generation: u64) -> Result<Self, CoreError> {
239 let key = self.mldsa()?;
240 if key.descriptor.is_stateless() && generation != 0 {
241 return Err(crypto_err(
242 "registration-free post-quantum accounts have no signing generation",
243 ));
244 }
245 if key.master_seed.is_none() {
246 return Err(crypto_err(
247 "post-quantum generation selection requires a private wallet",
248 ));
249 }
250 Ok(Self {
251 inner: KeypairInner::Mldsa(MldsaKeypair {
252 descriptor: key.descriptor,
253 master_seed: key.master_seed.clone(),
254 transaction_index: key.transaction_index,
255 generation,
256 }),
257 ss58_format: self.ss58_format,
258 mnemonic: self.mnemonic.clone(),
259 seed: self.seed.clone(),
260 })
261 }
262
263 pub fn mldsa_descriptor(&self) -> Result<Vec<u8>, CoreError> {
264 Ok(self.mldsa()?.descriptor.encode())
265 }
266 pub fn mldsa_profile(&self) -> Result<u8, CoreError> {
267 Ok(self.mldsa()?.descriptor.algorithm)
268 }
269 pub fn mldsa_generation(&self) -> Result<u64, CoreError> {
270 Ok(self.mldsa()?.generation)
271 }
272 pub fn mldsa_public_key(&self) -> Result<mldsa::PublicKeyBytes, CoreError> {
273 let key = self.mldsa()?;
274 if subtensor_hashed::falcon::is_account_profile(key.descriptor.algorithm) {
275 return Err(crypto_err("use pq_public_key for Falcon public keys"));
276 }
277 mldsa::public_key_from_seed(&*key.seed_at(ROLE_TRANSACTION, key.generation)?)
278 .map_err(crypto_err)
279 }
280 pub fn mldsa_commitment(&self, generation: u64) -> Result<[u8; 64], CoreError> {
281 let key = self.mldsa()?;
282 if generation == 0 || !key.descriptor.is_rotating() {
283 return Ok(key.descriptor.initial_commitment);
284 }
285 falcon::commitment(
286 key.descriptor.algorithm,
287 &falcon::public_key(
288 key.descriptor.algorithm,
289 &*key.seed_at(ROLE_TRANSACTION, generation)?,
290 )?,
291 )
292 }
293 pub fn mldsa_current_commitment(&self) -> Result<[u8; 64], CoreError> {
294 self.mldsa_commitment(self.mldsa_generation()?)
295 }
296 pub fn mldsa_next_commitment(&self) -> Result<[u8; 64], CoreError> {
297 self.mldsa_commitment(
298 self.mldsa_generation()?
299 .checked_add(1)
300 .ok_or_else(|| crypto_err("post-quantum signing generation exhausted"))?,
301 )
302 }
303
304 pub fn sign_mldsa(&self, payload: &[u8]) -> Result<Vec<u8>, CoreError> {
307 self.sign_mldsa_with_randomness(payload, &*randomness()?)
308 }
309
310 pub fn sign_mldsa_with_randomness(
312 &self,
313 payload: &[u8],
314 random: &[u8; 32],
315 ) -> Result<Vec<u8>, CoreError> {
316 let key = self.mldsa()?;
317 if subtensor_hashed::falcon::is_account_profile(key.descriptor.algorithm) {
318 return self.sign_falcon(payload, random);
319 }
320 let seed = key.seed_at(ROLE_TRANSACTION, key.generation)?;
321 let header = mldsa::Header {
322 account: key.account_id(),
323 version: mldsa::VERSION,
324 algorithm: key.descriptor.algorithm,
325 generation: key.generation,
326 public_key: mldsa::public_key_from_seed(&seed).map_err(crypto_err)?,
327 next_commitment: self.mldsa_next_commitment()?,
328 };
329 Ok(header
330 .sign(&seed, payload, random)
331 .map_err(crypto_err)?
332 .encode())
333 }
334
335 pub fn authorization_descriptor(&self) -> Result<Vec<u8>, CoreError> {
338 if is_pq_crypto(self.crypto_type()) {
339 self.mldsa_descriptor()
340 } else {
341 self.hashed_descriptor()
342 }
343 }
344 pub fn authorization_generation(&self) -> Result<u64, CoreError> {
345 if is_pq_crypto(self.crypto_type()) {
346 self.mldsa_generation()
347 } else {
348 self.hashed_generation()
349 }
350 }
351 pub fn authorization_public_key(&self) -> Result<Vec<u8>, CoreError> {
352 if is_pq_crypto(self.crypto_type()) {
353 self.pq_public_key()
354 } else {
355 self.hashed_signing_public_key()
356 }
357 }
358 pub fn authorization_commitment(&self, generation: u64) -> Result<Vec<u8>, CoreError> {
359 if is_pq_crypto(self.crypto_type()) {
360 Ok(self.mldsa_commitment(generation)?.to_vec())
361 } else {
362 Ok(self.hashed_commitment(generation)?.to_vec())
363 }
364 }
365 pub fn authorization_current_commitment(&self) -> Result<Vec<u8>, CoreError> {
366 self.authorization_commitment(self.authorization_generation()?)
367 }
368 pub fn authorization_next_commitment(&self) -> Result<Vec<u8>, CoreError> {
369 self.authorization_commitment(
370 self.authorization_generation()?
371 .checked_add(1)
372 .ok_or_else(|| crypto_err("signing generation exhausted"))?,
373 )
374 }
375 pub fn sign_authorization(&self, payload: &[u8]) -> Result<Vec<u8>, CoreError> {
376 if is_pq_crypto(self.crypto_type()) {
377 self.sign_mldsa(payload)
378 } else {
379 self.sign_hashed(payload)
380 }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 #![allow(clippy::unwrap_used, clippy::expect_used)]
387 use super::*;
388 use crate::keys::{
389 decode_hashed_receiving_address, encode_hashed_receiving_address, CRYPTO_HASHED,
390 CRYPTO_SR25519,
391 };
392
393 #[test]
395 fn hkdf_sha512_transaction_vectors_preserve_recovery() {
396 for (role, index, expected) in [
398 (
399 0,
400 0,
401 "ee5932d8d7f72ace8a35e6a7adaf0ddc3f247fb361e4a5002653af9fbc30675d",
402 ),
403 (
404 0,
405 41,
406 "226f863895c02488668a98d3bddf3796368b9616de8cea8143c65a9ee92ca379",
407 ),
408 ] {
409 assert_eq!(
410 hex::encode(*MldsaKeypair::derive_seed(&[7; 32], role, index).unwrap()),
411 expected
412 );
413 }
414 assert_eq!(
416 hex::encode(
417 *MldsaKeypair::derive_seed_for_profile(&[7; 32], mldsa::FIXED_ALGORITHM, 0, 0)
418 .unwrap()
419 ),
420 "753832bab3583b97195c3878523e70d03a5695675a62c6527a004ef1957cb736"
421 );
422 for unsupported_role in [1, 2, 3, u8::MAX] {
423 assert!(MldsaKeypair::derive_seed(&[7; 32], unsupported_role, 0).is_err());
424 }
425 }
426
427 #[test]
428 fn mnemonic_convention_requires_256_bits_and_supports_passphrase_restore() {
429 let phrase = [vec!["abandon"; 23], vec!["art"]].concat().join(" ");
430 let wallet = Keypair::from_mnemonic(&phrase, CRYPTO_MLDSA65, Some("TREZOR")).unwrap();
431 assert_eq!(
432 hex::encode(wallet.seed_bytes().unwrap()),
433 "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd30971"
434 );
435 assert!(wallet.mnemonic().is_none());
436 let restored = Keypair::from_seed(wallet.seed_bytes().unwrap(), CRYPTO_MLDSA65).unwrap();
437 assert_eq!(
438 wallet.mldsa_descriptor().unwrap(),
439 restored.mldsa_descriptor().unwrap()
440 );
441 assert_ne!(
442 wallet.public_key_bytes(),
443 Keypair::from_mnemonic(&phrase, CRYPTO_MLDSA65, None)
444 .unwrap()
445 .public_key_bytes()
446 );
447 assert!(Keypair::from_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", CRYPTO_MLDSA65, None).is_err());
448 }
449
450 #[test]
451 fn rotation_restore_keeps_identity_and_signs_complete_payload() {
452 let wallet = Keypair::from_seed(&[7; 32], CRYPTO_MLDSA65).unwrap();
453 let active = wallet.at_generation(41).unwrap();
454 let restored = Keypair::from_seed(&[7; 32], CRYPTO_MLDSA65)
455 .unwrap()
456 .at_generation(41)
457 .unwrap();
458 assert_eq!(wallet.public_key_bytes(), active.public_key_bytes());
459 assert_eq!(
460 active.mldsa_current_commitment().unwrap(),
461 restored.mldsa_current_commitment().unwrap()
462 );
463 assert_ne!(
464 wallet.mldsa_public_key().unwrap(),
465 active.mldsa_public_key().unwrap()
466 );
467 let payload = vec![19; 500];
468 let encoded = active
469 .sign_mldsa_with_randomness(&payload, &[0; 32])
470 .unwrap();
471 assert_eq!(encoded.len(), 5367);
472 let proof = mldsa::SignerAddress::from_bytes(&encoded).unwrap();
473 assert!(proof.verify(&payload));
474 let descriptor = wallet.mldsa_descriptor().unwrap();
475 let commitment = active.mldsa_current_commitment().unwrap();
476 assert!(
477 verify_mldsa_authorization(&encoded, &payload, &descriptor, 41, &commitment).unwrap()
478 );
479 assert!(
480 !verify_mldsa_authorization(&encoded, &payload, &descriptor, 42, &commitment).unwrap()
481 );
482 assert!(
483 !verify_mldsa_authorization(&encoded, &payload, &descriptor, 41, &[0; 64]).unwrap()
484 );
485 assert!(!verify_mldsa_authorization(
486 &encoded[..5366],
487 &payload,
488 &descriptor,
489 41,
490 &commitment
491 )
492 .unwrap());
493 assert_eq!(proof.account, wallet.public_key_bytes());
494 assert_eq!(
495 proof.next_commitment,
496 restored.mldsa_commitment(42).unwrap()
497 );
498 assert!(!proof.verify(&sp_core::hashing::blake2_256(&payload)));
499 let mut wrong = proof.clone();
500 wrong.account[0] ^= 1;
501 assert!(!wrong.verify(&payload));
502 let mut wrong = proof.clone();
503 wrong.next_commitment[0] ^= 1;
504 assert!(!wrong.verify(&payload));
505 let mut wrong = proof;
506 wrong.generation += 1;
507 assert!(!wrong.verify(&payload));
508 assert_eq!(active.mldsa_generation().unwrap(), 41);
509 assert!(wallet
510 .at_generation(u64::MAX)
511 .unwrap()
512 .sign_mldsa(b"no wrap")
513 .is_err());
514 }
515
516 #[test]
517 fn offline_authorization_rejects_cryptographically_valid_unusable_rotations() {
518 let wallet = Keypair::from_seed(&[7; 32], CRYPTO_MLDSA65).unwrap();
519 let descriptor = wallet.mldsa_descriptor().unwrap();
520 let payload = vec![19; 500];
521 let current = wallet.mldsa_commitment(7).unwrap();
522 let next = wallet.mldsa_commitment(8).unwrap();
523 for (generation, next_commitment, accepted) in [
524 (7, next, true),
525 (7, [0; 64], false),
526 (7, current, false),
527 (u64::MAX, next, false),
528 (
529 u64::MAX - 1,
530 wallet.mldsa_commitment(u64::MAX).unwrap(),
531 true,
532 ),
533 ] {
534 let active = wallet.at_generation(generation).unwrap();
535 let commitment = active.mldsa_current_commitment().unwrap();
536 let signing_seed = active
537 .mldsa()
538 .unwrap()
539 .seed_at(ROLE_TRANSACTION, generation)
540 .unwrap();
541 let proof = mldsa::Header {
545 account: wallet.public_key_bytes(),
546 version: mldsa::VERSION,
547 algorithm: mldsa::ALGORITHM,
548 generation,
549 public_key: active.mldsa_public_key().unwrap(),
550 next_commitment,
551 }
552 .sign(&signing_seed, &payload, &[0; 32])
553 .unwrap();
554 assert!(proof.verify(&payload));
555 if generation == 7 && next_commitment == [0; 64] {
556 if let Ok(path) = std::env::var("MLDSA_ZERO_NEXT_FIXTURE") {
560 std::fs::write(path, hex::encode(proof.encode())).unwrap();
561 } else {
562 assert_eq!(
563 proof.encode(),
564 hex::decode(
565 include_str!(
566 "../../../python/tests/fixtures/mldsa-zero-next-proof.hex"
567 )
568 .trim()
569 )
570 .unwrap(),
571 );
572 }
573 }
574 assert_eq!(
575 verify_mldsa_authorization(
576 &proof.encode(),
577 &payload,
578 &descriptor,
579 generation,
580 &commitment,
581 )
582 .unwrap(),
583 accepted,
584 "generation {generation}, next commitment {next_commitment:?}",
585 );
586 }
587 }
588
589 #[test]
590 fn watch_only_keeps_descriptor_but_never_signs_or_reveals_keys() {
591 let wallet = Keypair::from_seed(&[7; 32], CRYPTO_MLDSA65).unwrap();
592 let public = wallet.public_only().unwrap();
593 assert_eq!(
594 wallet.mldsa_descriptor().unwrap(),
595 public.mldsa_descriptor().unwrap()
596 );
597 assert_eq!(wallet.public_key_bytes(), public.public_key_bytes());
598 assert!(public.private_key_bytes().is_none());
599 assert!(public.mldsa_public_key().is_err());
600 assert!(public.sign_mldsa(b"no private material").is_err());
601 assert!(wallet.sign(b"arbitrary message").is_err());
602 assert!(Keypair::from_uri("//Alice", CRYPTO_MLDSA65).is_err());
603 for crypto in [CRYPTO_HASHED, CRYPTO_SR25519] {
604 assert_ne!(
605 wallet.public_key_bytes(),
606 Keypair::from_seed(&[7; 32], crypto)
607 .unwrap()
608 .public_key_bytes()
609 );
610 }
611 }
612
613 #[test]
614 fn receiving_mldsa_vector_checksum_and_canonical_spelling() {
615 let descriptor = mldsa::Descriptor {
616 version: 2,
617 algorithm: 4,
618 initial_commitment: [7; 64],
619 }
620 .encode();
621 let address = encode_hashed_receiving_address(&descriptor).unwrap();
622 assert_eq!(
623 address,
624 "tao_AgQHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHhs8-H4mAmmA"
625 );
626 assert_eq!(address.len(), 103);
627 for spelling in [address.clone(), address.replacen("tao_", "bth2_", 1)] {
628 let decoded = decode_hashed_receiving_address(&spelling).unwrap();
629 assert_eq!(decoded, descriptor);
630 assert_eq!(encode_hashed_receiving_address(&decoded).unwrap(), address);
631 for invalid in [
632 format!("{spelling}="),
633 format!(" {spelling}"),
634 format!("{spelling}\n"),
635 spelling.to_uppercase(),
636 spelling[..spelling.len() - 1].to_string(),
637 ] {
638 assert!(decode_hashed_receiving_address(&invalid).is_err());
639 }
640 let mut bytes = spelling.into_bytes();
641 bytes[30] = if bytes[30] == b'A' { b'B' } else { b'A' };
642 assert!(decode_hashed_receiving_address(std::str::from_utf8(&bytes).unwrap()).is_err());
643 }
644 assert!(decode_hashed_receiving_address(&address.replacen("tao_", "bth1_", 1)).is_err());
645 }
646
647 #[cfg(feature = "host")]
648 #[test]
649 fn versioned_private_and_public_keyfiles_preserve_mldsa() {
650 use crate::keyfiles::{
651 deserialize_keypair_from_keyfile_data, serialized_keypair_to_keyfile_data,
652 };
653 for crypto in [CRYPTO_MLDSA65, CRYPTO_MLDSA65_FIXED] {
654 let wallet = Keypair::from_seed(&[7; 32], crypto).unwrap();
655 for key in [wallet.public_only().unwrap(), wallet] {
656 let bytes = serialized_keypair_to_keyfile_data(&key).unwrap();
657 let restored = deserialize_keypair_from_keyfile_data(&bytes).unwrap();
658 assert_eq!(
659 key.mldsa_descriptor().unwrap(),
660 restored.mldsa_descriptor().unwrap()
661 );
662 assert_eq!(
663 key.private_key_bytes().is_some(),
664 restored.private_key_bytes().is_some()
665 );
666 let mut json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
667 json["cryptoType"] = serde_json::json!(if crypto == CRYPTO_MLDSA65 {
668 CRYPTO_MLDSA65_FIXED
669 } else {
670 CRYPTO_MLDSA65
671 });
672 assert!(
673 deserialize_keypair_from_keyfile_data(&serde_json::to_vec(&json).unwrap())
674 .is_err()
675 );
676 }
677 }
678 }
679 #[test]
680 fn standard_mldsa_creation_uses_a_recoverable_stateless_key_and_ordinary_address() {
681 for value in [0, 1, 7, 19, 42, 255] {
682 let seed = [value; 32];
683 let key = Keypair::from_seed(&seed, CRYPTO_MLDSA65_FIXED).unwrap();
684 assert_eq!(key.mldsa_profile().unwrap(), mldsa::FIXED_ALGORITHM);
685 assert!(mldsa::rejects_legacy_public_keys(&key.public_key_bytes()));
686 assert_eq!(key.hashed_receiving_address().unwrap(), key.ss58_address());
687 assert_eq!(
688 encode_hashed_receiving_address(&key.mldsa_descriptor().unwrap()).unwrap(),
689 key.ss58_address()
690 );
691 let restored = Keypair::from_seed(&seed, CRYPTO_MLDSA65_FIXED).unwrap();
692 assert_eq!(
693 restored.mldsa_descriptor().unwrap(),
694 key.mldsa_descriptor().unwrap()
695 );
696 assert_eq!(
697 restored.mldsa_public_key().unwrap(),
698 key.mldsa_public_key().unwrap()
699 );
700 assert_eq!(key.mldsa_generation().unwrap(), 0);
701 assert!(key.at_generation(1).is_err());
702 assert!(key.at_generation(u64::MAX).is_err());
703 let proof = key
704 .sign_mldsa_with_randomness(b"call/nonce/era/genesis", &[0; 32])
705 .unwrap();
706 let commitment = key.mldsa_current_commitment().unwrap();
707 assert_eq!(key.authorization_next_commitment().unwrap(), commitment);
708 assert!(verify_mldsa_authorization(
709 &proof,
710 b"call/nonce/era/genesis",
711 &key.mldsa_descriptor().unwrap(),
712 0,
713 &commitment
714 )
715 .unwrap());
716 assert!(!verify_mldsa_authorization(
717 &proof,
718 b"call/nonce/era/genesis",
719 &key.mldsa_descriptor().unwrap(),
720 1,
721 &commitment
722 )
723 .unwrap());
724 assert!(!verify_mldsa_authorization(
725 &proof,
726 b"other call/nonce/era/genesis",
727 &key.mldsa_descriptor().unwrap(),
728 0,
729 &commitment
730 )
731 .unwrap());
732 let rotating = Keypair::from_seed(&seed, CRYPTO_MLDSA65).unwrap();
733 assert_ne!(key.public_key_bytes(), rotating.public_key_bytes());
734 assert!(!verify_mldsa_authorization(
735 &proof,
736 b"call/nonce/era/genesis",
737 &rotating.mldsa_descriptor().unwrap(),
738 0,
739 &commitment
740 )
741 .unwrap());
742 let watch =
743 Keypair::new(Some(&key.ss58_address()), None, CRYPTO_MLDSA65_FIXED, 42).unwrap();
744 assert_eq!(
745 watch.hashed_receiving_address().unwrap(),
746 key.ss58_address()
747 );
748 assert!(watch.mldsa_profile().is_err()); assert!(watch.sign_mldsa(b"call").is_err());
750 let descriptor_watch = key.public_only().unwrap();
751 assert_eq!(
752 descriptor_watch.hashed_receiving_address().unwrap(),
753 key.ss58_address()
754 );
755 assert_eq!(
756 descriptor_watch.mldsa_profile().unwrap(),
757 mldsa::FIXED_ALGORITHM
758 );
759 assert!(descriptor_watch.sign_mldsa(b"call").is_err());
760 }
761 assert!(MldsaKeypair::stateless_transaction_key(&[7; 32], 0).is_err());
762 }
763
764 #[test]
765 fn fixed_account_derivation_and_recovery_vector_is_stable() {
766 let key = Keypair::from_seed(&[7; 32], CRYPTO_MLDSA65_FIXED).unwrap();
767 assert_eq!(
768 hex::encode(key.public_key_bytes()),
769 "aa4018475de4922453c911cc9b469f6c5b0c85135e77a3db98d8f6fe325064ef"
770 );
771 assert_eq!(hex::encode(key.mldsa_descriptor().unwrap()), "020553f8916ec9b0b73b929d66357198a2ca70b33070e3b740e00c35cfb310c1c03e7c6ec79aaa8fad9714f94504759de1bbe3bf504f81725e0c348466b05996f49b");
772 assert_eq!(
773 key.ss58_address(),
774 "5Fuw396EmC877oPz39aopWXWdEwdJ3KkmYVzpWsDcPTsoy8d"
775 );
776 let index = key.mldsa().unwrap().transaction_index;
777 assert!(index < STATELESS_KEY_SEARCH_LIMIT);
778 assert!(MldsaKeypair::stateless_transaction_key(&[7; 32], index).is_err());
781 assert_eq!(
782 MldsaKeypair::stateless_transaction_key(&[7; 32], index + 1).unwrap(),
783 (index, key.mldsa_current_commitment().unwrap())
784 );
785 }
786
787 #[cfg(feature = "host")]
788 #[test]
789 fn every_mldsa_profile_restores_its_recorded_identity_from_seed_and_mnemonic_keyfiles() {
790 use crate::keyfiles::{
791 deserialize_keypair_from_keyfile_data, serialized_keypair_to_keyfile_data,
792 };
793 let phrase = [vec!["abandon"; 23], vec!["art"]].concat().join(" ");
794 for (profile, crypto) in [
795 (mldsa::ALGORITHM, CRYPTO_MLDSA65),
796 (mldsa::FIXED_ALGORITHM, CRYPTO_MLDSA65_FIXED),
797 ] {
798 for key in [
799 Keypair::from_seed(&[7; 32], crypto).unwrap(),
800 Keypair::from_mnemonic(&phrase, crypto, None).unwrap(),
801 ] {
802 for original in [key.public_only().unwrap(), key] {
803 let serialized = serialized_keypair_to_keyfile_data(&original).unwrap();
804 let restored = deserialize_keypair_from_keyfile_data(&serialized).unwrap();
805 assert_eq!(restored.mldsa_profile().unwrap(), profile);
806 assert_eq!(
807 restored.mldsa_descriptor().unwrap(),
808 original.mldsa_descriptor().unwrap()
809 );
810 assert_eq!(restored.ss58_address(), original.ss58_address());
811 if original.private_key_bytes().is_some() {
812 assert_eq!(
813 restored.mldsa_public_key().unwrap(),
814 original.mldsa_public_key().unwrap()
815 );
816 }
817 let mut corrupt: serde_json::Value =
818 serde_json::from_slice(&serialized).unwrap();
819 let mut descriptor = original.mldsa_descriptor().unwrap();
820 descriptor[1] = if profile == mldsa::FIXED_ALGORITHM {
821 mldsa::ALGORITHM
822 } else {
823 mldsa::FIXED_ALGORITHM
824 };
825 corrupt["mldsaDescriptor"] =
826 serde_json::json!(format!("0x{}", hex::encode(descriptor)));
827 assert!(deserialize_keypair_from_keyfile_data(
828 &serde_json::to_vec(&corrupt).unwrap()
829 )
830 .is_err());
831 }
832 }
833 }
834 }
835
836 #[cfg(feature = "host")]
837 #[test]
838 fn fixed_address_only_watch_keyfiles_restore_without_claiming_private_authority() {
839 use crate::keyfiles::{
840 deserialize_keypair_from_keyfile_data, serialized_keypair_to_keyfile_data,
841 };
842 let wallet = Keypair::from_seed(&[7; 32], CRYPTO_MLDSA65_FIXED).unwrap();
843 let watch =
844 Keypair::new(Some(&wallet.ss58_address()), None, CRYPTO_MLDSA65_FIXED, 42).unwrap();
845 let serialized = serialized_keypair_to_keyfile_data(&watch).unwrap();
846 let mut json: serde_json::Value = serde_json::from_slice(&serialized).unwrap();
847 assert_eq!(json["walletVersion"], 2);
848 assert!(json.get("mldsaDescriptor").is_none());
849 let restored = deserialize_keypair_from_keyfile_data(&serialized).unwrap();
850 assert_eq!(restored.ss58_address(), wallet.ss58_address());
851 assert_eq!(restored.crypto_type(), CRYPTO_MLDSA65_FIXED);
852 assert_eq!(restored.account_type(), "standard");
853 assert_eq!(
854 restored.hashed_receiving_address().unwrap(),
855 wallet.ss58_address()
856 );
857 assert!(restored.private_key_bytes().is_none());
858 assert!(restored.mldsa_descriptor().is_err());
859 assert!(restored.mldsa_profile().is_err());
860 assert!(restored.sign_mldsa(b"call").is_err());
861 json["secretSeed"] = serde_json::json!(format!("0x{}", hex::encode([7; 32])));
862 assert!(
863 deserialize_keypair_from_keyfile_data(&serde_json::to_vec(&json).unwrap()).is_err()
864 );
865 }
866
867 #[cfg(feature = "host")]
868 #[test]
870 fn unsafe_pre_release_fixed_descriptor_cannot_be_reinterpreted_during_recovery() {
871 use crate::keyfiles::{
872 deserialize_keypair_from_keyfile_data, serialized_keypair_to_keyfile_data,
873 };
874 for value in 0u8..=16 {
875 let master = [value; 32];
876 let original_seed = MldsaKeypair::derive_seed_for_profile(
877 &master,
878 mldsa::FIXED_ALGORITHM,
879 ROLE_TRANSACTION,
880 0,
881 )
882 .unwrap();
883 let public_key = mldsa::public_key_from_seed(&original_seed).unwrap();
884 let descriptor = mldsa::Descriptor {
885 version: mldsa::VERSION,
886 algorithm: mldsa::FIXED_ALGORITHM,
887 initial_commitment: mldsa::commitment(&public_key),
888 };
889 let unsafe_account = mldsa::account_id(&descriptor);
890 if mldsa::rejects_legacy_public_keys(&unsafe_account) {
891 continue;
892 }
893 let current = Keypair::from_seed(&master, CRYPTO_MLDSA65_FIXED).unwrap();
894 assert_ne!(current.public_key_bytes(), unsafe_account);
895 assert!(Keypair::from_mldsa_descriptor(&descriptor.encode(), 42).is_err());
896 for key in [current.public_only().unwrap(), current] {
897 let encoded = serialized_keypair_to_keyfile_data(&key).unwrap();
898 let mut json: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
899 json["mldsaDescriptor"] =
900 serde_json::json!(format!("0x{}", hex::encode(descriptor.encode())));
901 json["ss58Address"] =
902 serde_json::json!(super::super::ss58_from_public(unsafe_account, 42));
903 for field in ["accountId", "publicKey"] {
904 json[field] = serde_json::json!(format!("0x{}", hex::encode(unsafe_account)));
905 }
906 assert!(
907 deserialize_keypair_from_keyfile_data(&serde_json::to_vec(&json).unwrap())
908 .is_err()
909 );
910 }
911 return;
912 }
913 panic!("test seeds did not contain a legacy-curve account");
914 }
915}