1use 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 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 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}