/__w/smoldot/smoldot/repo/lib/src/identity/ss58.rs
Line | Count | Source (jump to first uncovered line) |
1 | | // Smoldot |
2 | | // Copyright (C) 2023 Pierre Krieger |
3 | | // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 |
4 | | |
5 | | // This program is free software: you can redistribute it and/or modify |
6 | | // it under the terms of the GNU General Public License as published by |
7 | | // the Free Software Foundation, either version 3 of the License, or |
8 | | // (at your option) any later version. |
9 | | |
10 | | // This program is distributed in the hope that it will be useful, |
11 | | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | | // GNU General Public License for more details. |
14 | | |
15 | | // You should have received a copy of the GNU General Public License |
16 | | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | | |
18 | | use alloc::{string::String, vec::Vec}; |
19 | | use core::fmt; |
20 | | |
21 | | /// Decoded version of an SS58 address. |
22 | | #[derive(Debug, Copy, Clone, PartialEq, Eq)] |
23 | | pub struct Decoded<P> { |
24 | | /// Identifier indicating which chain is concerned. |
25 | | /// |
26 | | /// The mapping between chains and this prefix can be found in this central registry: |
27 | | /// <https://github.com/paritytech/ss58-registry/blob/main/ss58-registry.json>. |
28 | | pub chain_prefix: ChainPrefix, |
29 | | |
30 | | /// Public key of the account. |
31 | | pub public_key: P, |
32 | | } |
33 | | |
34 | | /// Identifier indicating which chain is concerned. |
35 | | /// |
36 | | /// The mapping between chains and this prefix can be found in this central registry: |
37 | | /// <https://github.com/paritytech/ss58-registry/blob/main/ss58-registry.json>. |
38 | | /// |
39 | | /// This prefix is a 14 bits unsigned integer. |
40 | | // |
41 | | // Implementation note: the `u16` is guaranteed to be only up to 14 bits long. The upper two bits |
42 | | // are always 0. |
43 | | #[derive(Copy, Clone, PartialEq, Eq)] |
44 | | pub struct ChainPrefix(u16); |
45 | | |
46 | | impl fmt::Debug for ChainPrefix { |
47 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
48 | 0 | fmt::Debug::fmt(&self.0, f) |
49 | 0 | } Unexecuted instantiation: _RNvXNtNtCsN16ciHI6Qf_7smoldot8identity4ss58NtB2_11ChainPrefixNtNtCsaYZPK01V26L_4core3fmt5Debug3fmt Unexecuted instantiation: _RNvXNtNtCseuYC0Zibziv_7smoldot8identity4ss58NtB2_11ChainPrefixNtNtCsaYZPK01V26L_4core3fmt5Debug3fmt |
50 | | } |
51 | | |
52 | | impl TryFrom<u16> for ChainPrefix { |
53 | | type Error = PrefixTooLargeError; |
54 | | |
55 | 0 | fn try_from(prefix: u16) -> Result<Self, Self::Error> { |
56 | 0 | if (prefix >> 14) == 0 { |
57 | 0 | Ok(ChainPrefix(prefix)) |
58 | | } else { |
59 | 0 | Err(PrefixTooLargeError()) |
60 | | } |
61 | 0 | } Unexecuted instantiation: _RNvXs_NtNtCsN16ciHI6Qf_7smoldot8identity4ss58NtB4_11ChainPrefixINtNtCsaYZPK01V26L_4core7convert7TryFromtE8try_from Unexecuted instantiation: _RNvXs_NtNtCseuYC0Zibziv_7smoldot8identity4ss58NtB4_11ChainPrefixINtNtCsaYZPK01V26L_4core7convert7TryFromtE8try_from |
62 | | } |
63 | | |
64 | | /// Integer is too large to be a valid prefix |
65 | 0 | #[derive(Debug, Clone, derive_more::Display)] Unexecuted instantiation: _RNvXsf_NtNtCsN16ciHI6Qf_7smoldot8identity4ss58NtB5_19PrefixTooLargeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt Unexecuted instantiation: _RNvXsf_NtNtCseuYC0Zibziv_7smoldot8identity4ss58NtB5_19PrefixTooLargeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt |
66 | | pub struct PrefixTooLargeError(); |
67 | | |
68 | | impl From<u8> for ChainPrefix { |
69 | 0 | fn from(prefix: u8) -> ChainPrefix { |
70 | 0 | ChainPrefix(u16::from(prefix)) |
71 | 0 | } Unexecuted instantiation: _RNvXs0_NtNtCsN16ciHI6Qf_7smoldot8identity4ss58NtB5_11ChainPrefixINtNtCsaYZPK01V26L_4core7convert4FromhE4from Unexecuted instantiation: _RNvXs0_NtNtCseuYC0Zibziv_7smoldot8identity4ss58NtB5_11ChainPrefixINtNtCsaYZPK01V26L_4core7convert4FromhE4from |
72 | | } |
73 | | |
74 | | impl From<ChainPrefix> for u16 { |
75 | 2 | fn from(prefix: ChainPrefix) -> u16 { |
76 | 2 | prefix.0 |
77 | 2 | } _RNvXs1_NtNtCsN16ciHI6Qf_7smoldot8identity4ss58tINtNtCsaYZPK01V26L_4core7convert4FromNtB5_11ChainPrefixE4from Line | Count | Source | 75 | 2 | fn from(prefix: ChainPrefix) -> u16 { | 76 | 2 | prefix.0 | 77 | 2 | } |
Unexecuted instantiation: _RNvXs1_NtNtCseuYC0Zibziv_7smoldot8identity4ss58tINtNtCsaYZPK01V26L_4core7convert4FromNtB5_11ChainPrefixE4from |
78 | | } |
79 | | |
80 | | /// Turns a decoded SS58 address into a string. |
81 | 2 | pub fn encode(decoded: Decoded<impl AsRef<[u8]>>) -> String { |
82 | 2 | let prefix = decoded.chain_prefix.0; |
83 | 2 | let public_key = decoded.public_key.as_ref(); |
84 | 2 | |
85 | 2 | let mut bytes = Vec::with_capacity(2 + public_key.len() + 2); |
86 | 2 | |
87 | 2 | if prefix < 64 { |
88 | 1 | bytes.push(prefix as u8); |
89 | 1 | } else { |
90 | 1 | // This encoding is plain weird. |
91 | 1 | bytes.push(((prefix & 0b0000_0000_1111_1100) as u8) >> 2 | 0b01000000); |
92 | 1 | bytes.push(((prefix >> 8) as u8) | ((prefix & 0b0000_0000_0000_0011) as u8) << 6); |
93 | 1 | } |
94 | | |
95 | 2 | bytes.extend_from_slice(public_key); |
96 | 2 | |
97 | 2 | let checksum = calculate_checksum(&bytes); |
98 | 2 | bytes.extend_from_slice(&checksum); |
99 | 2 | |
100 | 2 | bs58::encode(&bytes).into_string() |
101 | 2 | } _RINvNtNtCsN16ciHI6Qf_7smoldot8identity4ss586encodeNtNvB2_6decode6AdjustEB6_ Line | Count | Source | 81 | 2 | pub fn encode(decoded: Decoded<impl AsRef<[u8]>>) -> String { | 82 | 2 | let prefix = decoded.chain_prefix.0; | 83 | 2 | let public_key = decoded.public_key.as_ref(); | 84 | 2 | | 85 | 2 | let mut bytes = Vec::with_capacity(2 + public_key.len() + 2); | 86 | 2 | | 87 | 2 | if prefix < 64 { | 88 | 1 | bytes.push(prefix as u8); | 89 | 1 | } else { | 90 | 1 | // This encoding is plain weird. | 91 | 1 | bytes.push(((prefix & 0b0000_0000_1111_1100) as u8) >> 2 | 0b01000000); | 92 | 1 | bytes.push(((prefix >> 8) as u8) | ((prefix & 0b0000_0000_0000_0011) as u8) << 6); | 93 | 1 | } | 94 | | | 95 | 2 | bytes.extend_from_slice(public_key); | 96 | 2 | | 97 | 2 | let checksum = calculate_checksum(&bytes); | 98 | 2 | bytes.extend_from_slice(&checksum); | 99 | 2 | | 100 | 2 | bs58::encode(&bytes).into_string() | 101 | 2 | } |
Unexecuted instantiation: _RINvNtNtCseuYC0Zibziv_7smoldot8identity4ss586encodepEB6_ |
102 | | |
103 | | /// Decodes an SS58 address from a string. |
104 | 2 | pub fn decode(encoded: &'_ str) -> Result<Decoded<impl AsRef<[u8]>>, DecodeError> { |
105 | 2 | let mut bytes = bs58::decode(encoded) |
106 | 2 | .into_vec() |
107 | 2 | .map_err(|err| DecodeError::InvalidBs58(Bs58DecodeError(err))0 )?0 ; Unexecuted instantiation: _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity4ss586decode0B7_ Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity4ss586decode0B7_ |
108 | | |
109 | 2 | if bytes.len() < 4 { |
110 | 0 | return Err(DecodeError::TooShort); |
111 | 2 | } |
112 | 2 | |
113 | 2 | // Verify the checksum. |
114 | 2 | let expected_checksum = calculate_checksum(&bytes[..bytes.len() - 2]); |
115 | 2 | if expected_checksum[..] != bytes[bytes.len() - 2..] { |
116 | 0 | return Err(DecodeError::InvalidChecksum); |
117 | 2 | } |
118 | 2 | bytes.truncate(bytes.len() - 2); |
119 | | |
120 | | // Grab and remove the prefix. |
121 | 2 | let (prefix_len, chain_prefix) = if bytes[0] < 64 { |
122 | 1 | (1, ChainPrefix(u16::from(bytes[0]))) |
123 | 1 | } else if bytes[0] < 128 { |
124 | 1 | let prefix = u16::from_be_bytes([bytes[1] & 0b00111111, (bytes[0] << 2) | (bytes[1] >> 6)]); |
125 | 1 | (2, ChainPrefix(prefix)) |
126 | | } else { |
127 | 0 | return Err(DecodeError::InvalidPrefix); |
128 | | }; |
129 | | |
130 | | // Rather than remove the prefix from the beginning of `bytes`, we adjust the `AsRef` |
131 | | // implementation to skip the prefix. |
132 | 2 | let public_key = { |
133 | 2 | struct Adjust(Vec<u8>, usize); |
134 | 2 | impl AsRef<[u8]> for Adjust { |
135 | 4 | fn as_ref(&self) -> &[u8] { |
136 | 4 | &self.0[self.1..] |
137 | 4 | } _RNvXNvNtNtCsN16ciHI6Qf_7smoldot8identity4ss586decodeNtB2_6AdjustINtNtCsaYZPK01V26L_4core7convert5AsRefShE6as_ref Line | Count | Source | 135 | 4 | fn as_ref(&self) -> &[u8] { | 136 | 4 | &self.0[self.1..] | 137 | 4 | } |
Unexecuted instantiation: _RNvXNvNtNtCseuYC0Zibziv_7smoldot8identity4ss586decodeNtB2_6AdjustINtNtCsaYZPK01V26L_4core7convert5AsRefShE6as_ref |
138 | 2 | } |
139 | 2 | Adjust(bytes, prefix_len) |
140 | 2 | }; |
141 | 2 | |
142 | 2 | Ok(Decoded { |
143 | 2 | chain_prefix, |
144 | 2 | public_key, |
145 | 2 | }) |
146 | 2 | } _RNvNtNtCsN16ciHI6Qf_7smoldot8identity4ss586decode Line | Count | Source | 104 | 2 | pub fn decode(encoded: &'_ str) -> Result<Decoded<impl AsRef<[u8]>>, DecodeError> { | 105 | 2 | let mut bytes = bs58::decode(encoded) | 106 | 2 | .into_vec() | 107 | 2 | .map_err(|err| DecodeError::InvalidBs58(Bs58DecodeError(err)))?0 ; | 108 | | | 109 | 2 | if bytes.len() < 4 { | 110 | 0 | return Err(DecodeError::TooShort); | 111 | 2 | } | 112 | 2 | | 113 | 2 | // Verify the checksum. | 114 | 2 | let expected_checksum = calculate_checksum(&bytes[..bytes.len() - 2]); | 115 | 2 | if expected_checksum[..] != bytes[bytes.len() - 2..] { | 116 | 0 | return Err(DecodeError::InvalidChecksum); | 117 | 2 | } | 118 | 2 | bytes.truncate(bytes.len() - 2); | 119 | | | 120 | | // Grab and remove the prefix. | 121 | 2 | let (prefix_len, chain_prefix) = if bytes[0] < 64 { | 122 | 1 | (1, ChainPrefix(u16::from(bytes[0]))) | 123 | 1 | } else if bytes[0] < 128 { | 124 | 1 | let prefix = u16::from_be_bytes([bytes[1] & 0b00111111, (bytes[0] << 2) | (bytes[1] >> 6)]); | 125 | 1 | (2, ChainPrefix(prefix)) | 126 | | } else { | 127 | 0 | return Err(DecodeError::InvalidPrefix); | 128 | | }; | 129 | | | 130 | | // Rather than remove the prefix from the beginning of `bytes`, we adjust the `AsRef` | 131 | | // implementation to skip the prefix. | 132 | 2 | let public_key = { | 133 | 2 | struct Adjust(Vec<u8>, usize); | 134 | 2 | impl AsRef<[u8]> for Adjust { | 135 | 2 | fn as_ref(&self) -> &[u8] { | 136 | 2 | &self.0[self.1..] | 137 | 2 | } | 138 | 2 | } | 139 | 2 | Adjust(bytes, prefix_len) | 140 | 2 | }; | 141 | 2 | | 142 | 2 | Ok(Decoded { | 143 | 2 | chain_prefix, | 144 | 2 | public_key, | 145 | 2 | }) | 146 | 2 | } |
Unexecuted instantiation: _RNvNtNtCseuYC0Zibziv_7smoldot8identity4ss586decode |
147 | | |
148 | | /// Error while decoding an SS58 address. |
149 | 0 | #[derive(Debug, Clone, derive_more::Display)] Unexecuted instantiation: _RNvXsi_NtNtCsN16ciHI6Qf_7smoldot8identity4ss58NtB5_11DecodeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt Unexecuted instantiation: _RNvXsi_NtNtCseuYC0Zibziv_7smoldot8identity4ss58NtB5_11DecodeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt |
150 | | pub enum DecodeError { |
151 | | /// SS58 is too short to possibly be valid. |
152 | | TooShort, |
153 | | /// Invalid SS58 prefix encoding. |
154 | | InvalidPrefix, |
155 | | /// Invalid BS58 format. |
156 | | #[display(fmt = "{_0}")] |
157 | | InvalidBs58(Bs58DecodeError), |
158 | | /// Calculated checksum doesn't match the one provided. |
159 | | InvalidChecksum, |
160 | | } |
161 | | |
162 | | /// Error when decoding Base58 encoding. |
163 | 0 | #[derive(Debug, Clone, derive_more::Display, derive_more::From)] Unexecuted instantiation: _RNvXsl_NtNtCsN16ciHI6Qf_7smoldot8identity4ss58NtB5_15Bs58DecodeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt Unexecuted instantiation: _RNvXsl_NtNtCseuYC0Zibziv_7smoldot8identity4ss58NtB5_15Bs58DecodeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt |
164 | | pub struct Bs58DecodeError(bs58::decode::Error); |
165 | | |
166 | 4 | fn calculate_checksum(data: &[u8]) -> [u8; 2] { |
167 | 4 | let mut hasher = blake2_rfc::blake2b::Blake2b::new(64); |
168 | 4 | hasher.update(b"SS58PRE"); |
169 | 4 | hasher.update(data); |
170 | 4 | |
171 | 4 | let hash = hasher.finalize(); |
172 | 4 | *<&[u8; 2]>::try_from(&hash.as_bytes()[..2]).unwrap_or_else(|_| unreachable!()0 ) Unexecuted instantiation: _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity4ss5818calculate_checksum0B7_ Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity4ss5818calculate_checksum0B7_ |
173 | 4 | } _RNvNtNtCsN16ciHI6Qf_7smoldot8identity4ss5818calculate_checksum Line | Count | Source | 166 | 4 | fn calculate_checksum(data: &[u8]) -> [u8; 2] { | 167 | 4 | let mut hasher = blake2_rfc::blake2b::Blake2b::new(64); | 168 | 4 | hasher.update(b"SS58PRE"); | 169 | 4 | hasher.update(data); | 170 | 4 | | 171 | 4 | let hash = hasher.finalize(); | 172 | 4 | *<&[u8; 2]>::try_from(&hash.as_bytes()[..2]).unwrap_or_else(|_| unreachable!()) | 173 | 4 | } |
Unexecuted instantiation: _RNvNtNtCseuYC0Zibziv_7smoldot8identity4ss5818calculate_checksum |
174 | | |
175 | | #[cfg(test)] |
176 | | mod tests { |
177 | | #[test] |
178 | 1 | fn alice_polkadot() { |
179 | 1 | let encoded = "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5"; |
180 | 1 | |
181 | 1 | let decoded = super::decode(encoded).unwrap(); |
182 | 1 | assert_eq!(u16::from(decoded.chain_prefix), 0); |
183 | 1 | assert_eq!( |
184 | 1 | decoded.public_key.as_ref(), |
185 | 1 | &[ |
186 | 1 | 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, |
187 | 1 | 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125 |
188 | 1 | ][..] |
189 | 1 | ); |
190 | | |
191 | 1 | assert_eq!(super::encode(decoded), encoded); |
192 | 1 | } |
193 | | |
194 | | #[test] |
195 | 1 | fn sora_default_seed_phrase() { |
196 | 1 | let encoded = "cnT6GtrVo7AYsRc2LgfTgW8Gu4gpZpxhaaKMm7zH8Ry14pJ8b"; |
197 | 1 | |
198 | 1 | let decoded = super::decode(encoded).unwrap(); |
199 | 1 | assert_eq!(u16::from(decoded.chain_prefix), 69); |
200 | 1 | assert_eq!( |
201 | 1 | decoded.public_key.as_ref(), |
202 | 1 | &[ |
203 | 1 | 70, 235, 221, 239, 140, 217, 187, 22, 125, 195, 8, 120, 215, 17, 59, 126, 22, 142, |
204 | 1 | 111, 6, 70, 190, 255, 215, 125, 105, 211, 155, 173, 118, 180, 122 |
205 | 1 | ][..] |
206 | 1 | ); |
207 | | |
208 | 1 | assert_eq!(super::encode(decoded), encoded); |
209 | 1 | } |
210 | | } |