Skip to main content

ark_client/
key_provider.rs

1use crate::error::Error;
2use bitcoin::bip32::DerivationPath;
3use bitcoin::bip32::Xpriv;
4use bitcoin::key::Keypair;
5use bitcoin::secp256k1::Secp256k1;
6use std::sync::Arc;
7
8pub enum KeypairIndex {
9    /// Increments the index and returns a new keypair
10    New,
11    /// Returns the last unused address
12    LastUnused,
13}
14
15/// Provides keypairs for signing operations
16///
17/// This trait allows different key management strategies:
18/// - Static keypair (single key)
19/// - BIP32 HD wallet (hierarchical deterministic)
20/// - Hardware wallets (future)
21/// - Custom key derivation schemes
22pub trait KeyProvider: Send + Sync {
23    /// Get a keypair for receiving funds
24    ///
25    /// For static key providers, this always returns the same keypair regardless of the index.
26    /// For HD wallets, behavior depends on the `keypair_index` parameter.
27    ///
28    /// # Arguments
29    ///
30    /// * `keypair_index` - Controls which keypair to return:
31    ///   - `KeypairIndex::New`: Increments the internal index and returns a new keypair
32    ///   - `KeypairIndex::LastUnused`: Returns the last unused keypair without incrementing
33    ///
34    /// # Returns
35    ///
36    /// A keypair to use for receiving funds
37    fn get_next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error>;
38
39    /// Get a keypair for a specific BIP32 derivation path
40    ///
41    /// # Arguments
42    ///
43    /// * `path` - BIP32 derivation path as an array of child indexes
44    ///
45    /// # Returns
46    ///
47    /// A keypair derived at the specified path, or an error if derivation is not supported
48    fn get_keypair_for_path(&self, path: &[u32]) -> Result<Keypair, Error>;
49
50    /// Get a keypair for a specific public key
51    ///
52    /// This is essential for HD wallets where you need to find the correct keypair
53    /// for signing with a previously generated public key.
54    ///
55    /// # Arguments
56    ///
57    /// * `pk` - The X-only public key to find the keypair for
58    ///
59    /// # Returns
60    ///
61    /// The keypair corresponding to the public key, or an error if not found
62    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error>;
63
64    /// Get all public keys that this provider currently knows about
65    ///
66    /// For static key providers, this returns the single keypair's public key.
67    /// For HD wallets, this returns all public keys that have been derived and cached
68    /// (i.e., keys generated via `get_next_keypair`).
69    ///
70    /// This is useful for determining which keys are available for signing operations
71    /// without having to search or derive new keys.
72    ///
73    /// # Returns
74    ///
75    /// A vector of X-only public keys known to this provider
76    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error>;
77}
78
79/// Key provider extension for index-based discovery and restore.
80pub trait DiscoverableKeyProvider: KeyProvider {
81    /// Get the derivation index for a cached public key.
82    fn get_derivation_index_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Option<u32>;
83
84    /// Derive a keypair at a specific index without caching.
85    fn derive_at_discovery_index(&self, index: u32) -> Result<Option<Keypair>, Error>;
86
87    /// Cache a discovered keypair at the given index.
88    ///
89    /// This is called after discovery determines a key is "used" (has VTXOs).
90    /// Implementations should also advance receive-key state to avoid reuse.
91    fn cache_discovered_keypair(&self, index: u32, kp: Keypair) -> Result<(), Error>;
92
93    /// Derive and cache the keypair at a persisted derivation index.
94    ///
95    /// This is used after loading persisted contracts so HD wallets can sign for stored
96    /// contracts immediately after restart, without running a full restore scan.
97    /// Implementations must not advance receive-key discovery state here.
98    fn cache_keypair_at_index(&self, index: u32) -> Result<(), Error>;
99
100    fn mark_as_used(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<(), Error>;
101}
102
103/// A simple key provider that uses a static keypair
104///
105/// This is the simplest implementation and is backward compatible with
106/// the original single-keypair design.
107#[derive(Clone)]
108pub struct StaticKeyProvider {
109    kp: Keypair,
110}
111
112impl StaticKeyProvider {
113    /// Create a new static key provider
114    pub fn new(kp: Keypair) -> Self {
115        Self { kp }
116    }
117}
118
119impl KeyProvider for StaticKeyProvider {
120    fn get_next_keypair(&self, _: KeypairIndex) -> Result<Keypair, Error> {
121        // Static provider always returns the same keypair
122        Ok(self.kp)
123    }
124
125    fn get_keypair_for_path(&self, _path: &[u32]) -> Result<Keypair, Error> {
126        // Static provider always returns the same keypair
127        Ok(self.kp)
128    }
129
130    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error> {
131        // Verify that the requested public key matches our keypair
132        let our_pk = self.kp.x_only_public_key().0;
133        if &our_pk == pk {
134            Ok(self.kp)
135        } else {
136            Err(Error::ad_hoc(format!(
137                "Public key mismatch: requested {pk}, but only have {our_pk}"
138            )))
139        }
140    }
141
142    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error> {
143        Ok(vec![self.kp.public_key().into()])
144    }
145}
146
147/// A BIP32 hierarchical deterministic key provider
148///
149/// This provider derives keypairs from a master extended private key
150/// using BIP32 derivation paths. It maintains an index counter for
151/// generating new receiving addresses.
152///
153/// ## Example
154///
155/// ```rust
156/// # use std::str::FromStr;
157/// # use bitcoin::bip32::{Xpriv, DerivationPath};
158/// # use bitcoin::Network;
159/// # use crate::ark_client::KeyProvider;
160/// # use ark_client::Bip32KeyProvider;
161/// # use ark_client::key_provider::KeypairIndex;
162///
163/// fn example() -> Result<(), Box<dyn std::error::Error>> {
164/// // Create from a master key with a base path (e.g., m/84'/0'/0'/0)
165/// let master_key = Xpriv::from_str("xprv...")?;
166/// let base_path = DerivationPath::from_str("m/84'/0'/0'/0")?;
167///
168/// // This will derive keys at m/84'/0'/0'/0/0, m/84'/0'/0'/0/1, etc.
169/// let provider = Bip32KeyProvider::new(master_key, base_path);
170///
171/// // Get the next receiving keypair (increments index)
172/// let kp1 = provider.get_next_keypair(KeypairIndex::New)?; // m/84'/0'/0'/0/0
173/// let kp2 = provider.get_next_keypair(KeypairIndex::New)?; // m/84'/0'/0'/0/1
174///
175/// // Or derive a specific keypair by path
176/// let custom_path = vec![84 + 0x8000_0000, 0x8000_0000, 0x8000_0000, 0, 5];
177/// let kp = provider.get_keypair_for_path(&custom_path)?;
178/// # Ok(())
179/// # }
180/// ```
181pub struct Bip32KeyProvider {
182    master_key: Xpriv,
183    base_path: DerivationPath,
184    // Using std::sync::Mutex for interior mutability across Send + Sync
185    next_index: Arc<std::sync::Mutex<u32>>,
186    // Cache of derived keys: pk -> (path_index, keypair, used)
187    // The `used` flag indicates whether this keypair has been used (has VTXOs)
188    key_cache:
189        Arc<std::sync::RwLock<std::collections::HashMap<bitcoin::XOnlyPublicKey, KeyCacheValue>>>,
190}
191
192#[derive(Clone, Copy)]
193pub struct KeyCacheValue {
194    path_index: u32,
195    kp: Keypair,
196    /// Indicates whether this keypair has been used (has VTXOs).
197    used: bool,
198}
199
200impl Bip32KeyProvider {
201    /// Create a new BIP32 key provider
202    ///
203    /// # Arguments
204    ///
205    /// * `master_key` - The master extended private key (xpriv)
206    /// * `base_path` - The base derivation path (e.g., m/84'/0'/0'/0). The provider will append
207    ///   index numbers to this path.
208    pub fn new(master_key: Xpriv, base_path: DerivationPath) -> Self {
209        Self {
210            master_key,
211            base_path,
212            next_index: Arc::new(std::sync::Mutex::new(0)),
213            key_cache: Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
214        }
215    }
216
217    /// Create a new BIP32 key provider starting from a specific index
218    ///
219    /// # Arguments
220    ///
221    /// * `master_key` - The master extended private key (xpriv)
222    /// * `base_path` - The base derivation path
223    /// * `start_index` - The starting index for key derivation
224    pub fn new_with_index(master_key: Xpriv, base_path: DerivationPath, start_index: u32) -> Self {
225        Self {
226            master_key,
227            base_path,
228            next_index: Arc::new(std::sync::Mutex::new(start_index)),
229            key_cache: Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
230        }
231    }
232
233    /// Derive a keypair at the specified path
234    fn derive_keypair(&self, path: &DerivationPath) -> Result<Keypair, Error> {
235        let secp = Secp256k1::new();
236        let derived_key = self
237            .master_key
238            .derive_priv(&secp, path)
239            .map_err(|e| Error::ad_hoc(format!("BIP32 derivation failed: {e}")))?;
240
241        Ok(derived_key.to_keypair(&secp))
242    }
243
244    /// Derive a keypair at base_path/index
245    fn derive_at_index(&self, index: u32) -> Result<Keypair, Error> {
246        use bitcoin::bip32::ChildNumber;
247
248        let path = self.base_path.clone();
249        let path = path.extend([ChildNumber::Normal { index }]);
250
251        self.derive_keypair(&path)
252    }
253}
254
255impl KeyProvider for Bip32KeyProvider {
256    fn get_next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error> {
257        match keypair_index {
258            KeypairIndex::New => {
259                // Get and increment the next index
260                let index = {
261                    let mut next_index = self
262                        .next_index
263                        .lock()
264                        .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
265                    let current = *next_index;
266                    *next_index = next_index
267                        .checked_add(1)
268                        .ok_or_else(|| Error::ad_hoc("Key derivation index overflow"))?;
269                    current
270                };
271
272                // Derive the keypair at this index
273                let kp = self.derive_at_index(index)?;
274
275                // Cache it for later lookup (marked as unused)
276                let pk = kp.x_only_public_key().0;
277                {
278                    let mut cache = self
279                        .key_cache
280                        .write()
281                        .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
282                    cache.insert(
283                        pk,
284                        KeyCacheValue {
285                            path_index: index,
286                            kp,
287                            used: false,
288                        },
289                    );
290                }
291
292                Ok(kp)
293            }
294            KeypairIndex::LastUnused => {
295                // First, try to find an unused keypair in the cache
296                {
297                    let cache = self
298                        .key_cache
299                        .read()
300                        .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
301
302                    // Find the unused keypair with the lowest index
303                    let unused = cache
304                        .values()
305                        .filter(|KeyCacheValue { used, .. }| !used)
306                        .min_by_key(|KeyCacheValue { path_index, .. }| *path_index);
307
308                    if let Some(KeyCacheValue { kp, .. }) = unused {
309                        return Ok(*kp);
310                    }
311                }
312
313                // No unused keypair found, derive a new one
314                self.get_next_keypair(KeypairIndex::New)
315            }
316        }
317    }
318
319    fn get_keypair_for_path(&self, path: &[u32]) -> Result<Keypair, Error> {
320        use bitcoin::bip32::ChildNumber;
321        let child_numbers: Vec<ChildNumber> = path
322            .iter()
323            .map(|&n| {
324                if n & 0x8000_0000 != 0 {
325                    ChildNumber::Hardened {
326                        index: n & 0x7FFF_FFFF,
327                    }
328                } else {
329                    ChildNumber::Normal { index: n }
330                }
331            })
332            .collect();
333        let derivation_path = DerivationPath::from(child_numbers);
334        self.derive_keypair(&derivation_path)
335    }
336
337    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error> {
338        // First check the cache
339        {
340            let cache = self
341                .key_cache
342                .read()
343                .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
344            if let Some(KeyCacheValue { kp, .. }) = cache.get(pk) {
345                return Ok(*kp);
346            }
347        }
348
349        // If not in cache, we need to search. For now, we'll search up to the current index
350        let current_index = {
351            let next_index = self
352                .next_index
353                .lock()
354                .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
355            *next_index
356        };
357
358        // Search through derived keys up to current index
359        for i in 0..current_index {
360            let kp = self.derive_at_index(i)?;
361            let derived_pk = kp.x_only_public_key().0;
362
363            if &derived_pk == pk {
364                // Cache it for next time (assume used since we're looking it up for signing)
365                let mut cache = self
366                    .key_cache
367                    .write()
368                    .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
369                cache.insert(
370                    derived_pk,
371                    KeyCacheValue {
372                        path_index: i,
373                        kp,
374                        used: true,
375                    },
376                );
377                return Ok(kp);
378            }
379        }
380
381        Err(Error::ad_hoc(format!(
382            "Public key {pk} not found in HD wallet. \
383            Searched indices 0..{current_index}. \
384            The key may have been generated outside this provider."
385        )))
386    }
387
388    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error> {
389        let cache = self
390            .key_cache
391            .read()
392            .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
393
394        Ok(cache.keys().copied().collect())
395    }
396}
397
398impl DiscoverableKeyProvider for Bip32KeyProvider {
399    fn get_derivation_index_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Option<u32> {
400        let cache = self.key_cache.read().ok()?;
401        cache.get(pk).map(|v| v.path_index)
402    }
403
404    fn derive_at_discovery_index(&self, index: u32) -> Result<Option<Keypair>, Error> {
405        self.derive_at_index(index).map(Some)
406    }
407
408    fn cache_discovered_keypair(&self, index: u32, kp: Keypair) -> Result<(), Error> {
409        let pk = kp.x_only_public_key().0;
410
411        // Add to cache (marked as used since it was discovered with VTXOs)
412        {
413            let mut cache = self
414                .key_cache
415                .write()
416                .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
417            cache.insert(
418                pk,
419                KeyCacheValue {
420                    path_index: index,
421                    kp,
422                    used: true,
423                },
424            );
425        }
426
427        // Update next_index if needed (set to index + 1 if >= current)
428        {
429            let mut next = self
430                .next_index
431                .lock()
432                .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
433            if index >= *next {
434                *next = index
435                    .checked_add(1)
436                    .ok_or_else(|| Error::ad_hoc("Key derivation index overflow"))?;
437            }
438        }
439
440        Ok(())
441    }
442
443    fn cache_keypair_at_index(&self, index: u32) -> Result<(), Error> {
444        let kp = self.derive_at_index(index)?;
445        let pk = kp.x_only_public_key().0;
446        let mut cache = self
447            .key_cache
448            .write()
449            .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
450        cache.insert(
451            pk,
452            KeyCacheValue {
453                path_index: index,
454                kp,
455                used: true,
456            },
457        );
458        Ok(())
459    }
460
461    fn mark_as_used(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<(), Error> {
462        // First check the cache
463        {
464            let maybe_kp = {
465                let cache = self
466                    .key_cache
467                    .read()
468                    .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
469                cache.get(pk).copied()
470            };
471
472            match maybe_kp {
473                Some(KeyCacheValue {
474                    path_index,
475                    kp,
476                    used: false,
477                }) => {
478                    let mut cache = self
479                        .key_cache
480                        .write()
481                        .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
482                    cache.insert(
483                        *pk,
484                        KeyCacheValue {
485                            path_index,
486                            kp,
487                            used: true,
488                        },
489                    );
490                    return Ok(());
491                }
492                Some(KeyCacheValue { used: true, .. }) => {
493                    // already marked as used
494                    return Ok(());
495                }
496                _ => {
497                    // no found
498                }
499            }
500        }
501
502        // If not in cache, we need to search. For now, we'll search up to the current index
503        let current_index = {
504            let next_index = self
505                .next_index
506                .lock()
507                .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
508            *next_index
509        };
510
511        // Search through derived keys up to current index
512        for i in 0..current_index {
513            let kp = self.derive_at_index(i)?;
514            let derived_pk = kp.x_only_public_key().0;
515
516            if &derived_pk == pk {
517                // Cache it for next time (assume used since we're looking it up for signing)
518                let mut cache = self
519                    .key_cache
520                    .write()
521                    .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
522                cache.insert(
523                    derived_pk,
524                    KeyCacheValue {
525                        path_index: i,
526                        kp,
527                        used: true,
528                    },
529                );
530                return Ok(());
531            }
532        }
533
534        Err(Error::ad_hoc(format!(
535            "Public key {pk} not found in HD wallet. \
536            Searched indices 0..{current_index}. \
537            The key may have been generated outside this provider."
538        )))
539    }
540}
541
542// Implement KeyProvider for Arc<T> where T: KeyProvider
543impl<T: KeyProvider + ?Sized> KeyProvider for Arc<T> {
544    fn get_next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error> {
545        (**self).get_next_keypair(keypair_index)
546    }
547
548    fn get_keypair_for_path(&self, path: &[u32]) -> Result<Keypair, Error> {
549        (**self).get_keypair_for_path(path)
550    }
551
552    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error> {
553        (**self).get_keypair_for_pk(pk)
554    }
555
556    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error> {
557        (**self).get_cached_pks()
558    }
559}
560
561impl<T: DiscoverableKeyProvider + ?Sized> DiscoverableKeyProvider for Arc<T> {
562    fn get_derivation_index_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Option<u32> {
563        (**self).get_derivation_index_for_pk(pk)
564    }
565
566    fn derive_at_discovery_index(&self, index: u32) -> Result<Option<Keypair>, Error> {
567        (**self).derive_at_discovery_index(index)
568    }
569
570    fn cache_discovered_keypair(&self, index: u32, kp: Keypair) -> Result<(), Error> {
571        (**self).cache_discovered_keypair(index, kp)
572    }
573
574    fn cache_keypair_at_index(&self, index: u32) -> Result<(), Error> {
575        (**self).cache_keypair_at_index(index)
576    }
577
578    fn mark_as_used(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<(), Error> {
579        (**self).mark_as_used(pk)
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use bitcoin::Network;
587    use std::str::FromStr;
588
589    #[test]
590    fn cache_keypair_at_index_hydrates_hd_lookup_after_restart() {
591        let seed = [7_u8; 32];
592        let master = Xpriv::new_master(Network::Regtest, &seed).unwrap();
593        let base_path = DerivationPath::from_str("m/86'/1'/0'/0").unwrap();
594
595        let original = Bip32KeyProvider::new(master, base_path.clone());
596        let expected = original.derive_at_discovery_index(7).unwrap().unwrap();
597        let expected_pk = expected.x_only_public_key().0;
598        let first_receive_pk = original
599            .derive_at_discovery_index(0)
600            .unwrap()
601            .unwrap()
602            .x_only_public_key()
603            .0;
604
605        let restarted = Bip32KeyProvider::new(master, base_path);
606        assert!(restarted.get_keypair_for_pk(&expected_pk).is_err());
607
608        restarted.cache_keypair_at_index(7).unwrap();
609        let actual = restarted.get_keypair_for_pk(&expected_pk).unwrap();
610
611        assert_eq!(actual.x_only_public_key().0, expected_pk);
612        assert_eq!(restarted.get_derivation_index_for_pk(&expected_pk), Some(7));
613        assert_eq!(
614            restarted
615                .get_next_keypair(KeypairIndex::LastUnused)
616                .unwrap()
617                .x_only_public_key()
618                .0,
619            first_receive_pk
620        );
621    }
622}