Skip to main content

bittensor_core/keys/
falcon.rs

1//! Falcon backend dispatch for the shared descriptor-based wallet implementation.
2//! Recovery domains and the seeded backend are protocol, not implementation details.
3use codec::Encode;
4use subtensor_hashed::{falcon, mldsa};
5
6use super::{crypto_err, Keypair};
7use crate::error::CoreError;
8
9pub(super) fn public_key(profile: u8, seed: &[u8; 32]) -> Result<Vec<u8>, CoreError> {
10    if falcon::is_account_profile(profile) {
11        falcon::public_key_from_seed(profile, seed).map_err(crypto_err)
12    } else {
13        mldsa::public_key_from_seed(seed)
14            .map(|key| key.to_vec())
15            .map_err(crypto_err)
16    }
17}
18
19pub(super) fn commitment(profile: u8, public: &[u8]) -> Result<[u8; 64], CoreError> {
20    if falcon::is_account_profile(profile) {
21        falcon::commitment(profile, public).map_err(crypto_err)
22    } else {
23        let public = public
24            .try_into()
25            .map_err(|_| crypto_err("invalid ML-DSA public key length"))?;
26        Ok(mldsa::commitment(public))
27    }
28}
29
30impl Keypair {
31    /// Active transaction public key, with the width selected by the wallet profile.
32    pub fn pq_public_key(&self) -> Result<Vec<u8>, CoreError> {
33        let key = self.mldsa()?;
34        public_key(
35            key.descriptor.algorithm,
36            &*key.seed_at(super::ROLE_TRANSACTION, key.generation)?,
37        )
38    }
39
40    pub(super) fn sign_falcon(
41        &self,
42        payload: &[u8],
43        random: &[u8; 32],
44    ) -> Result<Vec<u8>, CoreError> {
45        let key = self.mldsa()?;
46        let seed = key.seed_at(super::ROLE_TRANSACTION, key.generation)?;
47        let header = falcon::Header {
48            account: key.account_id(),
49            version: falcon::VERSION,
50            algorithm: key.descriptor.algorithm,
51            generation: key.generation,
52            public_key: public_key(key.descriptor.algorithm, &seed)?,
53            next_commitment: self.mldsa_next_commitment()?,
54        };
55        Ok(header
56            .sign(&seed, payload, random)
57            .map_err(crypto_err)?
58            .encode())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    #![allow(clippy::unwrap_used, clippy::expect_used)]
65    use super::*;
66    use crate::keys::{
67        account_crypto_type, decode_hashed_receiving_address, is_fixed_pq_crypto,
68        verify_mldsa_authorization, CRYPTO_FALCON1024, CRYPTO_FALCON1024_HASHED, CRYPTO_FALCON512,
69        CRYPTO_FALCON512_HASHED,
70    };
71
72    const PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
73    const MODES: [u8; 4] = [
74        CRYPTO_FALCON512,
75        CRYPTO_FALCON512_HASHED,
76        CRYPTO_FALCON1024,
77        CRYPTO_FALCON1024_HASHED,
78    ];
79
80    #[test]
81    fn falcon_profiles_restore_sign_and_keep_permanent_addresses() {
82        let mut identities = Vec::new();
83        for crypto in MODES {
84            let key = Keypair::from_mnemonic(PHRASE, crypto, None).unwrap();
85            assert!(!identities.contains(&key.public_key_bytes()));
86            identities.push(key.public_key_bytes());
87            let fixed = is_fixed_pq_crypto(crypto);
88            let generation = if fixed { 0 } else { 7 };
89            let active = key.at_generation(generation).unwrap();
90            assert_eq!(active.public_key_bytes(), key.public_key_bytes());
91            assert_eq!(
92                key.account_type(),
93                if fixed { "standard" } else { "hashed" }
94            );
95            assert_eq!(
96                account_crypto_type(crypto, Some(key.account_type())).unwrap(),
97                crypto
98            );
99            let restored = Keypair::from_seed(key.seed_bytes().unwrap(), crypto)
100                .unwrap()
101                .at_generation(generation)
102                .unwrap();
103            assert_eq!(
104                restored.authorization_descriptor().unwrap(),
105                active.authorization_descriptor().unwrap()
106            );
107            assert_eq!(
108                restored.authorization_current_commitment().unwrap(),
109                active.authorization_current_commitment().unwrap()
110            );
111            if fixed {
112                assert_eq!(key.hashed_receiving_address().unwrap(), key.ss58_address());
113                assert!(mldsa::rejects_legacy_public_keys(&key.public_key_bytes()));
114                assert!(key.at_generation(1).is_err());
115                assert_eq!(
116                    key.mldsa_current_commitment().unwrap(),
117                    key.mldsa_next_commitment().unwrap()
118                );
119            } else {
120                assert_eq!(
121                    decode_hashed_receiving_address(&key.hashed_receiving_address().unwrap())
122                        .unwrap(),
123                    key.authorization_descriptor().unwrap()
124                );
125                assert_eq!(
126                    active.hashed_receiving_address().unwrap(),
127                    key.hashed_receiving_address().unwrap()
128                );
129                assert_ne!(
130                    active.mldsa_current_commitment().unwrap(),
131                    active.mldsa_next_commitment().unwrap()
132                );
133            }
134            let payload = b"chain/call/nonce/era/metadata";
135            let proof = active.sign_mldsa(payload).unwrap();
136            assert_eq!(
137                proof.len(),
138                falcon::proof_len(active.mldsa_profile().unwrap()).unwrap()
139            );
140            let verify = |bytes: &[u8], message: &[u8], gen| {
141                verify_mldsa_authorization(
142                    bytes,
143                    message,
144                    &active.mldsa_descriptor().unwrap(),
145                    gen,
146                    &active.mldsa_current_commitment().unwrap(),
147                )
148                .unwrap()
149            };
150            assert!(verify(&proof, payload, generation));
151            assert!(!verify(&proof, b"different call", generation));
152            assert!(!verify(&proof, payload, generation + 1));
153            let mut wrong_profile = proof.clone();
154            wrong_profile[33] ^= 1;
155            assert!(!verify(&wrong_profile, payload, generation));
156            let mut trailing = proof.clone();
157            trailing.push(0);
158            assert!(!verify(&trailing, payload, generation));
159            assert!(key.public_only().unwrap().sign_mldsa(payload).is_err());
160            assert!(Keypair::from_uri("//Alice", crypto).is_err());
161            assert!(Keypair::from_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", crypto, None).is_err());
162        }
163    }
164
165    #[cfg(feature = "host")]
166    #[test]
167    fn falcon_keyfiles_bind_crypto_mode_descriptor_and_seed() {
168        use crate::keyfiles::{
169            deserialize_keypair_from_keyfile_data, serialized_keypair_to_keyfile_data,
170        };
171        for crypto in MODES {
172            let key = Keypair::from_seed(&[31; 32], crypto).unwrap();
173            for wallet in [key.public_only().unwrap(), key] {
174                let encoded = serialized_keypair_to_keyfile_data(&wallet).unwrap();
175                let restored = deserialize_keypair_from_keyfile_data(&encoded).unwrap();
176                assert_eq!(wallet.public_key_bytes(), restored.public_key_bytes());
177                assert_eq!(wallet.crypto_type(), restored.crypto_type());
178                let mut json: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
179                json["cryptoType"] = serde_json::json!(crypto.to_string());
180                assert!(
181                    deserialize_keypair_from_keyfile_data(&serde_json::to_vec(&json).unwrap())
182                        .is_ok()
183                );
184                json["cryptoType"] = serde_json::json!(if crypto == CRYPTO_FALCON512 {
185                    CRYPTO_FALCON512_HASHED
186                } else {
187                    CRYPTO_FALCON512
188                });
189                assert!(
190                    deserialize_keypair_from_keyfile_data(&serde_json::to_vec(&json).unwrap())
191                        .is_err()
192                );
193            }
194        }
195    }
196
197    #[test]
198    fn falcon_recovery_vectors() {
199        // Immutable recovery contract for the public all-zero-entropy BIP39 fixture.
200        // These literals deliberately survive backend upgrades and optimizations.
201        for (crypto, descriptor, address, current, next) in [
202            (9, "0207f6aa11c692344eef51a0aa4b3db0c1f1eec8e8d1693a39572ff1a155deb380fc2002ebf57dcb5b1ca239413440a73214e85027efa38fcef4597b5743da793a21", "5E4K2QqtgunUxVAq6H5aXPeVw3TSmRECei3voYM5zVNbpSYS", "f6aa11c692344eef51a0aa4b3db0c1f1eec8e8d1693a39572ff1a155deb380fc2002ebf57dcb5b1ca239413440a73214e85027efa38fcef4597b5743da793a21", "f6aa11c692344eef51a0aa4b3db0c1f1eec8e8d1693a39572ff1a155deb380fc2002ebf57dcb5b1ca239413440a73214e85027efa38fcef4597b5743da793a21"),
203            (10, "02065c7137cd27990fee7911e060de0483e2bbb8932cc0213f4cbb51f1dfd4be07b06fa137850c079f83b1dcfbb0d6652395da0dd1f1243bff58f571a4f0c445fec7", "5DvQ46TL3ozmzCALgWEeZASZMqBs7YD53dRHBB4a5bbzjRSJ", "990e37be1e3dbec2d835758e5f2bb275e35a8c1a38bbb5a3aa612579dd5abe1f85a7393d3f29d94b0d17e5e996490eaf3b54c14070ea056b0533284cb75a1cf0", "9500a972b5e8fc4f32d81ba606034d26a57d147cbf1642808f478aeb179e5e043f1dd07be3ff1b53014c7da9ac840f9a464a76540224abe7c635f2685fa3213c"),
204            (11, "0209193b47f776273b8a416ee35ff87560011ccd612653de9eb66c536bb36f961502ffb92256cf74422768dd08fd919fd1ac1202fb9ccbb79095d15e569620b5cdf7", "5GtkHzJ1aq8hrAzq3baviawc8tr8fPhwCrbdMiyLUo4R8kkG", "193b47f776273b8a416ee35ff87560011ccd612653de9eb66c536bb36f961502ffb92256cf74422768dd08fd919fd1ac1202fb9ccbb79095d15e569620b5cdf7", "193b47f776273b8a416ee35ff87560011ccd612653de9eb66c536bb36f961502ffb92256cf74422768dd08fd919fd1ac1202fb9ccbb79095d15e569620b5cdf7"),
205            (12, "0208175720b8870ecd47bbe45a266b0b46d9d51cc39bbcf966f87b19d3bc6c1d62caae2e78352d74b466a48d36660fda4dd44f83fe86377ce3ebc8d6ad58fb04e8b9", "5DG14D3g6n5gbhBvaUysEASNGzHKRhZc4748BzsvQRbbxcSy", "a451e7283265e526714c8c115de3e86ee51a8bc94819ed3e03726a5f03e48aa0e15d075deab38e06b3e58b64aaba36dd5a973b675f5bbf25a16bcab6a41d5c51", "9e8c3e68c420d8d70f361bbcf9a268657d7d02ffb036e0799d51afbe61443ca66a477011ef81be6ce2608a779e0567bf9dafca04bbbfdba42d0b3d6aa93e272b"),
206        ] {
207            let key = Keypair::from_mnemonic(PHRASE, crypto, None).unwrap();
208            let active = key.at_generation(if is_fixed_pq_crypto(crypto) { 0 } else { 7 }).unwrap();
209            assert_eq!(hex::encode(key.mldsa_descriptor().unwrap()), descriptor);
210            assert_eq!(key.ss58_address(), address);
211            assert_eq!(hex::encode(active.mldsa_current_commitment().unwrap()), current);
212            assert_eq!(hex::encode(active.mldsa_next_commitment().unwrap()), next);
213        }
214    }
215}