1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
8use codec::Decode;
9use sp_core::crypto::{AccountId32, Ss58AddressFormat, Ss58Codec};
10use sp_core::hashing::blake2_256;
11use subtensor_hashed::Descriptor;
12
13#[cfg(test)]
14use super::CRYPTO_MLDSA65_FIXED;
15use super::{crypto_err, is_fixed_pq_crypto, Keypair};
16use crate::error::CoreError;
17
18const PREFIX: &str = "tao_";
19const DOMAIN: &[u8] = b"bittensor/hashed/v1/receiving";
20const DESCRIPTOR_LEN: usize = 34;
21const PAYLOAD_LEN: usize = DESCRIPTOR_LEN;
22const CHECKSUM_LEN: usize = 8;
23const BODY_LEN: usize = PAYLOAD_LEN + CHECKSUM_LEN;
24const ADDRESS_LEN: usize = 60;
25const MLDSA_LEGACY_PREFIX: &str = "bth2_";
26const MLDSA_ENCODED_BODY_LEN: usize = 99;
27
28fn validate_descriptor(bytes: &[u8]) -> Result<(), CoreError> {
29 if bytes.len() != DESCRIPTOR_LEN {
30 return Err(crypto_err(
31 "hashed receiving descriptor must be exactly 34 bytes",
32 ));
33 }
34 let mut input = bytes;
35 let descriptor = Descriptor::decode(&mut input)
36 .map_err(|_| crypto_err("invalid or unsupported hashed receiving descriptor"))?;
37 if !input.is_empty() || !descriptor.is_supported() {
38 return Err(crypto_err(
39 "invalid or unsupported hashed receiving descriptor",
40 ));
41 }
42 Ok(())
43}
44
45fn checksum(payload: &[u8]) -> [u8; CHECKSUM_LEN] {
46 let mut input = DOMAIN.to_vec();
47 input.extend_from_slice(payload);
48 let hash = blake2_256(&input);
49 let mut checksum = [0; CHECKSUM_LEN];
50 checksum.copy_from_slice(&hash[..CHECKSUM_LEN]);
51 checksum
52}
53
54pub fn encode_hashed_receiving_address(descriptor: &[u8]) -> Result<String, CoreError> {
57 if descriptor.first() == Some(&subtensor_hashed::mldsa::VERSION) {
58 return encode_mldsa_receiving_address(descriptor);
59 }
60 validate_descriptor(descriptor)?;
61 let mut body = Vec::with_capacity(BODY_LEN);
62 body.extend_from_slice(descriptor);
63 body.extend_from_slice(&checksum(&body));
64 Ok(format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(body)))
65}
66
67pub fn decode_hashed_receiving_address(address: &str) -> Result<Vec<u8>, CoreError> {
69 if (address.starts_with(PREFIX) && address.len() == PREFIX.len() + MLDSA_ENCODED_BODY_LEN)
72 || address.starts_with(MLDSA_LEGACY_PREFIX)
73 {
74 return decode_mldsa_receiving_address(address);
75 }
76 if address.len() != ADDRESS_LEN {
77 return Err(crypto_err(
78 "hashed receiving address must be exactly 60 characters",
79 ));
80 }
81 let encoded = address
82 .strip_prefix(PREFIX)
83 .ok_or_else(|| crypto_err("unsupported hashed receiving address prefix"))?;
84 let body = URL_SAFE_NO_PAD
85 .decode(encoded)
86 .map_err(|_| crypto_err("invalid hashed receiving address encoding"))?;
87 if body.len() != BODY_LEN || URL_SAFE_NO_PAD.encode(&body) != encoded {
88 return Err(crypto_err("noncanonical hashed receiving address"));
89 }
90 if body[PAYLOAD_LEN..] != checksum(&body[..PAYLOAD_LEN]) {
91 return Err(crypto_err("hashed receiving address checksum mismatch"));
92 }
93 let descriptor = &body[..PAYLOAD_LEN];
94 validate_descriptor(descriptor)?;
95 Ok(descriptor.to_vec())
96}
97
98impl Keypair {
99 pub fn hashed_receiving_address(&self) -> Result<String, CoreError> {
102 if is_fixed_pq_crypto(self.crypto_type()) {
103 return Ok(self.ss58_address());
104 }
105 encode_hashed_receiving_address(&self.authorization_descriptor()?)
106 }
107}
108
109fn mldsa_checksum(descriptor: &[u8]) -> [u8; CHECKSUM_LEN] {
110 let mut bytes = b"bittensor/ms/v2/receiving".to_vec();
111 bytes.extend_from_slice(descriptor);
112 let mut checksum = [0; CHECKSUM_LEN];
113 checksum.copy_from_slice(&blake2_256(&bytes)[..CHECKSUM_LEN]);
114 checksum
115}
116
117pub fn encode_mldsa_receiving_address(descriptor: &[u8]) -> Result<String, CoreError> {
121 let parsed = subtensor_hashed::mldsa::Descriptor::from_bytes(descriptor)
122 .map_err(|_| crypto_err("invalid post-quantum receiving descriptor"))?;
123 if parsed.is_stateless() {
124 return Ok(
125 AccountId32::new(subtensor_hashed::mldsa::account_id(&parsed))
126 .to_ss58check_with_version(Ss58AddressFormat::custom(super::DEFAULT_SS58_FORMAT)),
127 );
128 }
129 let mut body = descriptor.to_vec();
130 body.extend_from_slice(&mldsa_checksum(descriptor));
131 Ok(format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(body)))
132}
133
134pub fn decode_mldsa_receiving_address(address: &str) -> Result<Vec<u8>, CoreError> {
135 let encoded = address
136 .strip_prefix(PREFIX)
137 .or_else(|| address.strip_prefix(MLDSA_LEGACY_PREFIX))
138 .ok_or_else(|| crypto_err("unsupported post-quantum receiving prefix"))?;
139 if encoded.len() != MLDSA_ENCODED_BODY_LEN {
140 return Err(crypto_err(
141 "post-quantum receiving payload must be exactly 99 characters",
142 ));
143 }
144 let body = URL_SAFE_NO_PAD
145 .decode(encoded)
146 .map_err(|_| crypto_err("invalid post-quantum receiving encoding"))?;
147 if body.len() != 74 || URL_SAFE_NO_PAD.encode(&body) != encoded {
148 return Err(crypto_err("noncanonical post-quantum receiving address"));
149 }
150 let descriptor = &body[..66];
151 if body[66..] != mldsa_checksum(descriptor) {
152 return Err(crypto_err("post-quantum receiving checksum mismatch"));
153 }
154 let parsed = subtensor_hashed::mldsa::Descriptor::from_bytes(descriptor)
155 .map_err(|_| crypto_err("invalid post-quantum receiving descriptor"))?;
156 if parsed.is_stateless() {
157 return Err(crypto_err("unsupported post-quantum receiving descriptor"));
158 }
159 Ok(descriptor.to_vec())
160}
161
162#[cfg(test)]
163mod tests {
164 #![allow(clippy::unwrap_used, clippy::expect_used)]
165
166 use codec::Encode;
167 use subtensor_hashed::{account_id, Scheme, VERSION};
168
169 use super::*;
170 use crate::keys::{CRYPTO_HASHED, CRYPTO_SR25519, DEFAULT_SS58_FORMAT};
171
172 fn descriptor() -> Vec<u8> {
173 Descriptor {
174 version: VERSION,
175 scheme: Scheme::Sr25519,
176 initial_commitment: [7; 32],
177 }
178 .encode()
179 }
180
181 fn envelope(payload: &[u8]) -> String {
185 let mut body = payload.to_vec();
186 body.extend_from_slice(&checksum(payload));
187 format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(body))
188 }
189
190 #[test]
191 fn fixed_vector_matches_independent_blake2b_and_base64_implementation() {
192 const EXPECTED: &str = "tao_AQEHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHB2_GKIFP7N0l";
195 let descriptor = descriptor();
196 let address = encode_hashed_receiving_address(&descriptor).unwrap();
197 assert_eq!(address, EXPECTED);
198 assert_eq!(address.len(), ADDRESS_LEN);
199 let decoded_descriptor = decode_hashed_receiving_address(&address).unwrap();
200 assert_eq!(decoded_descriptor, descriptor);
201 let public =
202 Keypair::from_hashed_descriptor(&decoded_descriptor, DEFAULT_SS58_FORMAT).unwrap();
203 let parsed = Descriptor::decode(&mut &descriptor[..]).unwrap();
204 assert_eq!(public.public_key_bytes(), account_id(&parsed));
205 }
206
207 #[test]
208 fn fixed_mldsa_has_only_an_ordinary_receiving_address() {
209 let key = Keypair::from_seed(&[7; 32], CRYPTO_MLDSA65_FIXED).unwrap();
210 let descriptor = key.mldsa_descriptor().unwrap();
211 assert_eq!(
212 encode_mldsa_receiving_address(&descriptor).unwrap(),
213 key.ss58_address()
214 );
215 let mut body = descriptor.clone();
216 body.extend_from_slice(&mldsa_checksum(&descriptor));
217 for prefix in [PREFIX, MLDSA_LEGACY_PREFIX] {
218 let raw = format!("{prefix}{}", URL_SAFE_NO_PAD.encode(&body));
219 assert!(decode_mldsa_receiving_address(&raw).is_err());
220 }
221 let public = Keypair::from_mldsa_descriptor(&descriptor, 0).unwrap();
222 assert_eq!(
223 public.hashed_receiving_address().unwrap(),
224 public.ss58_address()
225 );
226 assert_ne!(public.ss58_address(), key.ss58_address());
227 }
228
229 #[test]
230 fn shared_prefix_preserves_each_rotating_scheme_and_account() {
231 for crypto_type in [
232 CRYPTO_HASHED,
233 crate::keys::CRYPTO_HASHED_ED25519,
234 crate::keys::CRYPTO_MLDSA65,
235 ] {
236 let key = Keypair::from_seed(&[17; 32], crypto_type).unwrap();
237 let descriptor = key.authorization_descriptor().unwrap();
238 let address = key.hashed_receiving_address().unwrap();
239 assert!(address.starts_with(PREFIX));
240 assert_eq!(
241 decode_hashed_receiving_address(&address).unwrap(),
242 descriptor
243 );
244 let restored =
245 Keypair::from_hashed_descriptor(&descriptor, DEFAULT_SS58_FORMAT).unwrap();
246 assert_eq!(restored.crypto_type(), crypto_type);
247 assert_eq!(restored.ss58_address(), key.ss58_address());
248 assert_eq!(restored.hashed_receiving_address().unwrap(), address);
249 if crypto_type != crate::keys::CRYPTO_MLDSA65 {
250 assert_eq!(address.len(), 60);
251 assert!(decode_mldsa_receiving_address(&address).is_err());
252 } else {
253 assert_eq!(address.len(), 103);
254 }
255 }
256 }
257
258 #[test]
259 fn every_payload_and_checksum_byte_is_covered() {
260 let address = encode_hashed_receiving_address(&descriptor()).unwrap();
261 let body = URL_SAFE_NO_PAD
262 .decode(address.strip_prefix(PREFIX).unwrap())
263 .unwrap();
264 for index in 0..BODY_LEN {
265 let mut damaged = body.clone();
266 damaged[index] ^= 1;
267 let encoded = format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(damaged));
268 assert!(
269 decode_hashed_receiving_address(&encoded).is_err(),
270 "byte {index} was not protected"
271 );
272 }
273 }
274
275 #[test]
276 fn addresses_are_stable_across_networks_and_rotation() {
277 for crypto_type in [CRYPTO_HASHED, crate::keys::CRYPTO_HASHED_ED25519] {
278 let key = Keypair::from_seed(&[17; 32], crypto_type).unwrap();
279 let address = key.hashed_receiving_address().unwrap();
280 assert_eq!(
281 key.at_generation(90_001)
282 .unwrap()
283 .hashed_receiving_address()
284 .unwrap(),
285 address
286 );
287 assert_eq!(
288 key.public_only()
289 .unwrap()
290 .hashed_receiving_address()
291 .unwrap(),
292 address
293 );
294 assert_eq!(
295 decode_hashed_receiving_address(&address).unwrap(),
296 key.hashed_descriptor().unwrap()
297 );
298 }
299 let classical = Keypair::from_seed(&[17; 32], CRYPTO_SR25519).unwrap();
300 assert!(classical.hashed_receiving_address().is_err());
301 }
302
303 #[test]
304 fn unsupported_descriptors_are_rejected_even_with_a_valid_checksum() {
305 for (offset, replacement) in [(0, 0), (0, 2), (1, 0), (1, 4), (1, 255)] {
306 let mut invalid = descriptor();
307 invalid[offset] = replacement;
308 assert!(encode_hashed_receiving_address(&invalid).is_err());
309 assert!(decode_hashed_receiving_address(&envelope(&invalid)).is_err());
310 }
311 let empty_commitment = Descriptor {
312 version: VERSION,
313 scheme: Scheme::Sr25519,
314 initial_commitment: [0; 32],
315 }
316 .encode();
317 assert!(encode_hashed_receiving_address(&empty_commitment).is_err());
318 assert!(decode_hashed_receiving_address(&envelope(&empty_commitment)).is_err());
319 for invalid in [vec![], vec![1; 33], vec![1; 35]] {
320 assert!(encode_hashed_receiving_address(&invalid).is_err());
321 }
322 }
323
324 #[test]
325 fn alternate_spellings_padding_and_trailing_data_are_rejected() {
326 let address = encode_hashed_receiving_address(&descriptor()).unwrap();
327 for invalid in [
328 address.replacen("tao_", "bth1_", 1),
329 address.replacen("tao_", "bth2_", 1),
330 address.replacen("tao_", "TAO_", 1),
331 address.replacen("tao_", "ss58_", 1),
332 format!(" {address}"),
333 format!("{address}\n"),
334 format!("{address}="),
335 address[..ADDRESS_LEN - 1].to_string(),
336 ] {
337 assert!(decode_hashed_receiving_address(&invalid).is_err());
338 }
339 for replacement in [b' ', b'\n', b'=', b'+', b'/'] {
340 let mut invalid = address.as_bytes().to_vec();
341 invalid[20] = replacement;
342 assert!(
343 decode_hashed_receiving_address(std::str::from_utf8(&invalid).unwrap()).is_err()
344 );
345 }
346 }
347
348 #[test]
349 fn unused_network_field_is_not_accepted() {
350 for network in [[0; 32], [17; 32]] {
351 let mut payload = network.to_vec();
352 payload.extend_from_slice(&descriptor());
353 assert!(decode_hashed_receiving_address(&envelope(&payload)).is_err());
354 }
355 }
356}