spark_rust/signer/traits/derivation_path.rs
1//! # Key derivation path handling for Spark wallet
2//!
3//! This module implements the key derivation scheme for the Spark wallet, following
4//! BIP43 conventions with a custom purpose identifier. The scheme is structured as:
5//!
6//! - Identity key: m/8797555'/account'/0'
7//! - Base signing key: m/8797555'/account'/1'
8//! - Temporary signing key: m/8797555'/account'/2'
9//!
10//! For leaf keys, an additional index is derived from the leaf ID:
11//! - Leaf key: m/8797555'/account'/1'/hash(leaf_id)'
12//!
13//! The purpose value 8797555 is derived as the last 3 bytes of sha256("spark") in decimal (863d73).
14//!
15//! All indices use hardened derivation (denoted by the apostrophe) for enhanced security.
16//! Account indices should start from 0.
17//!
18//! ## Security and Compatibility
19//!
20//! This derivation path scheme is **critical** for compatibility with other Spark wallets.
21//! All Spark wallets must follow this exact scheme to ensure interoperability and proper
22//! handling of funds within the Spark ecosystem.
23//!
24//! # WARNING
25//!
26//! Implementing this trait with non-standard derivation paths will make your wallet
27//! incompatible with the Spark ecosystem. Only implement this trait if you fully understand
28//! the consequences and are intentionally creating a non-standard wallet.
29
30use crate::error::SparkSdkError;
31
32use bitcoin::{
33 bip32::ChildNumber,
34 secp256k1::{PublicKey, SecretKey},
35 Network,
36};
37
38/// Represents a BIP32 derivation path for wallet keys.
39///
40/// This struct wraps a vector of `ChildNumber` values that define a complete
41/// derivation path for HD wallet keys. It provides convenience methods for
42/// formatting and accessing the path components.
43///
44/// # Examples
45///
46/// ```
47/// # use spark_rust::signer::traits::derivation_path::SparkDerivationPath;
48/// # use bitcoin::bip32::ChildNumber;
49/// # fn example() {
50/// // Create a path with purpose, account, key type (identity)
51/// let purpose = ChildNumber::from_hardened_idx(8797555).unwrap();
52/// let account = ChildNumber::from_hardened_idx(0).unwrap();
53/// let key_type = ChildNumber::from_hardened_idx(0).unwrap();
54///
55/// let path = SparkDerivationPath(vec![purpose, account, key_type]);
56///
57/// // Format as string: "m/8797555'/0'/0'"
58/// let path_str = path.to_string();
59/// # }
60/// ```
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct SparkDerivationPath(pub(crate) Vec<ChildNumber>);
63
64impl std::ops::Deref for SparkDerivationPath {
65 type Target = Vec<ChildNumber>;
66
67 fn deref(&self) -> &Self::Target {
68 &self.0
69 }
70}
71
72impl std::fmt::Display for SparkDerivationPath {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 let mut output = String::new();
75
76 // Only add the "m/" prefix for paths with more than one element
77 // For single-element paths (like leaf indices), don't add the prefix
78 if self.0.len() > 1 {
79 output.push('m');
80 }
81
82 // Format each child number in the path
83 for (i, child_number) in self.0.iter().enumerate() {
84 // Only add slash if it's not the first element or if we have the m/ prefix
85 if i > 0 || self.0.len() > 1 {
86 output.push('/');
87 }
88
89 // Get the index value
90 let index = match child_number {
91 ChildNumber::Hardened { index } => index,
92 ChildNumber::Normal { index } => index,
93 };
94
95 // Write the index
96 output.push_str(&index.to_string());
97
98 // Add apostrophe for hardened indices
99 if child_number.is_hardened() {
100 output.push('\'');
101 }
102 }
103
104 write!(f, "{}", output)
105 }
106}
107
108/// Key types for Spark wallet.
109///
110/// Each key type corresponds to a specific path in the hierarchical deterministic
111/// wallet structure, with the path format: m/8797555'/account'/key_type'
112///
113/// The key types serve different purposes in the Spark wallet:
114/// - Identity key: Used for wallet authentication and identification
115/// - Base signing key: Used as the foundation for leaf-specific keys
116/// - Temporary signing key: Used for one-time operations like deposits
117///
118/// These key types are encoded as the third component in the derivation path.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum SparkKeyType {
121 /// Identity key with path m/8797555'/account'/0'
122 ///
123 /// This key is used for wallet authentication, identification,
124 /// and signature verification with Spark Operators.
125 Identity,
126
127 /// Base signing key with path m/8797555'/account'/1'
128 ///
129 /// This key serves as the foundation for leaf-specific keys,
130 /// which are derived by adding a leaf-specific index.
131 BaseSigning,
132
133 /// Temporary signing key with path m/8797555'/account'/2'
134 ///
135 /// This key is used for one-time operations such as
136 /// deposit address generation and temporary signing operations.
137 TemporarySigning,
138}
139
140/// Trait for key derivation operations in the Spark wallet.
141///
142/// This trait defines the methods required to derive keys according to the
143/// Spark-specific derivation path scheme. Implementors must ensure they
144/// follow the exact derivation path structure to maintain compatibility
145/// with the Spark ecosystem.
146///
147/// The Spark derivation scheme follows:
148/// - m/8797555'/account'/key_type' for main keys
149/// - m/8797555'/account'/1'/hash(leaf_id)' for leaf-specific keys
150///
151/// Where:
152/// - 8797555' is the purpose value (derived from "spark")
153/// - account' is the account index (starting from 0)
154/// - key_type' is the key type (0' for identity, 1' for base signing, 2' for temporary)
155/// - hash(leaf_id)' is a hardened index derived from the leaf ID
156pub trait SparkSignerDerivationPath {
157 /// Derives the deposit signing key for the user.
158 ///
159 /// In Spark, users always have a single deposit key derived deterministically
160 /// from their seed. This method returns the public key corresponding to the
161 /// derived deposit signing key.
162 ///
163 /// The deposit signing key uses the `TemporarySigning` key type (path index 2')
164 /// in the derivation path.
165 ///
166 /// # Arguments
167 /// * `network` - The Bitcoin network to use for the key derivation
168 ///
169 /// # Returns
170 /// The public key for deposit operations or an error if derivation fails
171 fn get_deposit_signing_key(&self, network: Network) -> Result<PublicKey, SparkSdkError>;
172
173 /// Derives a Spark key for the specified key type, account, and optionally leaf ID.
174 ///
175 /// This is a core method for the Spark derivation path system. It handles the
176 /// derivation of all types of keys used in the Spark wallet, including identity
177 /// keys, base signing keys, temporary signing keys, and leaf-specific keys.
178 ///
179 /// # Arguments
180 /// * `leaf_id` - Optional leaf ID for leaf-specific keys. If provided, the key
181 /// will be derived using the leaf-specific path: m/8797555'/account'/1'/hash(leaf_id)'
182 /// * `account` - Account index to use in the derivation path
183 /// * `seed_bytes` - The seed bytes to derive the key from
184 /// * `key_type` - The type of key to derive (Identity, BaseSigning, or TemporarySigning)
185 /// * `network` - The Bitcoin network to use for the key derivation
186 ///
187 /// # Returns
188 /// The derived secret key or an error if derivation fails
189 fn derive_spark_key(
190 leaf_id: Option<String>,
191 account: u32,
192 seed_bytes: &[u8],
193 key_type: SparkKeyType,
194 network: Network,
195 ) -> Result<SecretKey, SparkSdkError>;
196
197 /// Returns the derivation path for the identity key.
198 ///
199 /// The identity key has the path: m/8797555'/account'/0'
200 /// This method constructs and returns this path for the specified account.
201 ///
202 /// # Arguments
203 /// * `account_index` - The account index to use in the derivation path
204 ///
205 /// # Returns
206 /// A `SparkDerivationPath` containing the identity key path components
207 /// or an error if path construction fails
208 fn get_identity_derivation_path(
209 account_index: u32,
210 ) -> Result<SparkDerivationPath, SparkSdkError>;
211}