Skip to main content

bittensor_core/keys/
receiving.rs

1//! A self-contained, network-independent receiving address for hashed accounts.
2//!
3//! The descriptor reveals only the initial commitment, never a signing key.
4//! The checksum detects transcription errors; it does not authenticate a sender.
5//! The payload contains only the descriptor; registration is independent per chain.
6
7use 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
54/// Encode a public descriptor without a network field; addresses work on every chain.
55/// This receiving address does not change the account's existing identity.
56pub 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
67/// Decode a receiving address into its original public descriptor.
68pub fn decode_hashed_receiving_address(address: &str) -> Result<Vec<u8>, CoreError> {
69    // Both formats share a prefix. Bound the encoded size before decoding, then
70    // validate the format-specific descriptor and checksum below.
71    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    /// Return the ordinary SS58 address for fixed ML-DSA, or the receiving
100    /// descriptor for a registered account, without exposing a signing key.
101    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
117/// Registered profiles retain their descriptor-bearing receiving address.
118/// Registration-free profiles use ordinary SS58; their descriptor cannot be
119/// recovered from the address and is unnecessary for receiving funds.
120pub 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    /// Construct a checksum-valid envelope independently of descriptor checks.
182    /// Tests use this to distinguish rejecting unsupported protocol data from
183    /// merely detecting an unchanged checksum after accidental damage.
184    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        // Generated with Python hashlib.blake2b(digest_size=32) and
193        // base64.urlsafe_b64encode, rather than this codec.
194        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}