Skip to main content

bittensor_core/keys/
mod.rs

1//! Substrate `sp-core` key primitives (absorbed from `py-sp-core`).
2//!
3//! Keypair derivation, signing, verification, sealed-box encryption, and
4//! SS58 codec — built against the same `sp-core` revision as the runtime, so
5//! client crypto can never drift from the chain's.
6
7// Client-side code: slicing and arithmetic on locally validated buffers is
8// the norm here, and this crate never runs inside the runtime.
9#![allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
10
11use ed25519_zebra::{SigningKey, VerificationKey};
12#[cfg(feature = "host")]
13use sodiumoxide::crypto::box_;
14#[cfg(feature = "host")]
15use sodiumoxide::crypto::sealedbox;
16#[cfg(feature = "host")]
17use sodiumoxide::crypto::sign::ed25519 as sign_ed25519;
18use sp_core::crypto::{
19    AccountId32, CryptoType, DeriveError, DeriveJunction, Pair as PairT, SecretStringError,
20    Ss58Codec,
21};
22use sp_core::{ed25519, sr25519, ByteArray};
23use zeroize::Zeroizing;
24
25use crate::error::CoreError;
26
27mod base58;
28mod hashed;
29pub use hashed::{verify_hashed_authorization, HashedKeypair};
30mod falcon;
31mod mldsa;
32pub use mldsa::{verify_mldsa_authorization, MldsaKeypair, ROLE_TRANSACTION};
33mod receiving;
34pub use receiving::{
35    decode_hashed_receiving_address, decode_mldsa_receiving_address,
36    encode_hashed_receiving_address, encode_mldsa_receiving_address,
37};
38#[cfg(feature = "host")]
39mod encrypted_json;
40
41/// Crypto type codes, matching the py-substrate-interface / btwallet convention.
42pub const CRYPTO_ED25519: u8 = 0;
43pub const CRYPTO_SR25519: u8 = 1;
44pub const CRYPTO_ECDSA: u8 = 2;
45/// Length of a compressed secp256k1 public key.
46pub const ECDSA_PUBLIC_KEY_LEN: usize = sp_core::ecdsa::PUBLIC_KEY_SERIALIZED_SIZE;
47/// Raw signature lengths per crypto type (without a MultiSignature variant byte).
48pub const ED25519_SIGNATURE_LEN: usize = ed25519::SIGNATURE_SERIALIZED_SIZE;
49pub const SR25519_SIGNATURE_LEN: usize = sr25519::SIGNATURE_SERIALIZED_SIZE;
50pub const ECDSA_SIGNATURE_LEN: usize = sp_core::ecdsa::SIGNATURE_SERIALIZED_SIZE;
51/// Versioned rotating commitment wrapper; initially wraps sr25519.
52pub const CRYPTO_HASHED: u8 = 4;
53pub const CRYPTO_HASHED_ED25519: u8 = 6;
54/// SDK identifier; distinct from the ML-DSA wire algorithm value 4.
55pub const CRYPTO_MLDSA65: u8 = 7;
56/// Fixed-key ML-DSA-65. Preserve code 7 and all original rotating identities.
57pub const CRYPTO_MLDSA65_FIXED: u8 = 8;
58
59pub const CRYPTO_FALCON512: u8 = 9;
60pub const CRYPTO_FALCON512_HASHED: u8 = 10;
61pub const CRYPTO_FALCON1024: u8 = 11;
62pub const CRYPTO_FALCON1024_HASHED: u8 = 12;
63
64pub fn is_falcon_crypto(crypto_type: u8) -> bool {
65    matches!(
66        crypto_type,
67        CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED | CRYPTO_FALCON1024 | CRYPTO_FALCON1024_HASHED
68    )
69}
70
71pub fn is_pq_crypto(crypto_type: u8) -> bool {
72    is_mldsa_crypto(crypto_type) || is_falcon_crypto(crypto_type)
73}
74
75pub fn is_fixed_pq_crypto(crypto_type: u8) -> bool {
76    matches!(
77        crypto_type,
78        CRYPTO_MLDSA65_FIXED | CRYPTO_FALCON512 | CRYPTO_FALCON1024
79    )
80}
81
82pub fn pq_profile(crypto_type: u8) -> Option<u8> {
83    match crypto_type {
84        CRYPTO_MLDSA65 => Some(4),
85        CRYPTO_MLDSA65_FIXED => Some(5),
86        CRYPTO_FALCON512_HASHED => Some(6),
87        CRYPTO_FALCON512 => Some(7),
88        CRYPTO_FALCON1024_HASHED => Some(8),
89        CRYPTO_FALCON1024 => Some(9),
90        _ => None,
91    }
92}
93
94pub fn is_mldsa_crypto(crypto_type: u8) -> bool {
95    matches!(crypto_type, CRYPTO_MLDSA65 | CRYPTO_MLDSA65_FIXED)
96}
97
98pub fn is_hashed_crypto(crypto_type: u8) -> bool {
99    matches!(crypto_type, CRYPTO_HASHED | CRYPTO_HASHED_ED25519)
100}
101
102pub fn is_rotating_crypto(crypto_type: u8) -> bool {
103    is_hashed_crypto(crypto_type)
104        || matches!(
105            crypto_type,
106            CRYPTO_MLDSA65 | CRYPTO_FALCON512_HASHED | CRYPTO_FALCON1024_HASHED
107        )
108}
109
110/// Compose account mode with a signing scheme; omitted mode preserves legacy codes.
111pub fn account_crypto_type(crypto_type: u8, mode: Option<&str>) -> Result<u8, CoreError> {
112    if is_falcon_crypto(crypto_type) {
113        let (fixed, hashed) = if matches!(crypto_type, CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED) {
114            (CRYPTO_FALCON512, CRYPTO_FALCON512_HASHED)
115        } else {
116            (CRYPTO_FALCON1024, CRYPTO_FALCON1024_HASHED)
117        };
118        return match mode {
119            None => Ok(crypto_type),
120            Some("hashed") => Ok(hashed),
121            Some("standard") => Ok(fixed),
122            _ => Err(crypto_err("account type must be standard or hashed")),
123        };
124    }
125    if is_mldsa_crypto(crypto_type) {
126        return match mode {
127            None => Ok(crypto_type),
128            Some("hashed") => Ok(CRYPTO_MLDSA65),
129            Some("standard") => Ok(CRYPTO_MLDSA65_FIXED),
130            _ => Err(crypto_err("account type must be standard or hashed")),
131        };
132    }
133    let Some(mode) = mode else {
134        return match crypto_type {
135            CRYPTO_SR25519 | CRYPTO_ED25519 | CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
136                Ok(crypto_type)
137            }
138            _ => Err(crypto_err("unsupported signing scheme")),
139        };
140    };
141    let scheme = match crypto_type {
142        CRYPTO_SR25519 | CRYPTO_HASHED => CRYPTO_SR25519,
143        CRYPTO_ED25519 | CRYPTO_HASHED_ED25519 => CRYPTO_ED25519,
144        _ => return Err(crypto_err("unsupported signing scheme")),
145    };
146    match (mode, scheme) {
147        ("standard", CRYPTO_SR25519) => Ok(CRYPTO_SR25519),
148        ("standard", CRYPTO_ED25519) => Ok(CRYPTO_ED25519),
149        ("hashed", CRYPTO_SR25519) => Ok(CRYPTO_HASHED),
150        ("hashed", CRYPTO_ED25519) => Ok(CRYPTO_HASHED_ED25519),
151        _ => Err(crypto_err("account type must be standard or hashed")),
152    }
153}
154
155pub const DEFAULT_SS58_FORMAT: u16 = 42;
156
157fn crypto_err(msg: impl Into<String>) -> CoreError {
158    CoreError::Crypto(msg.into())
159}
160
161#[cfg(feature = "host")]
162pub(crate) fn ensure_sodium() -> Result<(), CoreError> {
163    sodiumoxide::init().map_err(|_| crypto_err("failed to initialize libsodium"))?;
164    Ok(())
165}
166
167// sp-core's CryptoBytes has several AsRef impls; pin down the byte view.
168fn as_bytes<T: AsRef<[u8]>>(value: &T) -> Vec<u8> {
169    value.as_ref().to_vec()
170}
171
172/// Decode the 32-byte account identity. Hashed identities are commitments, not
173/// signing public keys; the historical function name remains for compatibility.
174pub fn public_key_from_ss58(ss58_address: &str) -> Result<[u8; 32], CoreError> {
175    let account = AccountId32::from_ss58check(ss58_address)
176        .map_err(|e| crypto_err(format!("invalid ss58 address: {e:?}")))?;
177    Ok(account.into())
178}
179
180/// ss58 rendering, byte-identical to sp-core's `to_ss58check_with_version`
181/// but ~6x faster (see `base58.rs`); every decoded account id goes through
182/// here, thousands per storage-map page / metagraph payload.
183pub fn ss58_from_public(public_key: [u8; 32], ss58_format: u16) -> String {
184    // "SS58PRE" ++ prefix (1-2 bytes) ++ key (32) ++ checksum (2)
185    let mut buf = [0u8; 7 + 2 + 32 + 2];
186    buf[..7].copy_from_slice(b"SS58PRE");
187    let ident = ss58_format & 0b0011_1111_1111_1111;
188    let prefix_len = if ident < 64 {
189        buf[7] = ident as u8;
190        1
191    } else {
192        // Two-byte prefix encoding per the ss58 registry spec.
193        buf[7] = (((ident & 0b0000_0000_1111_1100) >> 2) as u8) | 0b0100_0000;
194        buf[8] = ((ident >> 8) as u8) | (((ident & 0b11) as u8) << 6);
195        2
196    };
197    buf[7 + prefix_len..7 + prefix_len + 32].copy_from_slice(&public_key);
198    let body_end = 7 + prefix_len + 32;
199    let checksum = sp_core::hashing::blake2_512(&buf[..body_end]);
200    buf[body_end] = checksum[0];
201    buf[body_end + 1] = checksum[1];
202    base58::base58_encode(&buf[7..body_end + 2])
203}
204
205/// Account id of a secp256k1 public key: `blake2_256(compressed_pubkey)`,
206/// the `MultiSigner::Ecdsa` -> `AccountId32` mapping the chain uses.
207pub fn account_id_from_ecdsa_public(public_key: &[u8]) -> Result<[u8; 32], CoreError> {
208    let compressed = <[u8; ECDSA_PUBLIC_KEY_LEN]>::try_from(public_key).map_err(|_| {
209        crypto_err(format!(
210            "ecdsa public key must be {ECDSA_PUBLIC_KEY_LEN} bytes (compressed secp256k1)"
211        ))
212    })?;
213    if !matches!(compressed[0], 0x02 | 0x03) {
214        return Err(crypto_err(
215            "ecdsa public key must be compressed (0x02/0x03 prefix)",
216        ));
217    }
218    Ok(sp_core::hashing::blake2_256(&compressed))
219}
220
221/// SS58 address of a compressed secp256k1 public key.
222pub fn ss58_from_ecdsa_public(public_key: &[u8], ss58_format: u16) -> Result<String, CoreError> {
223    Ok(ss58_from_public(
224        account_id_from_ecdsa_public(public_key)?,
225        ss58_format,
226    ))
227}
228
229fn verify_with_crypto(
230    crypto_type: u8,
231    public_key: &[u8; 32],
232    message: &[u8],
233    signature: &[u8],
234) -> Result<bool, CoreError> {
235    match crypto_type {
236        CRYPTO_SR25519 => {
237            let public = sr25519::Public::from_raw(*public_key);
238            let sig = sr25519::Signature::from_slice(signature)
239                .map_err(|_| crypto_err("invalid sr25519 signature length"))?;
240            Ok(sr25519::Pair::verify(&sig, message, &public))
241        }
242        CRYPTO_ED25519 => {
243            let public = ed25519::Public::from_raw(*public_key);
244            let sig = ed25519::Signature::from_slice(signature)
245                .map_err(|_| crypto_err("invalid ed25519 signature length"))?;
246            Ok(ed25519::Pair::verify(&sig, message, &public))
247        }
248        CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => Ok(hashed::verify_message(
249            crypto_type,
250            public_key,
251            message,
252            signature,
253        )),
254        other => Err(crypto_err(format!("unknown crypto type {other}"))),
255    }
256}
257
258#[cfg(feature = "host")]
259fn ed25519_to_x25519_pk(public_key: &[u8; 32]) -> Result<box_::PublicKey, CoreError> {
260    ensure_sodium()?;
261    let ed25519_pk = sign_ed25519::PublicKey::from_slice(public_key)
262        .ok_or_else(|| crypto_err("invalid ed25519 public key"))?;
263    sign_ed25519::to_curve25519_pk(&ed25519_pk)
264        .map_err(|_| crypto_err("failed to convert ed25519 public key to x25519"))
265}
266
267fn ed25519_seed(pair: &Zeroizing<SigningKey>) -> &[u8] {
268    let pair: &SigningKey = pair;
269    pair.as_ref()
270}
271
272fn ed25519_public(pair: &Zeroizing<SigningKey>) -> [u8; 32] {
273    let pair: &SigningKey = pair;
274    VerificationKey::from(pair).into()
275}
276
277#[cfg(feature = "host")]
278fn ed25519_x25519_from_pair(
279    pair: &Zeroizing<SigningKey>,
280) -> Result<(box_::PublicKey, box_::SecretKey), CoreError> {
281    ensure_sodium()?;
282    let seed = sign_ed25519::Seed::from_slice(ed25519_seed(pair))
283        .ok_or_else(|| crypto_err("failed to derive x25519 keypair for decryption"))?;
284    let (pk, sk) = sign_ed25519::keypair_from_seed(&seed);
285    let x25519_pk = sign_ed25519::to_curve25519_pk(&pk)
286        .map_err(|_| crypto_err("failed to derive x25519 keypair for decryption"))?;
287    let x25519_sk = sign_ed25519::to_curve25519_sk(&sk)
288        .map_err(|_| crypto_err("failed to derive x25519 keypair for decryption"))?;
289    Ok((x25519_pk, x25519_sk))
290}
291
292struct WipingSeed(Zeroizing<[u8; 32]>);
293
294impl Default for WipingSeed {
295    fn default() -> Self {
296        Self(Zeroizing::new([0; 32]))
297    }
298}
299
300impl Clone for WipingSeed {
301    fn clone(&self) -> Self {
302        let mut clone = Self::default();
303        clone.as_mut().copy_from_slice(self.as_ref());
304        clone
305    }
306}
307
308impl AsRef<[u8]> for WipingSeed {
309    fn as_ref(&self) -> &[u8] {
310        &self.0[..]
311    }
312}
313
314impl AsMut<[u8]> for WipingSeed {
315    fn as_mut(&mut self) -> &mut [u8] {
316        &mut self.0[..]
317    }
318}
319
320const HDKD_PREIMAGE_LEN: usize = 1 + 11 + 32 + 32;
321
322/// Internal Ed25519 derivation owner. Both its seed and expanded signing key
323/// erase themselves on every return, error, panic, and final drop path.
324struct WipingEd25519Pair {
325    secret: Zeroizing<SigningKey>,
326}
327
328impl WipingEd25519Pair {
329    fn into_secret(self) -> Zeroizing<SigningKey> {
330        self.secret
331    }
332
333    fn seed(&self) -> WipingSeed {
334        let mut seed = WipingSeed::default();
335        seed.as_mut().copy_from_slice(ed25519_seed(&self.secret));
336        seed
337    }
338}
339
340impl CryptoType for WipingEd25519Pair {
341    type Pair = Self;
342}
343
344impl PairT for WipingEd25519Pair {
345    type Public = ed25519::Public;
346    type Seed = WipingSeed;
347    type Signature = ed25519::Signature;
348
349    fn from_seed_slice(seed: &[u8]) -> Result<Self, SecretStringError> {
350        let secret =
351            SigningKey::try_from(seed).map_err(|_| SecretStringError::InvalidSeedLength)?;
352        Ok(Self {
353            secret: Zeroizing::new(secret),
354        })
355    }
356
357    fn derive<Iter: Iterator<Item = DeriveJunction>>(
358        &self,
359        path: Iter,
360        _seed: Option<Self::Seed>,
361    ) -> Result<(Self, Option<Self::Seed>), DeriveError> {
362        let mut seed = self.seed();
363        for junction in path {
364            match junction {
365                DeriveJunction::Soft(_) => return Err(DeriveError::SoftKeyInPath),
366                DeriveJunction::Hard(chain_code) => {
367                    // The SCALE encoding of ("Ed25519HDKD", seed, chain_code):
368                    // compact length 11 (0x2c), the label, then both arrays.
369                    // A fixed wiping buffer replaces `using_encoded`, whose
370                    // temporary heap Vec would hold the parent seed unwiped.
371                    let mut preimage = Zeroizing::new([0u8; HDKD_PREIMAGE_LEN]);
372                    preimage[0] = 0x2c;
373                    preimage[1..12].copy_from_slice(b"Ed25519HDKD");
374                    preimage[12..44].copy_from_slice(&seed.0[..]);
375                    preimage[44..].copy_from_slice(&chain_code);
376                    let derived = Zeroizing::new(sp_core::hashing::blake2_256(&preimage[..]));
377                    seed.0.copy_from_slice(&derived[..]);
378                }
379            }
380        }
381        Ok((Self::from_seed(&seed), Some(seed)))
382    }
383
384    fn public(&self) -> Self::Public {
385        ed25519::Public::from_raw(ed25519_public(&self.secret))
386    }
387
388    fn sign(&self, message: &[u8]) -> Self::Signature {
389        Self::Signature::from_raw(self.secret.sign(message).to_bytes())
390    }
391
392    fn verify<M: AsRef<[u8]>>(
393        signature: &Self::Signature,
394        message: M,
395        public: &Self::Public,
396    ) -> bool {
397        ed25519::Pair::verify(signature, message, public)
398    }
399
400    fn to_raw_vec(&self) -> Vec<u8> {
401        ed25519_seed(&self.secret).to_vec()
402    }
403}
404
405// Each Keypair holds exactly one variant; the size skew between ed25519 and
406// public-only is irrelevant here.
407#[allow(clippy::large_enum_variant)]
408pub enum KeypairInner {
409    Ed25519(Zeroizing<SigningKey>),
410    Sr25519(sr25519::Pair),
411    Hashed(HashedKeypair),
412    Mldsa(MldsaKeypair),
413    PublicOnly {
414        public_key: [u8; 32],
415        crypto_type: u8,
416    },
417}
418
419impl KeypairInner {
420    fn private_key_bytes(&self) -> Option<Vec<u8>> {
421        match self {
422            KeypairInner::Ed25519(pair) => Some(ed25519_seed(pair).to_vec()),
423            KeypairInner::Sr25519(pair) => Some(pair.to_raw_vec()),
424            KeypairInner::Hashed(pair) => pair.master_seed().map(|seed| seed.to_vec()),
425            KeypairInner::Mldsa(pair) => pair.master_seed().map(|seed| seed.to_vec()),
426            KeypairInner::PublicOnly { .. } => None,
427        }
428    }
429}
430
431/// A native keypair or a versioned hashed account backed by workspace crypto.
432pub struct Keypair {
433    inner: KeypairInner,
434    ss58_format: u16,
435    /// The BIP39 phrase this keypair was derived from, when known and
436    /// sufficient on its own to re-derive the key (no derivation password).
437    /// Written to keyfiles as ``secretPhrase`` for parity with the legacy
438    /// btwallet format that third-party tools parse.
439    mnemonic: Option<Zeroizing<String>>,
440    /// The raw seed this keypair was derived from, when known. Written to
441    /// keyfiles as ``secretSeed`` (legacy-format parity).
442    seed: Option<Zeroizing<Vec<u8>>>,
443}
444
445impl Keypair {
446    pub fn account_type(&self) -> &'static str {
447        if is_rotating_crypto(self.crypto_type()) {
448            "hashed"
449        } else {
450            "standard"
451        }
452    }
453
454    pub fn signing_scheme(&self) -> &'static str {
455        match self.crypto_type() {
456            CRYPTO_HASHED | CRYPTO_SR25519 => "sr25519",
457            CRYPTO_HASHED_ED25519 | CRYPTO_ED25519 => "ed25519",
458            CRYPTO_MLDSA65 | CRYPTO_MLDSA65_FIXED => "mldsa65",
459            CRYPTO_FALCON512 | CRYPTO_FALCON512_HASHED => "falcon512",
460            CRYPTO_FALCON1024 | CRYPTO_FALCON1024_HASHED => "falcon1024",
461            _ => "unknown",
462        }
463    }
464    /// Public-only or full keypair from SS58 address and/or raw public key bytes.
465    pub fn new(
466        ss58_address: Option<&str>,
467        public_key: Option<&[u8]>,
468        crypto_type: u8,
469        ss58_format: u16,
470    ) -> Result<Self, CoreError> {
471        match crypto_type {
472            CRYPTO_SR25519
473            | CRYPTO_ED25519
474            | CRYPTO_HASHED
475            | CRYPTO_HASHED_ED25519
476            | CRYPTO_MLDSA65
477            | CRYPTO_MLDSA65_FIXED
478            | CRYPTO_FALCON512
479            | CRYPTO_FALCON512_HASHED
480            | CRYPTO_FALCON1024
481            | CRYPTO_FALCON1024_HASHED => {}
482            other => return Err(crypto_err(format!("unknown crypto type {other}"))),
483        }
484
485        let public_key = match (ss58_address, public_key) {
486            (Some(addr), None) => public_key_from_ss58(addr)?,
487            (None, Some(pk)) => <[u8; 32]>::try_from(pk)
488                .map_err(|_| crypto_err("public key must be exactly 32 bytes"))?,
489            (Some(addr), Some(pk)) => {
490                let from_addr = public_key_from_ss58(addr)?;
491                let from_pk = <[u8; 32]>::try_from(pk)
492                    .map_err(|_| crypto_err("public key must be exactly 32 bytes"))?;
493                if from_addr != from_pk {
494                    return Err(crypto_err(
495                        "ss58 address and public key refer to different accounts",
496                    ));
497                }
498                from_pk
499            }
500            (None, None) => {
501                return Err(crypto_err(
502                    "no ss58 formatted address or public key provided",
503                ));
504            }
505        };
506
507        Ok(Self {
508            inner: KeypairInner::PublicOnly {
509                public_key,
510                crypto_type,
511            },
512            ss58_format,
513            mnemonic: None,
514            seed: None,
515        })
516    }
517
518    /// Derive a keypair from a BIP39 mnemonic (with optional password).
519    pub fn from_mnemonic(
520        mnemonic: &str,
521        crypto_type: u8,
522        password: Option<&str>,
523    ) -> Result<Self, CoreError> {
524        let (inner, seed) = match crypto_type {
525            crypto if is_pq_crypto(crypto) => {
526                let phrase = bip39::Mnemonic::parse_in(bip39::Language::English, mnemonic)
527                    .map_err(|_| crypto_err("invalid post-quantum mnemonic"))?;
528                if phrase.word_count() != 24 {
529                    return Err(crypto_err(
530                        "post-quantum wallets require a 24-word mnemonic with 256-bit entropy",
531                    ));
532                }
533                let expanded = Zeroizing::new(phrase.to_seed(password.unwrap_or("")));
534                let seed = Zeroizing::new(expanded[..32].to_vec());
535                (
536                    KeypairInner::Mldsa(MldsaKeypair::from_seed(&seed, crypto_type)?),
537                    seed,
538                )
539            }
540            CRYPTO_SR25519 => {
541                let (pair, seed) = sr25519::Pair::from_phrase(mnemonic, password)
542                    .map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
543                (KeypairInner::Sr25519(pair), Zeroizing::new(seed.to_vec()))
544            }
545            CRYPTO_ED25519 => {
546                let (pair, seed) = WipingEd25519Pair::from_phrase(mnemonic, password)
547                    .map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
548                (
549                    KeypairInner::Ed25519(pair.into_secret()),
550                    Zeroizing::new(seed.as_ref().to_vec()),
551                )
552            }
553            CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
554                let (_, seed) = sr25519::Pair::from_phrase(mnemonic, password)
555                    .map_err(|e| crypto_err(format!("invalid mnemonic: {e:?}")))?;
556                (
557                    KeypairInner::Hashed(HashedKeypair::from_seed(&seed, crypto_type)?),
558                    Zeroizing::new(seed.to_vec()),
559                )
560            }
561            other => return Err(crypto_err(format!("unknown crypto type {other}"))),
562        };
563        Ok(Self {
564            inner,
565            ss58_format: DEFAULT_SS58_FORMAT,
566            // A phrase with a derivation password cannot re-derive the key on
567            // its own, so only a passwordless phrase is kept (and written to
568            // keyfiles); the derived seed is always sufficient.
569            mnemonic: password
570                .is_none()
571                .then(|| Zeroizing::new(mnemonic.to_string())),
572            seed: Some(seed),
573        })
574    }
575
576    /// Derive a keypair from a 32-byte seed.
577    pub fn from_seed(seed: &[u8], crypto_type: u8) -> Result<Self, CoreError> {
578        let inner = match crypto_type {
579            crypto if is_pq_crypto(crypto) => {
580                KeypairInner::Mldsa(MldsaKeypair::from_seed(seed, crypto_type)?)
581            }
582            CRYPTO_SR25519 => KeypairInner::Sr25519(
583                sr25519::Pair::from_seed_slice(seed)
584                    .map_err(|e| crypto_err(format!("invalid seed: {e:?}")))?,
585            ),
586            CRYPTO_ED25519 => KeypairInner::Ed25519(
587                WipingEd25519Pair::from_seed_slice(seed)
588                    .map_err(|e| crypto_err(format!("invalid seed: {e:?}")))?
589                    .into_secret(),
590            ),
591            CRYPTO_HASHED | CRYPTO_HASHED_ED25519 => {
592                KeypairInner::Hashed(HashedKeypair::from_seed(seed, crypto_type)?)
593            }
594            other => return Err(crypto_err(format!("unknown crypto type {other}"))),
595        };
596        Ok(Self {
597            inner,
598            ss58_format: DEFAULT_SS58_FORMAT,
599            mnemonic: None,
600            seed: Some(Zeroizing::new(seed.to_vec())),
601        })
602    }
603
604    /// Derive a keypair from a secret URI (e.g. "//Alice" or "<mnemonic>//hard/soft").
605    pub fn from_uri(uri: &str, crypto_type: u8) -> Result<Self, CoreError> {
606        if is_hashed_crypto(crypto_type) || is_pq_crypto(crypto_type) {
607            return Err(crypto_err(
608                "hashed and post-quantum wallets require a mnemonic or a 32-byte master seed; secret URIs are unsupported",
609            ));
610        }
611        let inner = match crypto_type {
612            CRYPTO_SR25519 => KeypairInner::Sr25519(
613                sr25519::Pair::from_string(uri, None)
614                    .map_err(|e| crypto_err(format!("invalid secret uri: {e:?}")))?,
615            ),
616            CRYPTO_ED25519 => KeypairInner::Ed25519(
617                WipingEd25519Pair::from_string(uri, None)
618                    .map_err(|e| crypto_err(format!("invalid secret uri: {e:?}")))?
619                    .into_secret(),
620            ),
621            other => return Err(crypto_err(format!("unknown crypto type {other}"))),
622        };
623        // A URI may carry derivation junctions, so neither the phrase nor a
624        // bare seed is retained.
625        Ok(Self {
626            inner,
627            ss58_format: DEFAULT_SS58_FORMAT,
628            mnemonic: None,
629            seed: None,
630        })
631    }
632
633    /// Derive a keypair from a hex-encoded private key or seed bytes.
634    pub fn from_private_key(private_key: &str, crypto_type: u8) -> Result<Self, CoreError> {
635        // Don't interpolate the hex error: it echoes offending secret characters.
636        let private_key_vec = Zeroizing::new(
637            hex::decode(private_key.trim_start_matches("0x"))
638                .map_err(|_| crypto_err("invalid private_key hex string"))?,
639        );
640
641        if is_hashed_crypto(crypto_type) || is_pq_crypto(crypto_type) {
642            return Self::from_seed(&private_key_vec, crypto_type);
643        }
644
645        let inner = match crypto_type {
646            CRYPTO_SR25519 => {
647                KeypairInner::Sr25519(sr25519::Pair::from_seed_slice(&private_key_vec).map_err(
648                    |error| crypto_err(format!("invalid sr25519 private key: {error:?}")),
649                )?)
650            }
651            CRYPTO_ED25519 => {
652                let seed = if private_key_vec.len() >= 32 {
653                    &private_key_vec[..32]
654                } else {
655                    return Err(crypto_err("ed25519 private key must be at least 32 bytes"));
656                };
657                KeypairInner::Ed25519(
658                    WipingEd25519Pair::from_seed_slice(seed)
659                        .map_err(|error| {
660                            crypto_err(format!("invalid ed25519 private key: {error:?}"))
661                        })?
662                        .into_secret(),
663                )
664            }
665            other => return Err(crypto_err(format!("unknown crypto type {other}"))),
666        };
667
668        Ok(Self {
669            inner,
670            ss58_format: DEFAULT_SS58_FORMAT,
671            mnemonic: None,
672            seed: None,
673        })
674    }
675
676    /// Generate a fresh mnemonic with ``n_words`` (12/15/18/21/24).
677    pub fn generate_mnemonic(n_words: usize) -> Result<String, CoreError> {
678        use bip39::{Language, Mnemonic};
679
680        if !matches!(n_words, 12 | 15 | 18 | 21 | 24) {
681            return Err(crypto_err(format!(
682                "unsupported mnemonic length {n_words}; expected 12, 15, 18, 21, or 24"
683            )));
684        }
685
686        Mnemonic::generate_in(Language::English, n_words)
687            .map(|mnemonic| mnemonic.to_string())
688            .map_err(|error| crypto_err(format!("failed to generate mnemonic: {error}")))
689    }
690
691    /// Import a keypair from a PolkadotJS encrypted JSON keystore (v3).
692    #[cfg(feature = "host")]
693    pub fn from_encrypted_json(json_data: &str, passphrase: &str) -> Result<Self, CoreError> {
694        encrypted_json::create_from_encrypted_json(json_data, passphrase)
695    }
696
697    pub fn crypto_type(&self) -> u8 {
698        match &self.inner {
699            KeypairInner::Ed25519(_) => CRYPTO_ED25519,
700            KeypairInner::Sr25519(_) => CRYPTO_SR25519,
701            KeypairInner::Hashed(key) => key.crypto_type(),
702            KeypairInner::Mldsa(key) => match key.descriptor.algorithm {
703                4 => CRYPTO_MLDSA65,
704                5 => CRYPTO_MLDSA65_FIXED,
705                6 => CRYPTO_FALCON512_HASHED,
706                7 => CRYPTO_FALCON512,
707                8 => CRYPTO_FALCON1024_HASHED,
708                9 => CRYPTO_FALCON1024,
709                _ => unreachable!("validated post-quantum profile"),
710            },
711            KeypairInner::PublicOnly { crypto_type, .. } => *crypto_type,
712        }
713    }
714
715    /// Stable account identity. For hashed wallets, obtain the active signer
716    /// separately with `hashed_public_key`, only when constructing its proof.
717    pub fn public_key_bytes(&self) -> [u8; 32] {
718        match &self.inner {
719            KeypairInner::Ed25519(pair) => ed25519_public(pair),
720            KeypairInner::Sr25519(pair) => pair.public().0,
721            KeypairInner::Hashed(pair) => pair.account_id(),
722            KeypairInner::Mldsa(pair) => pair.account_id(),
723            KeypairInner::PublicOnly { public_key, .. } => *public_key,
724        }
725    }
726
727    pub fn ss58_address(&self) -> String {
728        ss58_from_public(self.public_key_bytes(), self.ss58_format)
729    }
730
731    pub fn ss58_format(&self) -> u16 {
732        self.ss58_format
733    }
734
735    pub fn private_key_bytes(&self) -> Option<Vec<u8>> {
736        self.inner.private_key_bytes()
737    }
738
739    /// The BIP39 phrase this keypair was derived from, when retained.
740    pub fn mnemonic(&self) -> Option<&str> {
741        self.mnemonic.as_deref().map(String::as_str)
742    }
743
744    /// The raw seed this keypair was derived from, when retained.
745    pub fn seed_bytes(&self) -> Option<&[u8]> {
746        self.seed.as_deref().map(Vec::as_slice)
747    }
748
749    #[cfg(feature = "host")]
750    pub(crate) fn from_inner(inner: KeypairInner, ss58_format: u16) -> Self {
751        Self {
752            inner,
753            ss58_format,
754            mnemonic: None,
755            seed: None,
756        }
757    }
758
759    /// Sign a message; returns the raw 64-byte signature.
760    pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
761        match &self.inner {
762            KeypairInner::Ed25519(pair) => Ok(pair.sign(message).to_bytes().to_vec()),
763            KeypairInner::Sr25519(pair) => Ok(as_bytes(&pair.sign(message))),
764            KeypairInner::Hashed(_) => self.sign_hashed_message(message),
765            KeypairInner::Mldsa(_) => Err(crypto_err(
766                "post-quantum arbitrary message signing is not supported",
767            )),
768            KeypairInner::PublicOnly { .. } => {
769                Err(crypto_err("no private key set to create signatures"))
770            }
771        }
772    }
773
774    /// Verify a signature. Matches btwallet's ``<Bytes>`` wrapping fallback.
775    pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, CoreError> {
776        let public_key = self.public_key_bytes();
777        let crypto_type = self.crypto_type();
778        if verify_with_crypto(crypto_type, &public_key, message, signature)? {
779            return Ok(true);
780        }
781        let wrapped = [b"<Bytes>", message, b"</Bytes>"].concat();
782        verify_with_crypto(crypto_type, &public_key, &wrapped, signature)
783    }
784
785    /// Encrypt a message to this keypair's ed25519 public key (sealed box).
786    #[cfg(feature = "host")]
787    pub fn encrypt(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
788        if self.crypto_type() != CRYPTO_ED25519 {
789            return Err(crypto_err(
790                "encrypt/decrypt is only supported for ed25519 keypairs",
791            ));
792        }
793        ensure_sodium()?;
794        let x25519_pk = ed25519_to_x25519_pk(&self.public_key_bytes())?;
795        Ok(sealedbox::seal(message, &x25519_pk))
796    }
797
798    /// Decrypt a sealed-box ciphertext with this ed25519 keypair's private key.
799    #[cfg(feature = "host")]
800    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CoreError> {
801        if self.crypto_type() != CRYPTO_ED25519 {
802            return Err(crypto_err(
803                "encrypt/decrypt is only supported for ed25519 keypairs",
804            ));
805        }
806        let KeypairInner::Ed25519(pair) = &self.inner else {
807            return Err(crypto_err(
808                "decryption requires a keypair with a private key",
809            ));
810        };
811        ensure_sodium()?;
812        let (x25519_pk, x25519_sk) = ed25519_x25519_from_pair(pair)?;
813        sealedbox::open(ciphertext, &x25519_pk, &x25519_sk)
814            .map_err(|_| crypto_err("decryption failed: invalid ciphertext or wrong key"))
815    }
816
817    /// Encrypt a message for a recipient SS58 address (ed25519 sealed box).
818    #[cfg(feature = "host")]
819    pub fn encrypt_for(
820        ss58_address: &str,
821        message: &[u8],
822        crypto_type: u8,
823    ) -> Result<Vec<u8>, CoreError> {
824        let recipient = Self::new(Some(ss58_address), None, crypto_type, DEFAULT_SS58_FORMAT)?;
825        recipient.encrypt(message)
826    }
827}
828
829/// Verify exactly the supplied message bytes against an SS58 address.
830///
831/// Uses the same per-scheme verifier as [`verify`] (pinned Substrate
832/// sr25519/ed25519 and the hashed-account message envelopes), without its
833/// legacy `<Bytes>` wrapper retry. Invalid SS58 addresses, unsupported crypto
834/// types and incorrect sr25519/ed25519 signature lengths return errors; other
835/// signatures that do not verify return `Ok(false)`.
836pub fn verify_strict(
837    message: &[u8],
838    signature: &[u8],
839    ss58_address: &str,
840    crypto_type: u8,
841) -> Result<bool, CoreError> {
842    let public_key = public_key_from_ss58(ss58_address)?;
843    verify_with_crypto(crypto_type, &public_key, message, signature)
844}
845
846/// Verify a signature against an SS58 address without holding the secret key.
847pub fn verify(
848    message: &[u8],
849    signature: &[u8],
850    ss58_address: &str,
851    crypto_type: u8,
852) -> Result<bool, CoreError> {
853    let public_key = public_key_from_ss58(ss58_address)?;
854    if verify_with_crypto(crypto_type, &public_key, message, signature)? {
855        return Ok(true);
856    }
857    let wrapped = [b"<Bytes>", message, b"</Bytes>"].concat();
858    verify_with_crypto(crypto_type, &public_key, &wrapped, signature)
859}
860
861#[cfg(test)]
862mod tests {
863    #![allow(clippy::unwrap_used, clippy::expect_used)]
864
865    use sp_core::crypto::Ss58AddressFormat;
866
867    use super::*;
868
869    #[test]
870    fn ss58_rendering_matches_sp_core() {
871        let mut state = 0x243f_6a88_85a3_08d3u64;
872        for format in [0u16, 2, 42, 63, 64, 255, 4096, 16383] {
873            for i in 0..50u32 {
874                let mut key = [0u8; 32];
875                key[..4].copy_from_slice(&i.to_le_bytes());
876                for b in &mut key[4..] {
877                    state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
878                    *b = (state >> 33) as u8;
879                }
880                assert_eq!(
881                    ss58_from_public(key, format),
882                    AccountId32::from(key)
883                        .to_ss58check_with_version(Ss58AddressFormat::custom(format)),
884                    "diverged for format {format}"
885                );
886            }
887        }
888    }
889
890    #[test]
891    fn alice_uri_matches_known_address() {
892        let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
893        assert_eq!(
894            kp.ss58_address(),
895            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
896        );
897    }
898
899    #[test]
900    fn public_only_from_ss58_matches_full_key() {
901        let full = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
902        let public = Keypair::new(Some(&full.ss58_address()), None, CRYPTO_SR25519, 42).unwrap();
903        assert_eq!(public.ss58_address(), full.ss58_address());
904        assert_eq!(public.public_key_bytes(), full.public_key_bytes());
905    }
906
907    #[test]
908    fn sr25519_verify_roundtrip() {
909        let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
910        let message = b"hello";
911        let sig = kp.sign(message).unwrap();
912        assert!(kp.verify(message, &sig).unwrap());
913    }
914
915    #[test]
916    fn ecdsa_ss58_matches_sp_core() {
917        use sp_core::ecdsa;
918
919        // sp-runtime's `MultiSigner::Ecdsa(pk).into_account()` is
920        // `blake2_256(pk.as_ref())` over the 33-byte compressed key.
921        let pair = ecdsa::Pair::from_string("//Alice", None).unwrap();
922        let public = pair.public();
923        let compressed: &[u8] = public.as_ref();
924        assert_eq!(compressed.len(), ECDSA_PUBLIC_KEY_LEN);
925        let expected = AccountId32::from(sp_core::hashing::blake2_256(compressed));
926        let account = account_id_from_ecdsa_public(compressed).unwrap();
927        assert_eq!(account, <[u8; 32]>::from(expected.clone()));
928        for format in [0u16, 42, 4096] {
929            assert_eq!(
930                ss58_from_ecdsa_public(compressed, format).unwrap(),
931                expected.to_ss58check_with_version(Ss58AddressFormat::custom(format))
932            );
933        }
934    }
935
936    #[test]
937    fn ecdsa_ss58_rejects_non_compressed_keys() {
938        assert!(account_id_from_ecdsa_public(&[0x02; 32]).is_err());
939        assert!(account_id_from_ecdsa_public(&[0x04; 33]).is_err());
940        assert!(account_id_from_ecdsa_public(&[0x04; 65]).is_err());
941    }
942
943    #[test]
944    fn sr25519_verify_bytes_wrapping() {
945        let kp = Keypair::from_uri("//Alice", CRYPTO_SR25519).unwrap();
946        let sig = kp.sign(b"<Bytes>hello</Bytes>").unwrap();
947        assert!(kp.verify(b"hello", &sig).unwrap());
948    }
949
950    #[test]
951    fn ed25519_signing_object_has_a_wiping_owner_and_keeps_chain_bytes() {
952        // The stored variant must be the wiping owner; this fails to compile
953        // if `KeypairInner::Ed25519` reverts to a non-wiping pair type.
954        fn stored(inner: &KeypairInner) -> Option<&Zeroizing<SigningKey>> {
955            match inner {
956                KeypairInner::Ed25519(secret) => Some(secret),
957                _ => None,
958            }
959        }
960        let keypair = Keypair::from_uri("//Alice", CRYPTO_ED25519).unwrap();
961        assert!(stored(&keypair.inner).is_some());
962
963        let seed = hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
964            .unwrap();
965        let keypair = Keypair::from_seed(&seed, CRYPTO_ED25519).unwrap();
966        assert_eq!(
967            hex::encode(keypair.public_key_bytes()),
968            "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
969        );
970        assert_eq!(
971            hex::encode(keypair.sign(b"").unwrap()),
972            "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
973        );
974    }
975
976    #[test]
977    fn zeroize_clears_every_signing_key_secret() {
978        use zeroize::Zeroize;
979        let mut key = SigningKey::from([7u8; 32]);
980        key.zeroize();
981        // Zebra's derived Zeroize clears the seed, expanded scalar and nonce
982        // prefix; the seed is the observable remainder.
983        assert_eq!(<[u8; 32]>::from(key), [0u8; 32]);
984    }
985
986    #[test]
987    fn hard_derivation_keeps_substrate_vectors() {
988        let alice = Keypair::from_uri("//Alice", CRYPTO_ED25519).unwrap();
989        assert_eq!(
990            hex::encode(alice.public_key_bytes()),
991            "88dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee"
992        );
993        let bob = Keypair::from_uri("//Bob", CRYPTO_ED25519).unwrap();
994        assert_eq!(
995            hex::encode(bob.public_key_bytes()),
996            "d17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae69"
997        );
998    }
999}