1use codec::{Decode, Encode};
5use ed25519_zebra::{SigningKey, VerificationKey};
6use hkdf::Hkdf;
7use sha2::Sha256;
8use sp_core::{crypto::Pair as _, ed25519, sr25519};
9use subtensor_hashed::{account_id, key_commitment, Descriptor, Proof, Scheme, SignerAddress};
10use zeroize::Zeroizing;
11
12use super::{crypto_err, Keypair, KeypairInner, CRYPTO_HASHED, CRYPTO_HASHED_ED25519};
13use crate::error::CoreError;
14
15const VERSION: u8 = 1;
16const DERIVATION_SALT: &[u8] = b"bittensor/hashed/v1/derive";
17
18pub fn verify_hashed_authorization(
23 encoded: &[u8],
24 payload: &[u8],
25 descriptor: &[u8],
26 generation: u64,
27 expected_commitment: &[u8; 32],
28) -> Result<bool, CoreError> {
29 let mut input = descriptor;
30 let descriptor = Descriptor::decode(&mut input)
31 .map_err(|_| crypto_err("invalid hashed account descriptor"))?;
32 if !input.is_empty() || !descriptor.is_supported() {
33 return Err(crypto_err("invalid hashed account descriptor"));
34 }
35 if encoded.len() != 8 + 32 + 32 + 64 || generation == u64::MAX {
37 return Ok(false);
38 }
39 let proof = Proof::decode(&mut &encoded[..])
40 .map_err(|_| crypto_err("invalid hashed authorization proof"))?;
41 if proof.generation != generation
42 || key_commitment(descriptor.scheme, &proof.public_key) != *expected_commitment
43 || proof.next_commitment == [0; 32]
44 || proof.next_commitment == *expected_commitment
45 {
46 return Ok(false);
47 }
48 let address = SignerAddress {
49 account: account_id(&descriptor),
50 version: descriptor.version,
51 scheme: descriptor.scheme,
52 generation,
53 public_key: proof.public_key,
54 next_commitment: proof.next_commitment,
55 };
56 let message = address.signing_hash(payload);
57 Ok(match descriptor.scheme {
58 Scheme::Sr25519 => sr25519::Pair::verify(
59 &sr25519::Signature::from_raw(proof.signature),
60 message,
61 &sr25519::Public::from_raw(proof.public_key),
62 ),
63 Scheme::Ed25519 => ed25519::Pair::verify(
64 &ed25519::Signature::from_raw(proof.signature),
65 message,
66 &ed25519::Public::from_raw(proof.public_key),
67 ),
68 })
69}
70
71pub struct HashedKeypair {
74 descriptor: Descriptor,
75 master_seed: Option<Zeroizing<[u8; 32]>>,
76 generation: u64,
77}
78
79impl HashedKeypair {
80 pub(super) fn from_seed(seed: &[u8], crypto_type: u8) -> Result<Self, CoreError> {
81 if seed.len() != 32 {
82 return Err(crypto_err("hashed master seed must be exactly 32 bytes"));
83 }
84 let mut master_seed = Zeroizing::new([0u8; 32]);
87 master_seed.copy_from_slice(seed);
88 let scheme = match crypto_type {
89 CRYPTO_HASHED => Scheme::Sr25519,
90 CRYPTO_HASHED_ED25519 => Scheme::Ed25519,
91 _ => return Err(crypto_err("unsupported hashed signing scheme")),
92 };
93 let version = VERSION;
94 let initial_commitment = match scheme {
95 Scheme::Sr25519 => key_commitment(scheme, &Self::derive(&master_seed, 0)?.public().0),
96 Scheme::Ed25519 => {
97 key_commitment(scheme, &ed25519_public(&Self::derive_ed(&master_seed, 0)?))
98 }
99 };
100 Ok(Self {
101 descriptor: Descriptor {
102 version,
103 scheme,
104 initial_commitment,
105 },
106 master_seed: Some(master_seed),
107 generation: 0,
108 })
109 }
110
111 fn derive(master: &[u8; 32], generation: u64) -> Result<sr25519::Pair, CoreError> {
112 let kdf = Hkdf::<Sha256>::new(Some(DERIVATION_SALT), master);
115 let info = (VERSION, Scheme::Sr25519, generation).encode();
116 let mut seed = Zeroizing::new([0u8; 32]);
117 kdf.expand(&info, seed.as_mut())
118 .map_err(|_| crypto_err("hashed signing-key derivation failed"))?;
119 Ok(sr25519::Pair::from_seed(&seed))
120 }
121
122 fn derive_ed(master: &[u8; 32], generation: u64) -> Result<Zeroizing<SigningKey>, CoreError> {
127 let kdf = Hkdf::<Sha256>::new(Some(DERIVATION_SALT), master);
128 let mut seed = Zeroizing::new([0u8; 32]);
129 kdf.expand(
130 &(VERSION, Scheme::Ed25519, generation).encode(),
131 seed.as_mut(),
132 )
133 .map_err(|_| crypto_err("Ed25519 signing-key derivation failed"))?;
134 let secret = SigningKey::try_from(&seed[..])
137 .map_err(|_| crypto_err("Ed25519 signing-key derivation failed"))?;
138 Ok(Zeroizing::new(secret))
139 }
140
141 fn ed_at(&self, generation: u64) -> Result<Zeroizing<SigningKey>, CoreError> {
142 let master = self
143 .master_seed()
144 .ok_or_else(|| crypto_err("signing requires a private wallet"))?;
145 Self::derive_ed(master, generation)
146 }
147
148 pub(super) fn crypto_type(&self) -> u8 {
149 match self.descriptor.scheme {
150 Scheme::Sr25519 => CRYPTO_HASHED,
151
152 Scheme::Ed25519 => CRYPTO_HASHED_ED25519,
153 }
154 }
155
156 pub(super) fn master_seed(&self) -> Option<&[u8; 32]> {
157 self.master_seed.as_deref()
158 }
159
160 pub(super) fn account_id(&self) -> [u8; 32] {
161 account_id(&self.descriptor)
162 }
163
164 fn pair_at(&self, generation: u64) -> Result<sr25519::Pair, CoreError> {
165 let master = self.master_seed().ok_or_else(|| {
166 crypto_err("hashed signing-key derivation requires the mnemonic or master seed")
167 })?;
168 Self::derive(master, generation)
169 }
170}
171
172impl Keypair {
173 fn hashed(&self) -> Result<&HashedKeypair, CoreError> {
174 match &self.inner {
175 KeypairInner::Hashed(hashed) => Ok(hashed),
176 _ => Err(crypto_err("operation requires a hashed key descriptor")),
177 }
178 }
179
180 pub fn from_hashed_descriptor(encoded: &[u8], ss58_format: u16) -> Result<Self, CoreError> {
182 if encoded.first() == Some(&subtensor_hashed::mldsa::VERSION) {
183 return Self::from_mldsa_descriptor(encoded, ss58_format);
184 }
185 let mut input = encoded;
186 let descriptor = Descriptor::decode(&mut input)
187 .map_err(|_| crypto_err("invalid or unsupported hashed account descriptor"))?;
188 if !input.is_empty() || !descriptor.is_supported() {
189 return Err(crypto_err(
190 "invalid or unsupported hashed account descriptor",
191 ));
192 }
193 Ok(Self {
194 inner: KeypairInner::Hashed(HashedKeypair {
195 descriptor,
196 master_seed: None,
197 generation: 0,
198 }),
199 ss58_format,
200 mnemonic: None,
201 seed: None,
202 })
203 }
204
205 pub fn public_only(&self) -> Result<Self, CoreError> {
207 if matches!(&self.inner, KeypairInner::Mldsa(_)) {
208 return self.mldsa_public_only();
209 }
210 if let KeypairInner::Hashed(hashed) = &self.inner {
211 let mut public =
212 Self::from_hashed_descriptor(&hashed.descriptor.encode(), self.ss58_format)?;
213 if let KeypairInner::Hashed(copy) = &mut public.inner {
214 copy.generation = hashed.generation;
215 }
216 return Ok(public);
217 }
218 Self::new(
219 None,
220 Some(&self.public_key_bytes()),
221 self.crypto_type(),
222 self.ss58_format,
223 )
224 }
225
226 pub fn at_generation(&self, generation: u64) -> Result<Self, CoreError> {
229 if super::is_pq_crypto(self.crypto_type()) {
230 return self.mldsa_at_generation(generation);
231 }
232 let hashed = self.hashed()?;
233 if hashed.master_seed.is_none() {
234 return Err(crypto_err(
235 "hashed generation selection requires a private wallet",
236 ));
237 }
238 Ok(Self {
239 inner: KeypairInner::Hashed(HashedKeypair {
240 descriptor: hashed.descriptor,
241 master_seed: hashed.master_seed.clone(),
242 generation,
243 }),
244 ss58_format: self.ss58_format,
245 mnemonic: self.mnemonic.clone(),
246 seed: self.seed.clone(),
247 })
248 }
249
250 pub fn hashed_descriptor(&self) -> Result<Vec<u8>, CoreError> {
251 Ok(self.hashed()?.descriptor.encode())
252 }
253
254 pub fn hashed_generation(&self) -> Result<u64, CoreError> {
255 Ok(self.hashed()?.generation)
256 }
257
258 pub fn hashed_public_key(&self) -> Result<[u8; 32], CoreError> {
259 let hashed = self.hashed()?;
260 if hashed.descriptor.scheme == Scheme::Ed25519 {
261 return Ok(ed25519_public(&hashed.ed_at(hashed.generation)?));
262 }
263 Ok(hashed.pair_at(hashed.generation)?.public().0)
264 }
265
266 pub fn hashed_commitment(&self, generation: u64) -> Result<[u8; 32], CoreError> {
267 let hashed = self.hashed()?;
268 if generation == 0 {
269 return Ok(hashed.descriptor.initial_commitment);
270 }
271 Ok(match hashed.descriptor.scheme {
272 Scheme::Sr25519 => {
273 key_commitment(Scheme::Sr25519, &hashed.pair_at(generation)?.public().0)
274 }
275 Scheme::Ed25519 => {
276 key_commitment(Scheme::Ed25519, &ed25519_public(&hashed.ed_at(generation)?))
277 }
278 })
279 }
280
281 pub fn hashed_current_commitment(&self) -> Result<[u8; 32], CoreError> {
282 self.hashed_commitment(self.hashed_generation()?)
283 }
284
285 pub fn hashed_next_commitment(&self) -> Result<[u8; 32], CoreError> {
286 let next = self
287 .hashed_generation()?
288 .checked_add(1)
289 .ok_or_else(|| crypto_err("hashed signing generation exhausted"))?;
290 self.hashed_commitment(next)
291 }
292
293 pub fn sign_hashed(&self, payload: &[u8]) -> Result<Vec<u8>, CoreError> {
297 let hashed = self.hashed()?;
298 let address = SignerAddress {
299 version: VERSION,
300 scheme: hashed.descriptor.scheme,
301 account: self.public_key_bytes(),
302 generation: hashed.generation,
303 public_key: self.hashed_public_key()?,
304 next_commitment: self.hashed_next_commitment()?,
305 };
306 let payload = address.signing_hash(payload);
307 let signature = match hashed.descriptor.scheme {
308 Scheme::Sr25519 => hashed.pair_at(hashed.generation)?.sign(&payload).0,
309 Scheme::Ed25519 => hashed.ed_at(hashed.generation)?.sign(&payload).to_bytes(),
310 };
311 Ok(Proof {
312 generation: hashed.generation,
313 public_key: self.hashed_public_key()?,
314 next_commitment: self.hashed_next_commitment()?,
315 signature,
316 }
317 .encode())
318 }
319
320 pub fn hashed_signing_public_key(&self) -> Result<Vec<u8>, CoreError> {
321 let hashed = self.hashed()?;
322 Ok(match hashed.descriptor.scheme {
323 Scheme::Sr25519 => hashed.pair_at(hashed.generation)?.public().0.to_vec(),
324 Scheme::Ed25519 => ed25519_public(&hashed.ed_at(hashed.generation)?).to_vec(),
325 })
326 }
327
328 pub(super) fn sign_hashed_message(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
332 let hashed = self.hashed()?;
333 let payload = message_payload(&hashed.account_id(), message);
334 let mut envelope = MESSAGE_MAGIC.to_vec();
335 envelope.extend(hashed.descriptor.encode());
336 match hashed.descriptor.scheme {
337 Scheme::Sr25519 => {
338 if hashed.generation == 0 {
339 return Err(crypto_err("hashed message signing requires generation zero to be retired on chain first"));
340 }
341 let pair = hashed.pair_at(0)?;
342 envelope.extend_from_slice(&pair.public().0);
343 envelope.extend_from_slice(&pair.sign(&payload).0);
344 }
345 Scheme::Ed25519 => {
346 if hashed.generation == 0 {
347 return Err(crypto_err("hashed message signing requires generation zero to be retired on chain first"));
348 }
349 let secret = hashed.ed_at(0)?;
350 envelope.extend_from_slice(&ed25519_public(&secret));
351 envelope.extend_from_slice(&secret.sign(&payload).to_bytes());
352 }
353 }
354 Ok(envelope)
355 }
356}
357
358const MESSAGE_MAGIC: &[u8] = b"BTHMSG01";
359
360fn ed25519_public(secret: &Zeroizing<SigningKey>) -> [u8; 32] {
361 let secret: &SigningKey = secret;
362 VerificationKey::from(secret).into()
363}
364
365fn message_payload(account: &[u8; 32], message: &[u8]) -> Vec<u8> {
366 let mut payload = b"bittensor/hashed/v1/message".to_vec();
367 (account, message).encode_to(&mut payload);
368 payload
369}
370
371pub(super) fn verify_message(
372 crypto_type: u8,
373 account: &[u8; 32],
374 message: &[u8],
375 envelope: &[u8],
376) -> bool {
377 let expected_len = 8 + 34 + 32 + 64;
378 if envelope.len() != expected_len || !envelope.starts_with(MESSAGE_MAGIC) {
379 return false;
380 }
381 let mut input = &envelope[8..];
382 let Ok(descriptor) = Descriptor::decode(&mut input) else {
383 return false;
384 };
385 let scheme = if crypto_type == CRYPTO_HASHED_ED25519 {
386 Scheme::Ed25519
387 } else {
388 Scheme::Sr25519
389 };
390 if !descriptor.is_supported()
391 || descriptor.scheme != scheme
392 || account_id(&descriptor) != *account
393 {
394 return false;
395 }
396 let payload = message_payload(account, message);
397 match scheme {
398 Scheme::Sr25519 => {
399 let (Ok(public), Ok(signature)) = (
400 <[u8; 32]>::try_from(&input[..32]),
401 <[u8; 64]>::try_from(&input[32..]),
402 ) else {
403 return false;
404 };
405 descriptor.initial_commitment == key_commitment(scheme, &public)
406 && sr25519::Pair::verify(
407 &sr25519::Signature::from_raw(signature),
408 &payload,
409 &sr25519::Public::from_raw(public),
410 )
411 }
412 Scheme::Ed25519 => {
413 let (Ok(public), Ok(signature)) = (
414 <[u8; 32]>::try_from(&input[..32]),
415 <[u8; 64]>::try_from(&input[32..]),
416 ) else {
417 return false;
418 };
419 descriptor.initial_commitment == key_commitment(scheme, &public)
420 && ed25519::Pair::verify(
421 &ed25519::Signature::from_raw(signature),
422 &payload,
423 &ed25519::Public::from_raw(public),
424 )
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 #![allow(clippy::unwrap_used, clippy::expect_used)]
432 use super::*;
433 use crate::keys::{CRYPTO_HASHED, DEFAULT_SS58_FORMAT};
434
435 fn transaction_payload(
436 account: &[u8; 32],
437 scheme: Scheme,
438 generation: u64,
439 next: &[u8; 32],
440 public: &[u8; 32],
441 call_and_extensions: &[u8],
442 ) -> Vec<u8> {
443 SignerAddress {
444 version: 1,
445 scheme,
446 account: *account,
447 generation,
448 public_key: *public,
449 next_commitment: *next,
450 }
451 .signing_hash(call_and_extensions)
452 .to_vec()
453 }
454 fn sign_test_transaction(
455 key: &Keypair,
456 call_and_extensions: &[u8],
457 ) -> Result<Vec<u8>, CoreError> {
458 key.sign_hashed(call_and_extensions)
459 }
460
461 #[test]
462 fn composable_schemes_preserve_mode_recovery_and_signatures() {
463 use crate::keys::{account_crypto_type, CRYPTO_ED25519, CRYPTO_SR25519};
464 let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
465 let mut addresses = std::collections::BTreeSet::new();
466 for scheme in [CRYPTO_SR25519, CRYPTO_ED25519] {
467 for mode in ["standard", "hashed"] {
468 let code = account_crypto_type(scheme, Some(mode)).unwrap();
469 let key = Keypair::from_mnemonic(phrase, code, None).unwrap();
470 assert_eq!(key.account_type(), mode);
471 assert!(addresses.insert(key.ss58_address()));
472 let recovered = Keypair::from_seed(key.seed_bytes().unwrap(), code).unwrap();
473 assert_eq!(recovered.public_key_bytes(), key.public_key_bytes());
474 if !super::super::is_hashed_crypto(code) {
475 let signature = key.sign(b"message").unwrap();
476 assert!(recovered.verify(b"message", &signature).unwrap());
477 continue;
478 }
479 let public = key.public_only().unwrap();
480 assert_eq!(public.crypto_type(), code);
481 assert_eq!(
482 key.hashed_descriptor().unwrap(),
483 recovered.hashed_descriptor().unwrap()
484 );
485 assert!(public.sign_hashed(b"implication").is_err());
486 let initial = key.hashed_signing_public_key().unwrap();
487 let later = key.at_generation(42).unwrap();
488 assert_eq!(
489 initial == later.hashed_signing_public_key().unwrap(),
490 mode == "standard"
491 );
492 assert_eq!(
493 later.hashed_current_commitment().unwrap()
494 == later.hashed_next_commitment().unwrap(),
495 mode == "standard"
496 );
497 assert_eq!(later.public_key_bytes(), key.public_key_bytes());
498 let signature = later.sign(b"message").unwrap();
499 assert!(public.verify(b"message", &signature).unwrap());
500 assert!(!public.verify(b"changed", &signature).unwrap());
501 let wrong_mode = account_crypto_type(
502 scheme,
503 Some(if mode == "standard" {
504 "hashed"
505 } else {
506 "standard"
507 }),
508 )
509 .unwrap();
510 assert!(!super::super::verify_with_crypto(
511 wrong_mode,
512 &key.public_key_bytes(),
513 b"message",
514 &signature
515 )
516 .unwrap_or(false));
517 }
518 }
519 }
520
521 #[test]
522 fn hashed_ed_proof_is_scheme_bound_and_retires_message_key() {
523 let key = Keypair::from_seed(&[31; 32], CRYPTO_HASHED_ED25519).unwrap();
524 assert!(key.sign(b"message").is_err());
525 let signer = key.at_generation(8).unwrap();
526 let bytes = sign_test_transaction(&signer, b"implication").unwrap();
527 let proof = Proof::decode(&mut bytes.as_slice()).unwrap();
528 assert_eq!(proof.generation, 8);
529 let payload = transaction_payload(
530 &key.public_key_bytes(),
531 Scheme::Ed25519,
532 8,
533 &proof.next_commitment,
534 &proof.public_key,
535 b"implication",
536 );
537 assert!(ed25519::Pair::verify(
538 &ed25519::Signature::from_raw(proof.signature),
539 payload,
540 &ed25519::Public::from_raw(proof.public_key)
541 ));
542 let wrong = transaction_payload(
543 &key.public_key_bytes(),
544 Scheme::Sr25519,
545 8,
546 &proof.next_commitment,
547 &proof.public_key,
548 b"implication",
549 );
550 assert!(!ed25519::Pair::verify(
551 &ed25519::Signature::from_raw(proof.signature),
552 wrong,
553 &ed25519::Public::from_raw(proof.public_key)
554 ));
555 }
556
557 #[test]
558 fn retired_sr_message_key_never_reveals_current_spending_key() {
559 let key = Keypair::from_seed(&[43; 32], CRYPTO_HASHED).unwrap();
560 assert!(key.sign(b"challenge").is_err());
561 let current = key.at_generation(1).unwrap();
562 let envelope = current.sign(b"challenge").unwrap();
563 assert_eq!(&envelope[42..74], &key.hashed_public_key().unwrap());
564 assert_ne!(&envelope[42..74], ¤t.hashed_public_key().unwrap());
565 assert!(key
566 .public_only()
567 .unwrap()
568 .verify(b"challenge", &envelope)
569 .unwrap());
570 assert!(!key.verify(b"changed challenge", &envelope).unwrap());
571 assert!(!Keypair::from_seed(&[44; 32], CRYPTO_HASHED)
572 .unwrap()
573 .verify(b"challenge", &envelope)
574 .unwrap());
575 }
576
577 const PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
578
579 #[test]
580 fn recovery_derivation_v1_is_pinned() {
581 let key = Keypair::from_seed(&[3; 32], CRYPTO_HASHED).unwrap();
585 assert_eq!(
586 key.ss58_address(),
587 "5Du5D4jstmEn3QPytQLqgowz2huycvq7XvUFpY8u5PJTx6Dd"
588 );
589 assert_eq!(
590 hex::encode(key.hashed_descriptor().unwrap()),
591 "010123c2153ab6fc4d0a3f3cf4b2ffcb1bc0ecc5d6737ecb32addbc1f20fadb3223a"
592 );
593 assert_eq!(
594 hex::encode(key.hashed_public_key().unwrap()),
595 "4220dab7c75d2b0a2088639dc3d314d22f82493cfc86a0efa36e194bc5443612"
596 );
597 assert_eq!(
598 hex::encode(key.at_generation(872).unwrap().hashed_public_key().unwrap()),
599 "18885ee3441c25bd1c0ac0986959fcf33169ec2a6300e7a7a4572010463a7366"
600 );
601 }
602
603 #[test]
604 fn restore_any_generation_from_mnemonic_without_signing_history() {
605 let wallet = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None).unwrap();
606 assert_eq!(
607 wallet.ss58_address(),
608 "5ELvzckdbaShVHktHtSa7gH77AWawQSyfeRLXT1sbvquNjha"
609 );
610 let advanced = wallet.at_generation(872).unwrap();
611 let restored = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None)
612 .unwrap()
613 .at_generation(872)
614 .unwrap();
615 assert_eq!(wallet.public_key_bytes(), advanced.public_key_bytes());
616 assert_eq!(advanced.ss58_address(), restored.ss58_address());
617 assert_eq!(
618 advanced.hashed_public_key().unwrap(),
619 restored.hashed_public_key().unwrap()
620 );
621 assert_eq!(
622 advanced.hashed_next_commitment().unwrap(),
623 restored.hashed_next_commitment().unwrap()
624 );
625 assert_eq!(wallet.hashed_generation().unwrap(), 0);
626 assert_ne!(
627 wallet.hashed_public_key().unwrap(),
628 advanced.hashed_public_key().unwrap()
629 );
630 assert_ne!(
631 wallet.hashed_current_commitment().unwrap(),
632 advanced.hashed_current_commitment().unwrap()
633 );
634 }
635
636 #[test]
637 fn proof_binds_next_generation_and_full_transaction() {
638 let key = Keypair::from_seed(&[9; 32], CRYPTO_HASHED)
639 .unwrap()
640 .at_generation(4)
641 .unwrap();
642 let implication = b"\x01call extensions genesis runtime versions";
643 let encoded = sign_test_transaction(&key, implication).unwrap();
644 assert_eq!(encoded.len(), 136);
645 let proof = Proof::decode(&mut &encoded[..]).unwrap();
646 let payload = transaction_payload(
647 &key.public_key_bytes(),
648 Scheme::Sr25519,
649 4,
650 &proof.next_commitment,
651 &proof.public_key,
652 implication,
653 );
654 assert!(sr25519::Pair::verify(
655 &sr25519::Signature::from_raw(proof.signature),
656 payload,
657 &sr25519::Public::from_raw(proof.public_key)
658 ));
659 for altered in [
660 transaction_payload(
661 &key.public_key_bytes(),
662 Scheme::Sr25519,
663 5,
664 &proof.next_commitment,
665 &proof.public_key,
666 implication,
667 ),
668 transaction_payload(
669 &key.public_key_bytes(),
670 Scheme::Sr25519,
671 4,
672 &[0; 32],
673 &proof.public_key,
674 implication,
675 ),
676 transaction_payload(
677 &[0; 32],
678 Scheme::Sr25519,
679 4,
680 &proof.next_commitment,
681 &proof.public_key,
682 implication,
683 ),
684 transaction_payload(
685 &key.public_key_bytes(),
686 Scheme::Sr25519,
687 4,
688 &proof.next_commitment,
689 &proof.public_key,
690 b"different call",
691 ),
692 ] {
693 assert!(!sr25519::Pair::verify(
694 &sr25519::Signature::from_raw(proof.signature),
695 altered,
696 &sr25519::Public::from_raw(proof.public_key)
697 ));
698 }
699 }
700
701 #[test]
702 fn restored_current_key_verifies_but_old_and_sibling_keys_do_not() {
703 let original = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None).unwrap();
704 let previous = original.at_generation(40).unwrap();
705 let current = original.at_generation(41).unwrap();
706 let recovered = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None)
707 .unwrap()
708 .at_generation(41)
709 .unwrap();
710 let implication = b"\x01restored device transaction";
711 let proof =
712 Proof::decode(&mut &sign_test_transaction(&recovered, implication).unwrap()[..])
713 .unwrap();
714 let payload = transaction_payload(
715 ¤t.public_key_bytes(),
716 Scheme::Sr25519,
717 proof.generation,
718 &proof.next_commitment,
719 &proof.public_key,
720 implication,
721 );
722 assert_eq!(proof.generation, 41);
723 assert_eq!(
724 previous.hashed_next_commitment().unwrap(),
725 current.hashed_current_commitment().unwrap()
726 );
727 assert_eq!(
728 proof.next_commitment,
729 original.hashed_commitment(42).unwrap()
730 );
731 let signature = sr25519::Signature::from_raw(proof.signature);
732 assert!(sr25519::Pair::verify(
733 &signature,
734 payload,
735 &sr25519::Public::from_raw(current.hashed_public_key().unwrap())
736 ));
737 let sibling = Keypair::from_seed(&[19; 32], CRYPTO_HASHED)
738 .unwrap()
739 .at_generation(41)
740 .unwrap();
741 for other in [previous, sibling] {
742 assert_ne!(
743 key_commitment(Scheme::Sr25519, &other.hashed_public_key().unwrap()),
744 current.hashed_current_commitment().unwrap()
745 );
746 let other_proof =
747 Proof::decode(&mut &sign_test_transaction(&other, implication).unwrap()[..])
748 .unwrap();
749 let other_signature = sr25519::Signature::from_raw(other_proof.signature);
750 let other_payload = transaction_payload(
753 &other.public_key_bytes(),
754 Scheme::Sr25519,
755 other_proof.generation,
756 &other_proof.next_commitment,
757 &other_proof.public_key,
758 implication,
759 );
760 assert!(sr25519::Pair::verify(
761 &other_signature,
762 other_payload,
763 &sr25519::Public::from_raw(other_proof.public_key)
764 ));
765 let expected_payload = transaction_payload(
766 ¤t.public_key_bytes(),
767 Scheme::Sr25519,
768 41,
769 &other_proof.next_commitment,
770 &other_proof.public_key,
771 implication,
772 );
773 assert!(!sr25519::Pair::verify(
774 &other_signature,
775 expected_payload,
776 &sr25519::Public::from_raw(current.hashed_public_key().unwrap())
777 ));
778 }
779 assert_eq!(recovered.hashed_generation().unwrap(), 41);
781 let retry =
782 Proof::decode(&mut &recovered.sign_hashed(b"replacement transaction").unwrap()[..])
783 .unwrap();
784 assert_eq!(retry.generation, 41);
785 assert_eq!(retry.public_key, proof.public_key);
786 assert_eq!(retry.next_commitment, proof.next_commitment);
787 }
788
789 #[test]
790 fn recovery_requires_the_same_mnemonic_and_derivation_passphrase() {
791 const OTHER_PHRASE: &str =
792 "legal winner thank year wave sausage worth useful legal winner thank yellow";
793 let original = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, Some("correct passphrase"))
794 .unwrap()
795 .at_generation(73)
796 .unwrap();
797 let recovered = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, Some("correct passphrase"))
798 .unwrap()
799 .at_generation(73)
800 .unwrap();
801 assert_eq!(
802 recovered.hashed_descriptor().unwrap(),
803 original.hashed_descriptor().unwrap()
804 );
805 assert_eq!(
806 recovered.hashed_current_commitment().unwrap(),
807 original.hashed_current_commitment().unwrap()
808 );
809 for incorrect in [
810 Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None).unwrap(),
811 Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, Some("wrong passphrase")).unwrap(),
812 Keypair::from_mnemonic(OTHER_PHRASE, CRYPTO_HASHED, Some("correct passphrase"))
813 .unwrap(),
814 ] {
815 assert_ne!(incorrect.ss58_address(), original.ss58_address());
818 assert_ne!(
819 incorrect.hashed_commitment(73).unwrap(),
820 original.hashed_current_commitment().unwrap()
821 );
822 }
823 assert!(Keypair::from_mnemonic("not a valid mnemonic", CRYPTO_HASHED, None).is_err());
824 }
825
826 #[test]
827 fn final_usable_generation_commits_terminal_key_without_wrapping() {
828 let wallet = Keypair::from_seed(&[23; 32], CRYPTO_HASHED).unwrap();
829 let last = wallet.at_generation(u64::MAX - 1).unwrap();
830 let terminal = wallet.at_generation(u64::MAX).unwrap();
831 let proof =
832 Proof::decode(&mut &last.sign_hashed(b"last authorization").unwrap()[..]).unwrap();
833 assert_eq!(proof.generation, u64::MAX - 1);
834 assert_eq!(
835 proof.next_commitment,
836 terminal.hashed_current_commitment().unwrap()
837 );
838 assert_ne!(
839 proof.next_commitment,
840 wallet.hashed_current_commitment().unwrap()
841 );
842 assert!(terminal.hashed_next_commitment().is_err());
843 assert!(terminal
844 .sign_hashed(b"must never wrap to generation zero")
845 .is_err());
846 assert_eq!(wallet.hashed_generation().unwrap(), 0);
847 }
848
849 #[test]
850 fn public_descriptor_preserves_identity_without_exposing_signer() {
851 let key = Keypair::from_seed(&[3; 32], CRYPTO_HASHED).unwrap();
852 let public = key.public_only().unwrap();
853 assert_eq!(key.public_key_bytes(), public.public_key_bytes());
854 assert_eq!(
855 key.hashed_descriptor().unwrap(),
856 public.hashed_descriptor().unwrap()
857 );
858 assert_eq!(
859 key.hashed_current_commitment().unwrap(),
860 public.hashed_current_commitment().unwrap()
861 );
862 assert!(public.private_key_bytes().is_none());
863 assert!(public.hashed_public_key().is_err());
864 assert!(public.sign_hashed(b"message").is_err());
865 assert!(key.sign(b"message").is_err());
866 }
867
868 #[test]
869 fn descriptors_fail_closed_and_derivation_is_separated() {
870 let key = Keypair::from_seed(&[3; 32], CRYPTO_HASHED).unwrap();
871 let raw = Keypair::from_seed(&[3; 32], super::super::CRYPTO_SR25519).unwrap();
872 assert_ne!(key.hashed_public_key().unwrap(), raw.public_key_bytes());
873 for offset in [0, 1] {
874 let mut descriptor = key.hashed_descriptor().unwrap();
875 descriptor[offset] = 255;
876 assert!(Keypair::from_hashed_descriptor(&descriptor, DEFAULT_SS58_FORMAT).is_err());
877 }
878 let mut trailing = key.hashed_descriptor().unwrap();
879 trailing.push(0);
880 assert!(Keypair::from_hashed_descriptor(&trailing, DEFAULT_SS58_FORMAT).is_err());
881 assert!(Keypair::from_uri("//Alice", CRYPTO_HASHED).is_err());
882 }
883
884 #[test]
885 fn hashed_ed25519_signing_key_has_a_wiping_owner_and_keeps_vectors() {
886 fn owner(secret: Zeroizing<SigningKey>) -> Zeroizing<SigningKey> {
888 secret
889 }
890 let secret = owner(HashedKeypair::derive_ed(&[31; 32], 8).unwrap());
891 assert_eq!(
895 hex::encode(ed25519_public(&secret)),
896 "8b9c4ae043290453eb92a40d48f5074178031bc6623e60544ae4a2f5ab6542a1"
897 );
898 let key = Keypair::from_seed(&[31; 32], CRYPTO_HASHED_ED25519).unwrap();
899 assert_eq!(
900 key.ss58_address(),
901 "5EPuttWmgUjN8Wq2qKELKso6WrfRpBpgZpuNRzAwyAgQTXzo"
902 );
903 assert_eq!(
904 hex::encode(key.hashed_signing_public_key().unwrap()),
905 "87ba9efaab5ed270bf9fa5e8a4101283cd6ba1541f9166177778f1ca47aa88c7"
906 );
907 let signer = key.at_generation(8).unwrap();
908 assert_eq!(
909 hex::encode(signer.sign_hashed(b"implication").unwrap()),
910 "08000000000000008b9c4ae043290453eb92a40d48f5074178031bc6623e60544ae4a2f5ab6542a17d11decb0793d6c942bd149f0111f6e89a809b9438a355647213877069580c7952715702d31e438a3af305cf648a1ca1d376a5e371ac711d0faa883810639d3df553bbdf37044ad2b656d94d56011574b6673aab2222d22f9750780117a54f07"
911 );
912 assert_eq!(
913 hex::encode(key.at_generation(1).unwrap().sign(b"challenge").unwrap()),
914 "4254484d534730310103d49382c67690fe7a07e595daedccb680629a6102d4c8c6986a7ea0fae84548d787ba9efaab5ed270bf9fa5e8a4101283cd6ba1541f9166177778f1ca47aa88c7b0c5ac55ee389545d3086578a4f4d91119b9080d6f36e239c8bc6e8f3bc49825212c1e8f06de0a4c9386c109ca233d0b7b6dd5c82858fe3beef01ea954503404"
915 );
916 assert!(HashedKeypair::from_seed(&[31; 31], CRYPTO_HASHED_ED25519).is_err());
917 }
918}