penumbra_sdk_keys/keys/
spend.rs

1use bip32::XPrv;
2use std::convert::TryFrom;
3
4use hmac::Hmac;
5use pbkdf2::pbkdf2;
6use penumbra_sdk_proto::{penumbra::core::keys::v1 as pb, DomainType};
7use serde::{Deserialize, Serialize};
8
9use super::{
10    bip44::Bip44Path,
11    seed_phrase::{SeedPhrase, NUM_PBKDF2_ROUNDS},
12    FullViewingKey, IncomingViewingKey, NullifierKey, OutgoingViewingKey,
13};
14use crate::{
15    prf,
16    rdsa::{SigningKey, SpendAuth},
17};
18
19pub const SPENDKEY_LEN_BYTES: usize = 32;
20
21/// A refinement type for a `[u8; 32]` indicating that it stores the
22/// bytes of a spend key.
23///
24/// TODO(hdevalence): In the future, we should hide the SpendKeyBytes
25/// and force everything to use the proto format / bech32 serialization.
26/// But we can't do this now, because we need it to support existing wallets.
27#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
28pub struct SpendKeyBytes(pub [u8; SPENDKEY_LEN_BYTES]);
29
30/// A key representing a single spending authority.
31#[derive(Debug, Clone, Deserialize, Serialize)]
32#[serde(try_from = "pb::SpendKey", into = "pb::SpendKey")]
33pub struct SpendKey {
34    seed: SpendKeyBytes,
35    ask: SigningKey<SpendAuth>,
36    fvk: FullViewingKey,
37}
38
39impl PartialEq for SpendKey {
40    fn eq(&self, other: &Self) -> bool {
41        self.seed == other.seed
42    }
43}
44
45impl Eq for SpendKey {}
46
47impl DomainType for SpendKey {
48    type Proto = pb::SpendKey;
49}
50
51impl TryFrom<pb::SpendKey> for SpendKey {
52    type Error = anyhow::Error;
53
54    fn try_from(msg: pb::SpendKey) -> Result<Self, Self::Error> {
55        Ok(SpendKeyBytes::try_from(msg.inner.as_slice())?.into())
56    }
57}
58
59impl From<SpendKey> for pb::SpendKey {
60    fn from(msg: SpendKey) -> Self {
61        Self {
62            inner: msg.to_bytes().0.to_vec(),
63        }
64    }
65}
66
67impl From<SpendKeyBytes> for SpendKey {
68    fn from(seed: SpendKeyBytes) -> Self {
69        let ask = SigningKey::new_from_field(prf::expand_ff(b"Penumbra_ExpndSd", &seed.0, &[0; 1]));
70        let nk = NullifierKey(prf::expand_ff(b"Penumbra_ExpndSd", &seed.0, &[1; 1]));
71        let fvk = FullViewingKey::from_components(ask.into(), nk);
72
73        Self { seed, ask, fvk }
74    }
75}
76
77impl SpendKey {
78    /// Get the [`SpendKeyBytes`] this [`SpendKey`] was derived from.
79    ///
80    /// This is useful for serialization.
81    pub fn to_bytes(&self) -> SpendKeyBytes {
82        self.seed.clone()
83    }
84
85    /// Deterministically generate a [`SpendKey`] from a [`SeedPhrase`].
86    ///
87    /// The choice of KDF (PBKDF2), iteration count, and PRF (HMAC-SHA512) are specified
88    /// in [`BIP39`]. The salt is specified in BIP39 as the string "mnemonic" plus an optional
89    /// passphrase, which we set to an index. This allows us to derive multiple spend
90    /// authorities from a single seed phrase.
91    ///
92    /// [`BIP39`]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
93    pub fn from_seed_phrase_bip39(seed_phrase: SeedPhrase, index: u64) -> Self {
94        let password = format!("{seed_phrase}");
95        let salt = format!("mnemonic{index}");
96        let mut spend_seed_bytes = [0u8; 32];
97        pbkdf2::<Hmac<sha2::Sha512>>(
98            password.as_bytes(),
99            salt.as_bytes(),
100            NUM_PBKDF2_ROUNDS,
101            &mut spend_seed_bytes,
102        )
103        .expect("seed phrase hash always succeeds");
104        SpendKeyBytes(spend_seed_bytes).into()
105    }
106
107    pub fn from_seed_phrase_bip44(seed_phrase: SeedPhrase, path: &Bip44Path) -> Self {
108        let password = format!("{seed_phrase}");
109        let salt = "mnemonic";
110        let mut seed_bytes = [0u8; 64];
111        pbkdf2::<Hmac<sha2::Sha512>>(
112            password.as_bytes(),
113            salt.as_bytes(),
114            NUM_PBKDF2_ROUNDS,
115            &mut seed_bytes,
116        )
117        .expect("seed phrase hash always succeeds");
118
119        // Now we derive the child keys from the BIP44 path. There are up five levels
120        // in the BIP44 path: purpose, coin type, account, change, and address index.
121        let child_key = XPrv::derive_from_path(
122            &seed_bytes[..],
123            &path.path().parse().expect("valid BIP44 path"),
124        )
125        .expect("can derive child key");
126        let child_key_bytes = child_key.to_bytes();
127
128        SpendKeyBytes(child_key_bytes).into()
129    }
130
131    // XXX how many of these do we need? leave them for now
132    // but don't document until design is more settled
133
134    pub fn spend_auth_key(&self) -> &SigningKey<SpendAuth> {
135        &self.ask
136    }
137
138    pub fn full_viewing_key(&self) -> &FullViewingKey {
139        &self.fvk
140    }
141
142    pub fn nullifier_key(&self) -> &NullifierKey {
143        self.fvk.nullifier_key()
144    }
145
146    pub fn outgoing_viewing_key(&self) -> &OutgoingViewingKey {
147        self.fvk.outgoing()
148    }
149
150    pub fn incoming_viewing_key(&self) -> &IncomingViewingKey {
151        self.fvk.incoming()
152    }
153}
154
155impl From<[u8; SPENDKEY_LEN_BYTES]> for SpendKeyBytes {
156    fn from(bytes: [u8; SPENDKEY_LEN_BYTES]) -> Self {
157        Self(bytes)
158    }
159}
160
161impl TryFrom<&[u8]> for SpendKeyBytes {
162    type Error = anyhow::Error;
163    fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
164        if slice.len() != SPENDKEY_LEN_BYTES {
165            anyhow::bail!("spendseed must be 32 bytes, got {:?}", slice.len());
166        }
167
168        let mut bytes = [0u8; SPENDKEY_LEN_BYTES];
169        bytes.copy_from_slice(&slice[0..32]);
170        Ok(SpendKeyBytes(bytes))
171    }
172}
173
174impl std::fmt::Display for SpendKey {
175    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
176        use penumbra_sdk_proto::serializers::bech32str;
177        let proto = pb::SpendKey::from(self.clone());
178        f.write_str(&bech32str::encode(
179            &proto.inner,
180            bech32str::spend_key::BECH32_PREFIX,
181            bech32str::Bech32m,
182        ))
183    }
184}
185
186impl std::str::FromStr for SpendKey {
187    type Err = anyhow::Error;
188
189    fn from_str(s: &str) -> Result<Self, Self::Err> {
190        use penumbra_sdk_proto::serializers::bech32str;
191        pb::SpendKey {
192            inner: bech32str::decode(s, bech32str::spend_key::BECH32_PREFIX, bech32str::Bech32m)?,
193        }
194        .try_into()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use std::str::FromStr;
201
202    use super::*;
203
204    #[test]
205    fn bip44_test_ledger() {
206        // Test account
207        let seed = SeedPhrase::from_str("comfort ten front cycle churn burger oak absent rice ice urge result art couple benefit cabbage frequent obscure hurry trick segment cool job debate").unwrap();
208
209        let expected_bytes =
210            hex::decode("1b8113fad04f5db00e6acf541949950f85eca3e02e70254838b750b42a2caa51")
211                .expect("valid");
212        let expected_spendkey = SpendKeyBytes(expected_bytes.try_into().expect("fits in 32 bytes"));
213
214        let derivation_path = Bip44Path::new(0);
215        let software_spendkey = SpendKey::from_seed_phrase_bip44(seed, &derivation_path);
216
217        assert_eq!(software_spendkey.to_bytes(), expected_spendkey);
218    }
219}