spark_cryptography/
derivation_path.rs

1//! Spark Cryptography Module
2//!
3//! This module provides cryptographic functionality for the Spark wallet system, specifically
4//! handling key derivation and management.
5//!
6//! # Overview
7//!
8//! The Spark wallet uses a hierarchical deterministic (HD) wallet structure based on BIP32,
9//! with a custom purpose number (8797555) for Spark-specific keys. The key hierarchy is:
10//!
11//! ```
12//! // m/8797555'/account'/key_type'[/leaf_index']
13//! ```
14//!
15//! Where:
16//! - `account`: The account index (hardened)
17//! - `key_type`: The type of key (0' for identity, 1' for base signing, 2' for deposit)
18//! - `leaf_index`: Optional leaf-specific index (hardened)
19//!
20//! # Key Types
21//!
22//! The wallet supports three types of keys:
23//!
24//! 1. **Identity Key** (0')
25//!    - Used for wallet authentication and identification
26//!    - Used for signature verification with Spark Operators
27//!
28//! 2. **Base Signing Key** (1')
29//!    - Foundation for leaf-specific keys
30//!    - Used for general signing operations
31//!
32//! 3. **Deposit Key** (2')
33//!    - Used for deposit transactions. All deposit transactions are signed with this key.
34//!      However, each deposit address is different. Deposit signing key is static, and it is
35//!      not to be confused with one-time deposit address.
36//!    - After deposit, leaves are transferred to BaseSigning key
37//!
38//! # Usage
39//!
40//! ```rust
41//! use spark_cryptography::derivation_path::{derive_spark_key, SparkKeyType};
42//! use bitcoin::Network;
43//!
44//! // Generate a seed (in practice, use a secure random number generator)
45//! let seed_bytes = b"0000000000000000000000000000000000000000000000000000000000000000";
46//!
47//! // Derive an identity key
48//! let identity_key = derive_spark_key(
49//!     None,                    // No leaf ID for identity key
50//!     0,                       // Account index
51//!     seed_bytes,             // Seed
52//!     SparkKeyType::Identity, // Key type
53//!     Network::Bitcoin,       // Network
54//! ).unwrap();
55//!
56//! // Derive a leaf-specific key
57//! let leaf_key = derive_spark_key(
58//!     Some("leaf-uuid".to_string()),
59//!     0,
60//!     seed_bytes,
61//!     SparkKeyType::BaseSigning,
62//!     Network::Bitcoin,
63//! ).unwrap();
64//! ```
65//!
66//! # Security Considerations
67//!
68//! - All derivation paths use hardened derivation (with ' suffix)
69//! - Seeds must be at least 16 bytes long
70//! - Leaf indices are deterministically derived from leaf IDs
71//! - Different key types ensure separation of concerns
72//!
73//! # Error Handling
74//!
75//! The module provides detailed error types for various failure cases:
76//! - Invalid seeds
77//! - Invalid derivation paths
78//! - Invalid leaf indices
79//! - Invalid key types
80//! - Network-specific errors
81
82use bitcoin::{
83    bip32::{ChildNumber, Xpriv},
84    hashes::{sha256, Hash, HashEngine},
85    key::Secp256k1,
86    secp256k1::{All, PublicKey, SecretKey},
87    Network,
88};
89
90use crate::error::{InvalidLeafError, SparkCryptographyError};
91
92pub const SPARK_DERIVATION_PATH_PURPOSE: u32 = 8797555;
93
94/// The derivation path for a Spark key.
95///
96/// This is a wrapper around a vector of `ChildNumber` that represents the derivation path for a Spark key.
97/// It is used to derive a secret key from a seed and a derivation path.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct SparkDerivationPath(pub(crate) Vec<ChildNumber>);
100
101impl std::ops::Deref for SparkDerivationPath {
102    type Target = Vec<ChildNumber>;
103
104    fn deref(&self) -> &Self::Target {
105        &self.0
106    }
107}
108
109/// Key types for Spark wallet.
110///
111/// Each key type corresponds to a specific path in the hierarchical deterministic
112/// wallet structure, with the path format: m/8797555'/account'/key_type'
113///
114/// The key types serve different purposes in the Spark wallet:
115/// - Identity key: Used for wallet authentication and identification
116/// - Base signing key: Used as the foundation for leaf-specific keys
117/// - Temporary signing key: Used for one-time operations like deposits
118///
119/// These key types are encoded as the third component in the derivation path.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum SparkKeyType {
122    /// Identity key with path m/8797555'/account'/0'
123    ///
124    /// This key is used for wallet authentication, identification,
125    /// and signature verification with Spark Operators.
126    Identity,
127
128    /// Base signing key with path m/8797555'/account'/1'
129    ///
130    /// This key serves as the foundation for leaf-specific keys,
131    /// which are derived by adding a leaf-specific index.
132    BaseSigning,
133
134    /// Deposit key with path m/8797555'/account'/2'
135    ///
136    /// This key is used to sign deposits transactions. After the deposit is signed,
137    /// the user should transfer the leaves to their BaseSigning key under the same
138    /// leaf UUID. This happens automatically in `sparks-wallet`.
139    Deposit,
140}
141
142/// Derives the secret key for a given account index, network, and key type.
143///
144/// This function constructs a complete derivation path for the key,
145/// using the purpose index (8797555'), the account index, and the key type index.
146/// It then derives the key using the provided seed and secp256k1 context.
147///
148/// # Arguments
149///
150/// - `seed`: The seed bytes to derive the key from. In &[`u8`] format.
151/// - `account_index`: The account index in [`u32`] format.
152/// - `network`: The [`Network`] to derive the key for.
153/// - `secp`: The [`Secp256k1`] context to use for the derivation.
154/// - `key_type`: The [`SparkKeyType`] to derive the key for. For example, to get the identity key, use [`SparkKeyType::Identity`].
155///
156/// # Returns
157///
158/// A [`SecretKey`] representing the derived key, or an error if the derivation fails.
159///
160/// # Errors
161///
162/// - [`SparkCryptographyError::InvalidSeed`]: If the seed is invalid.
163/// - [`SparkCryptographyError::InvalidDerivationPath`]: If the derivation path is invalid.
164pub fn get_secret_key_with_key_type(
165    seed: &[u8],
166    account_index: u32,
167    network: Network,
168    key_type: SparkKeyType,
169    secp: &Secp256k1<All>,
170) -> Result<SecretKey, SparkCryptographyError> {
171    let seed = Xpriv::new_master(network, seed).map_err(|_| SparkCryptographyError::InvalidSeed)?;
172
173    let identity_derivation_path = get_derivation_path_with_key_type(account_index, key_type)?;
174
175    let identity_key = seed
176        .derive_priv(secp, &*identity_derivation_path)
177        .map_err(|_| {
178            SparkCryptographyError::InvalidDerivationPath(format!("{:?}", identity_derivation_path))
179        })?;
180
181    Ok(identity_key.private_key)
182}
183
184/// Derives the public key for a given account index, network, and key type.
185///
186/// This function constructs a complete derivation path for the key,
187/// using the purpose index (8797555'), the account index, and the key type index.
188/// It then derives the key using the provided seed and secp256k1 context.
189///
190/// # Arguments
191///
192/// - `seed`: The seed bytes to derive the key from. In &[`u8`] format.
193/// - `account_index`: The account index in [`u32`] format.
194/// - `network`: The [`Network`] to derive the key for.
195/// - `secp`: The [`Secp256k1`] context to use for the derivation.
196/// - `key_type`: The [`SparkKeyType`] to derive the key for. For example, to get the identity key, use [`SparkKeyType::Identity`].
197///
198/// # Returns
199///
200/// A [`PublicKey`] representing the derived key, or an error if the derivation fails.
201///
202/// # Errors
203///
204/// - [`SparkCryptographyError::InvalidSeed`]: If the seed is invalid.
205/// - [`SparkCryptographyError::InvalidDerivationPath`]: If the derivation path is invalid.
206///
207/// # Example
208///
209/// ```rust
210/// use spark_cryptography::derivation_path::{get_public_key_with_key_type, SparkKeyType};
211/// use bitcoin::Network;
212///
213/// let secp = bitcoin::secp256k1::Secp256k1::new();
214/// let seed_bytes = "0x0000000000000000000000000000000000000000000000000000000000000000".as_bytes();
215/// let public_key = get_public_key_with_key_type(seed_bytes, 0, Network::Bitcoin, SparkKeyType::Identity, &secp).unwrap();
216/// ```
217pub fn get_public_key_with_key_type(
218    seed: &[u8],
219    account_index: u32,
220    network: Network,
221    key_type: SparkKeyType,
222    secp: &Secp256k1<All>,
223) -> Result<PublicKey, SparkCryptographyError> {
224    let identity_secret_key =
225        get_secret_key_with_key_type(seed, account_index, network, key_type, secp)?;
226
227    Ok(identity_secret_key.public_key(secp))
228}
229
230/// Calculates the derivation path component for a leaf key based on a leaf ID.
231///
232/// This function hashes the leaf ID using SHA-256 and processes the hash to derive
233/// a hardened index for use in leaf key derivation paths.
234///
235/// The index is calculated by:
236/// 1. Computing SHA-256 hash of the leaf ID
237/// 2. Interpreting the first 4 bytes of the hash as u32 value
238/// 3. Modulo the u32 value with 0x80000000 to ensure it fits within valid index range
239/// 4. Converting to a hardened ChildNumber
240///
241/// # Arguments
242///
243/// - `leaf_id`: The leaf ID in &[`str`] format to derive the index for.
244///
245/// # Returns
246///
247/// A [`ChildNumber`] representing the derived index, or an error if the derivation fails.
248pub fn get_leaf_index(leaf_id: &str) -> Result<ChildNumber, SparkCryptographyError> {
249    // Compute SHA-256 hash of the leaf ID
250    let mut engine = sha256::Hash::engine();
251    engine.input(leaf_id.as_bytes());
252    let hash = sha256::Hash::from_engine(engine);
253
254    let chunk = &hash[0..4];
255    let amount = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) % 0x80000000;
256
257    // Return the hardened path component
258    get_child_number(amount, true)
259}
260
261/// Creates a ChildNumber from an index value and hardened flag.
262///
263/// This function handles validation of the provided index and creation of the
264/// appropriate ChildNumber type (normal or hardened).
265///
266/// # Parameters
267///
268/// - `index`: The index value (must be less than 0x80000000)
269/// - `hardened`: Whether the index should be hardened
270///
271/// # Returns
272///
273/// A ChildNumber representing the index, or an error if the index is invalid.
274pub fn get_child_number(index: u32, hardened: bool) -> Result<ChildNumber, SparkCryptographyError> {
275    if index > 0x7FFFFFFF {
276        return Err(SparkCryptographyError::InvalidLeaf(
277            InvalidLeafError::CannotDeriveChildNumberFromIndex(index),
278        ));
279    }
280
281    let child_number = if hardened {
282        ChildNumber::from_hardened_idx(index).unwrap()
283    } else {
284        ChildNumber::from_normal_idx(index).unwrap()
285    };
286
287    Ok(child_number)
288}
289
290/// Derives a Spark key from a seed and derivation path.
291///
292/// This function constructs a complete derivation path for a Spark key, including
293/// the purpose index, account index, key type index, and optional leaf index.
294/// It then uses the seed to derive the key using the secp256k1 library.
295///
296/// # Arguments
297///
298/// - `leaf_id`: The leaf ID to derive the key for.
299/// - `account`: The account index to derive the key for.
300/// - `seed_bytes`: The seed bytes to derive the key from.
301/// - `key_type`: The key type to derive the key for.
302/// - `network`: The network to derive the key for.
303///
304/// # Returns
305///
306/// A SecretKey representing the derived key, or an error if the derivation fails.
307///
308/// # Errors
309///
310/// - `InvalidSeed`: If the seed is invalid.
311/// - `InvalidDerivationPath`: If the derivation path is invalid.
312/// - `InvalidLeaf`: If the leaf index is invalid.
313/// - `InvalidKeyType`: If the key type is invalid.
314/// - `InvalidNetwork`: If the network is invalid.
315///
316/// # Example
317///
318/// ```rust
319/// use spark_cryptography::derivation_path::{derive_spark_key, SparkKeyType};
320/// use bitcoin::Network;
321///
322/// let seed_bytes = "0x0000000000000000000000000000000000000000000000000000000000000000".as_bytes();
323/// let key_type = SparkKeyType::Identity;
324/// let network = Network::Bitcoin;
325///
326/// let key = derive_spark_key(None, 0, seed_bytes, key_type, network).unwrap();
327/// let key_bytes = key.secret_bytes();
328/// ```
329pub fn derive_spark_key(
330    leaf_id: Option<String>,
331    account: u32,
332    seed_bytes: &[u8],
333    key_type: SparkKeyType,
334    network: Network,
335) -> Result<SecretKey, SparkCryptographyError> {
336    let path = prepare_derivation_path(account, key_type, leaf_id)?;
337
338    // Create the master key from seed
339    let seed = match Xpriv::new_master(network, seed_bytes) {
340        Ok(seed) => seed,
341        Err(_) => return Err(SparkCryptographyError::InvalidSeed),
342    };
343
344    // Derive the key for the given path
345    let secp = bitcoin::secp256k1::Secp256k1::new();
346    let extended_key = seed
347        .derive_priv(&secp, &path)
348        .map_err(|_| SparkCryptographyError::InvalidDerivationPath(format!("{:?}", path)))?;
349
350    Ok(extended_key.private_key)
351}
352
353/// Converts a SparkKeyType to its corresponding child index number.
354///
355/// Returns a hardened ChildNumber representing the key type index.
356fn get_key_type_index(key_type: SparkKeyType) -> Result<ChildNumber, SparkCryptographyError> {
357    let index = match key_type {
358        SparkKeyType::Identity => 0,
359        SparkKeyType::BaseSigning => 1,
360        SparkKeyType::Deposit => 2,
361    };
362
363    // Key type index should be hardened
364    get_child_number(index, true)
365}
366
367fn prepare_derivation_path(
368    account_index: u32,
369    key_type: SparkKeyType,
370    leaf_index: Option<String>,
371) -> Result<Vec<ChildNumber>, SparkCryptographyError> {
372    let purpose_index = get_child_number(SPARK_DERIVATION_PATH_PURPOSE, true)?;
373
374    // Returns the hardened account index
375    let account_index = get_child_number(account_index, true)?;
376
377    // Returns the hardened key type index
378    let key_type_index = get_key_type_index(key_type)?;
379
380    // If leaf_id is provided and non-empty, calculate the leaf index
381    let leaf_index = if let Some(leaf_id) = leaf_index {
382        Some(get_leaf_index(leaf_id.as_str())?)
383    } else {
384        None
385    };
386
387    Ok(prepare_path(
388        purpose_index,
389        account_index,
390        key_type_index,
391        leaf_index,
392    ))
393}
394
395/// Prepares a complete derivation path for a Spark key.
396///
397/// Constructs a derivation path with the following components:
398/// - Purpose index (8797555')
399/// - Account index
400/// - Key type index (0' for identity, 1' for base signing, 2' for temporary signing)
401/// - Optional leaf index (for leaf keys)
402///
403/// The purpose index is the first element of the path, followed by account index,
404/// key type, and optionally a leaf index.
405fn prepare_path(
406    purpose_index: ChildNumber,
407    account_index: ChildNumber,
408    key_type_index: ChildNumber,
409    // If leaf index is NOT provided, it means this is for the identity key.
410    leaf_index: Option<ChildNumber>,
411) -> Vec<ChildNumber> {
412    let mut path = vec![purpose_index, account_index, key_type_index];
413
414    if let Some(leaf_index) = leaf_index {
415        path.push(leaf_index);
416    }
417
418    path
419}
420
421/// Derives the identity derivation path for a given account index.
422///
423/// This function constructs a complete derivation path for the identity key,
424/// using the purpose index (8797555'), the account index, and the key type index (0').
425///
426/// # Arguments
427///
428/// - `account_index`: The account index in [`u32`] format.
429///
430/// # Returns
431///
432/// A [`SparkDerivationPath`] representing the derivation path for the identity key, or an error if the derivation fails.
433///
434/// # Errors
435///
436/// - [`SparkCryptographyError::InvalidLeaf`]: If the leaf index is invalid.
437///
438/// # Example
439///
440/// ```rust
441/// use spark_cryptography::derivation_path::{get_derivation_path_with_key_type, SparkKeyType};
442/// use bitcoin::Network;
443///
444/// let path = get_derivation_path_with_key_type(0, SparkKeyType::Identity).unwrap();
445/// ```
446pub fn get_derivation_path_with_key_type(
447    account_index: u32,
448    key_type: SparkKeyType,
449) -> Result<SparkDerivationPath, SparkCryptographyError> {
450    let path = prepare_derivation_path(account_index, key_type, None)?;
451    Ok(SparkDerivationPath(path))
452}
453
454#[cfg(test)]
455mod derivation_path_tests {
456    use super::*;
457    use bitcoin::Network;
458
459    #[test]
460    fn test_get_leaf_index() {
461        let leaf_id_1 = "019534f0-f4e2-7845-87fe-c6ea2fa69f80";
462        let leaf_id_2 = "019534f0-f4e2-7868-b3fa-d06dc10b79e7";
463        let leaf_id_3 = "dbb5c090-dca4-47ec-9f20-41edd4594dcf";
464
465        let child_number_1 = get_leaf_index(leaf_id_1).unwrap();
466        let child_number_2 = get_leaf_index(leaf_id_2).unwrap();
467        let child_number_3 = get_leaf_index(leaf_id_3).unwrap();
468
469        assert_eq!(
470            child_number_1,
471            ChildNumber::from_hardened_idx(1137822116).unwrap()
472        );
473        assert_eq!(
474            child_number_2,
475            ChildNumber::from_hardened_idx(1199130649).unwrap()
476        );
477        assert_eq!(
478            child_number_3,
479            ChildNumber::from_hardened_idx(1743780874).unwrap()
480        );
481    }
482
483    #[test]
484    fn test_get_identity_derivation_path() {
485        let path = get_derivation_path_with_key_type(0, SparkKeyType::Identity).unwrap();
486        assert_eq!(path.0.len(), 3);
487
488        // Test with different account indices
489        let path_account_1 = get_derivation_path_with_key_type(1, SparkKeyType::Identity).unwrap();
490        let path_account_2 = get_derivation_path_with_key_type(2, SparkKeyType::Identity).unwrap();
491
492        assert_eq!(path_account_1.0.len(), 3);
493        assert_eq!(path_account_2.0.len(), 3);
494
495        // Verify the components
496        assert_eq!(
497            path.0[0],
498            ChildNumber::from_hardened_idx(SPARK_DERIVATION_PATH_PURPOSE).unwrap()
499        );
500        assert_eq!(path.0[1], ChildNumber::from_hardened_idx(0).unwrap());
501        assert_eq!(path.0[2], ChildNumber::from_hardened_idx(0).unwrap()); // Identity key type
502    }
503
504    #[test]
505    fn test_derive_spark_key() {
506        let seed_bytes = b"0000000000000000000000000000000000000000000000000000000000000000";
507        let network = Network::Bitcoin;
508        let secp = Secp256k1::new();
509
510        // Test identity key derivation
511        let identity_key =
512            derive_spark_key(None, 0, seed_bytes, SparkKeyType::Identity, network).unwrap();
513        let identity_pubkey = identity_key.public_key(&secp);
514
515        // Test base signing key derivation
516        let base_signing_key =
517            derive_spark_key(None, 0, seed_bytes, SparkKeyType::BaseSigning, network).unwrap();
518        let base_signing_pubkey = base_signing_key.public_key(&secp);
519
520        // Test deposit key derivation
521        let deposit_key =
522            derive_spark_key(None, 0, seed_bytes, SparkKeyType::Deposit, network).unwrap();
523        let deposit_pubkey = deposit_key.public_key(&secp);
524
525        // Verify all keys are different
526        assert_ne!(identity_pubkey, base_signing_pubkey);
527        assert_ne!(identity_pubkey, deposit_pubkey);
528        assert_ne!(base_signing_pubkey, deposit_pubkey);
529    }
530
531    #[test]
532    fn test_derive_spark_key_with_leaf() {
533        let seed_bytes = b"0000000000000000000000000000000000000000000000000000000000000000";
534        let network = Network::Bitcoin;
535        let leaf_id = "test-leaf-id";
536
537        // Test base signing key with leaf
538        let key_with_leaf = derive_spark_key(
539            Some(leaf_id.to_string()),
540            0,
541            seed_bytes,
542            SparkKeyType::BaseSigning,
543            network,
544        )
545        .unwrap();
546
547        // Test base signing key without leaf
548        let key_without_leaf =
549            derive_spark_key(None, 0, seed_bytes, SparkKeyType::BaseSigning, network).unwrap();
550
551        // Verify keys are different
552        assert_ne!(key_with_leaf, key_without_leaf);
553    }
554
555    #[test]
556    fn test_get_child_number() {
557        assert_eq!(2147483648, 0x7FFFFFFF_u32 + 1);
558
559        // Test valid hardened indices
560        assert!(get_child_number(0, true).is_ok());
561        assert!(get_child_number(0x7FFFFFFF, true).is_ok());
562
563        // Test valid normal indices
564        assert!(get_child_number(0, false).is_ok());
565        assert!(get_child_number(0x7FFFFFFF, false).is_ok());
566
567        assert_eq!(
568            get_child_number(0x80000000, false).unwrap_err(),
569            SparkCryptographyError::InvalidLeaf(
570                InvalidLeafError::CannotDeriveChildNumberFromIndex(0x80000000)
571            )
572        );
573    }
574
575    #[test]
576    fn test_get_key_type_index() {
577        // Test all key types
578        let identity_idx = get_key_type_index(SparkKeyType::Identity).unwrap();
579        let base_signing_idx = get_key_type_index(SparkKeyType::BaseSigning).unwrap();
580        let deposit_idx = get_key_type_index(SparkKeyType::Deposit).unwrap();
581
582        assert_eq!(identity_idx, ChildNumber::from_hardened_idx(0).unwrap());
583        assert_eq!(base_signing_idx, ChildNumber::from_hardened_idx(1).unwrap());
584        assert_eq!(deposit_idx, ChildNumber::from_hardened_idx(2).unwrap());
585    }
586
587    #[test]
588    fn test_network_specific() {
589        let seed_bytes = b"0000000000000000000000000000000000000000000000000000000000000000";
590        let secp = Secp256k1::new();
591
592        // Test with different networks
593        let bitcoin_key = derive_spark_key(
594            None,
595            0,
596            seed_bytes,
597            SparkKeyType::Identity,
598            Network::Bitcoin,
599        )
600        .unwrap();
601        let testnet_key = derive_spark_key(
602            None,
603            0,
604            seed_bytes,
605            SparkKeyType::Identity,
606            Network::Testnet,
607        )
608        .unwrap();
609        let regtest_key = derive_spark_key(
610            None,
611            0,
612            seed_bytes,
613            SparkKeyType::Identity,
614            Network::Regtest,
615        )
616        .unwrap();
617        let signet_key =
618            derive_spark_key(None, 0, seed_bytes, SparkKeyType::Identity, Network::Signet).unwrap();
619
620        // Verify keys are the same for different networks. This will change in the future as the derivation path becomes different for different networks.
621        assert_eq!(bitcoin_key.public_key(&secp), testnet_key.public_key(&secp));
622        assert_eq!(bitcoin_key.public_key(&secp), regtest_key.public_key(&secp));
623        assert_eq!(bitcoin_key.public_key(&secp), signet_key.public_key(&secp));
624    }
625
626    #[test]
627    fn test_get_public_and_secret_keys_with_key_type() {
628        // Setup test environment with a zero seed and Bitcoin network
629        let seed_bytes = b"0000000000000000000000000000000000000000000000000000000000000000";
630        let network = Network::Bitcoin;
631        let secp = Secp256k1::new();
632
633        // Test identity key derivation using derive_spark_key
634        let identity_key =
635            derive_spark_key(None, 0, seed_bytes, SparkKeyType::Identity, network).unwrap();
636        let identity_pubkey = identity_key.public_key(&secp);
637
638        // Test base signing key derivation using derive_spark_key
639        let base_signing_key =
640            derive_spark_key(None, 0, seed_bytes, SparkKeyType::BaseSigning, network).unwrap();
641        let base_signing_pubkey = base_signing_key.public_key(&secp);
642
643        // Verify that get_secret_key_with_key_type produces the same identity key
644        let identity_secret_key =
645            get_secret_key_with_key_type(seed_bytes, 0, network, SparkKeyType::Identity, &secp)
646                .unwrap();
647        assert_eq!(identity_secret_key, identity_key);
648
649        // Verify that get_public_key_with_key_type produces the same identity public key
650        let identity_public_key =
651            get_public_key_with_key_type(seed_bytes, 0, network, SparkKeyType::Identity, &secp)
652                .unwrap();
653        assert_eq!(identity_public_key, identity_pubkey);
654
655        // Verify that get_secret_key_with_key_type produces the same base signing key
656        let base_signing_secret_key =
657            get_secret_key_with_key_type(seed_bytes, 0, network, SparkKeyType::BaseSigning, &secp)
658                .unwrap();
659        assert_eq!(base_signing_secret_key, base_signing_key);
660
661        // Verify that get_public_key_with_key_type produces the same base signing public key
662        let base_signing_public_key =
663            get_public_key_with_key_type(seed_bytes, 0, network, SparkKeyType::BaseSigning, &secp)
664                .unwrap();
665        assert_eq!(base_signing_public_key, base_signing_pubkey);
666    }
667}