gear_ss58/
lib.rs

1// This file is part of Gear.
2//
3// Copyright (C) 2024-2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! SS58 encoding implementation
20//!
21//! This library is extracted from [ss58 codec][ss58-codec] in `sp-core`, not
22//! importing `sp-core` because it is super big (~300 dependencies).
23//!
24//! [ss58-codec]: https://paritytech.github.io/polkadot-sdk/master/sp_core/crypto/trait.Ss58Codec.html
25
26#![cfg_attr(not(feature = "std"), no_std)]
27
28#[cfg(feature = "alloc")]
29extern crate alloc;
30
31#[cfg(feature = "alloc")]
32use alloc::{
33    string::{String, ToString},
34    vec::Vec,
35};
36use blake2::{Blake2b512, Digest};
37use bs58::{
38    decode::{self, DecodeTarget},
39    encode::{self, EncodeTarget},
40};
41use core::{
42    array::TryFromSliceError,
43    fmt,
44    ops::{Deref, DerefMut, RangeInclusive},
45    str,
46    sync::atomic::{AtomicU16, Ordering},
47};
48
49// Prefix for checksum.
50const PREFIX: &[u8] = b"SS58PRE";
51
52/// Allowed prefix length.
53const PREFIX_LEN_RANGE: RangeInclusive<usize> = 1..=2;
54/// Minimum prefix length.
55const MIN_PREFIX_LEN: usize = *PREFIX_LEN_RANGE.start();
56/// Maximum prefix length.
57const MAX_PREFIX_LEN: usize = *PREFIX_LEN_RANGE.end();
58
59/// Length of public key is 32 bytes.
60const BODY_LEN: usize = 32;
61
62/// Default checksum size is 2 bytes.
63const CHECKSUM_LEN: usize = 2;
64
65/// Minimum address length without base58 encoding.
66const MIN_ADDRESS_LEN: usize = MIN_PREFIX_LEN + BODY_LEN + CHECKSUM_LEN;
67/// Maximum address length without base58 encoding.
68const MAX_ADDRESS_LEN: usize = MAX_PREFIX_LEN + BODY_LEN + CHECKSUM_LEN;
69/// Allowed address length without base58 encoding.
70const ADDRESS_LEN_RANGE: RangeInclusive<usize> = MIN_ADDRESS_LEN..=MAX_ADDRESS_LEN;
71
72/// Function is taken from [`bs58`] to calculate the maximum length required for
73/// base58 encoding.
74const fn base58_max_encoded_len(len: usize) -> usize {
75    // log_2(256) / log_2(58) ≈ 1.37.  Assume 1.5 for easier calculation.
76    len + len.div_ceil(2)
77}
78
79/// Maximum address length in base58 encoding.
80const MAX_ADDRESS_LEN_BASE58: usize = base58_max_encoded_len(MAX_ADDRESS_LEN);
81
82/// The SS58 prefix of substrate.
83pub const SUBSTRATE_SS58_PREFIX: u16 = 42;
84/// The SS58 prefix of vara network.
85pub const VARA_SS58_PREFIX: u16 = 137;
86
87/// The default ss58 version.
88static DEFAULT_SS58_VERSION: AtomicU16 = AtomicU16::new(VARA_SS58_PREFIX);
89
90/// Get the default ss58 version.
91pub fn default_ss58_version() -> u16 {
92    DEFAULT_SS58_VERSION.load(Ordering::Relaxed)
93}
94
95/// Set the default ss58 version.
96pub fn set_default_ss58_version(version: u16) {
97    DEFAULT_SS58_VERSION.store(version, Ordering::Relaxed);
98}
99
100struct Buffer<const N: usize>([u8; N]);
101
102impl<const N: usize> Buffer<N> {
103    pub const fn new() -> Self {
104        Self([0; N])
105    }
106}
107
108impl<const N: usize> Deref for Buffer<N> {
109    type Target = [u8];
110
111    fn deref(&self) -> &Self::Target {
112        &self.0
113    }
114}
115
116impl<const N: usize> DerefMut for Buffer<N> {
117    fn deref_mut(&mut self) -> &mut Self::Target {
118        &mut self.0
119    }
120}
121
122impl DecodeTarget for Buffer<MAX_ADDRESS_LEN> {
123    fn decode_with(
124        &mut self,
125        _max_len: usize,
126        f: impl for<'a> FnOnce(&'a mut [u8]) -> decode::Result<usize>,
127    ) -> decode::Result<usize> {
128        let len = f(&mut self[..])?;
129        Ok(len)
130    }
131}
132
133impl EncodeTarget for Buffer<MAX_ADDRESS_LEN_BASE58> {
134    fn encode_with(
135        &mut self,
136        _max_len: usize,
137        f: impl for<'a> FnOnce(&'a mut [u8]) -> encode::Result<usize>,
138    ) -> encode::Result<usize> {
139        let len = f(&mut self[..])?;
140        Ok(len)
141    }
142}
143
144/// An error type for SS58 decoding.
145#[derive(Debug, PartialEq, Eq)]
146pub enum Error {
147    Base58Encode,
148    BadBase58,
149    BadLength,
150    InvalidPrefix,
151    InvalidChecksum,
152    #[cfg(feature = "alloc")]
153    InvalidSliceLength,
154}
155
156impl fmt::Display for Error {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            Self::Base58Encode => writeln!(f, "Base 58 encoding failed"),
160            Self::BadBase58 => writeln!(f, "Base 58 requirement is violated"),
161            Self::BadLength => writeln!(f, "Length is bad"),
162            Self::InvalidPrefix => writeln!(f, "Invalid SS58 prefix byte"),
163            Self::InvalidChecksum => writeln!(f, "Invalid checksum"),
164            #[cfg(feature = "alloc")]
165            Self::InvalidSliceLength => writeln!(f, "Slice should be 32 length"),
166        }
167    }
168}
169
170#[cfg(feature = "std")]
171impl std::error::Error for Error {}
172
173/// Represents SS58 address.
174pub struct Ss58Address {
175    len: usize,
176    buf: Buffer<MAX_ADDRESS_LEN_BASE58>,
177}
178
179impl Ss58Address {
180    /// Returns string slice containing SS58 address.
181    pub fn as_str(&self) -> &str {
182        unsafe { str::from_utf8_unchecked(&self.buf).get_unchecked(..self.len) }
183    }
184}
185
186impl fmt::Display for Ss58Address {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        f.write_str(self.as_str())
189    }
190}
191
192impl fmt::Debug for Ss58Address {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        fmt::Display::fmt(self, f)
195    }
196}
197
198/// Represents public key bytes.
199pub struct RawSs58Address([u8; BODY_LEN]);
200
201impl From<RawSs58Address> for [u8; BODY_LEN] {
202    fn from(address: RawSs58Address) -> Self {
203        address.0
204    }
205}
206
207impl From<[u8; BODY_LEN]> for RawSs58Address {
208    fn from(array: [u8; BODY_LEN]) -> Self {
209        Self(array)
210    }
211}
212
213impl TryFrom<&[u8]> for RawSs58Address {
214    type Error = TryFromSliceError;
215
216    fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
217        <[u8; BODY_LEN]>::try_from(slice).map(Self)
218    }
219}
220
221impl fmt::Display for RawSs58Address {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        let mut buf = [0; BODY_LEN * 2];
224        let _ = hex::encode_to_slice(self.0, &mut buf);
225        f.write_str("0x")?;
226        f.write_str(unsafe { str::from_utf8_unchecked(&buf) })
227    }
228}
229
230impl fmt::Debug for RawSs58Address {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        fmt::Display::fmt(self, f)
233    }
234}
235
236impl RawSs58Address {
237    /// Returns raw address if string is properly encoded ss58-check address.
238    pub fn from_ss58check(s: &str) -> Result<Self, Error> {
239        Self::from_ss58check_with_prefix(s).map(|(address, _)| address)
240    }
241
242    /// Returns raw address with prefix if string is properly encoded ss58-check address.
243    pub fn from_ss58check_with_prefix(s: &str) -> Result<(Self, u16), Error> {
244        let mut data = Buffer::<MAX_ADDRESS_LEN>::new();
245        let data_len = bs58::decode(s)
246            .onto(&mut data)
247            .map_err(|_| Error::BadBase58)?;
248
249        if !ADDRESS_LEN_RANGE.contains(&data_len) {
250            return Err(Error::BadLength);
251        }
252
253        let (prefix_len, prefix) = match data[0] {
254            0..=63 => (1, data[0] as u16),
255            64..=127 => {
256                // weird bit manipulation owing to the combination of LE encoding and missing two
257                // bits from the left.
258                // d[0] d[1] are: 01aaaaaa bbcccccc
259                // they make the LE-encoded 16-bit value: aaaaaabb 00cccccc
260                // so the lower byte is formed of aaaaaabb and the higher byte is 00cccccc
261                let lower = (data[0] << 2) | (data[1] >> 6);
262                let upper = data[1] & 0b00111111;
263                (2, (lower as u16) | ((upper as u16) << 8))
264            }
265            _ => return Err(Error::InvalidPrefix),
266        };
267
268        if data_len != prefix_len + BODY_LEN + CHECKSUM_LEN {
269            return Err(Error::BadLength);
270        }
271
272        let (address_data, address_checksum) = data.split_at(prefix_len + BODY_LEN);
273
274        let hash = ss58hash(address_data);
275        let checksum = &hash[..CHECKSUM_LEN];
276
277        if &address_checksum[..CHECKSUM_LEN] != checksum {
278            return Err(Error::InvalidChecksum);
279        }
280
281        match <[u8; BODY_LEN]>::try_from(&address_data[prefix_len..]) {
282            Ok(array) => Ok((Self(array), prefix)),
283            Err(_) => Err(Error::BadLength),
284        }
285    }
286}
287
288impl RawSs58Address {
289    /// Returns ss58-check string for this address. The prefix can be overridden via [`default_ss58_version()`].
290    pub fn to_ss58check(&self) -> Result<Ss58Address, Error> {
291        self.to_ss58check_with_prefix(default_ss58_version())
292    }
293
294    /// Returns ss58-check string for this address with given prefix.
295    pub fn to_ss58check_with_prefix(&self, prefix: u16) -> Result<Ss58Address, Error> {
296        let mut buffer = Buffer::<MAX_ADDRESS_LEN>::new();
297
298        // We mask out the upper two bits of the ident - SS58 Prefix currently only supports 14-bits
299        let ident = prefix & 0b0011_1111_1111_1111;
300        let (prefix_len, address_len) = match ident {
301            0..=63 => {
302                buffer[0] = ident as u8;
303                (MIN_PREFIX_LEN, MIN_ADDRESS_LEN)
304            }
305            64..=16_383 => {
306                // upper six bits of the lower byte(!)
307                let first = ((ident & 0b0000_0000_1111_1100) as u8) >> 2;
308                // lower two bits of the lower byte in the high pos,
309                // lower bits of the upper byte in the low pos
310                let second = ((ident >> 8) as u8) | (((ident & 0b0000_0000_0000_0011) as u8) << 6);
311
312                buffer[0] = first | 0b01000000;
313                buffer[1] = second;
314                (MAX_PREFIX_LEN, MAX_ADDRESS_LEN)
315            }
316            _ => unreachable!("masked out the upper two bits; qed"),
317        };
318
319        let (address_data, address_checksum) = buffer.split_at_mut(prefix_len + BODY_LEN);
320
321        address_data[prefix_len..].copy_from_slice(&self.0);
322        let hash = ss58hash(address_data);
323        address_checksum[..CHECKSUM_LEN].copy_from_slice(&hash[..CHECKSUM_LEN]);
324
325        let mut buf = Buffer::<MAX_ADDRESS_LEN_BASE58>::new();
326        let len = bs58::encode(&buffer[..address_len])
327            .onto(&mut buf)
328            .map_err(|_| Error::Base58Encode)?;
329
330        Ok(Ss58Address { len, buf })
331    }
332}
333
334fn ss58hash(data: &[u8]) -> [u8; 64] {
335    let mut ctx = Blake2b512::new();
336    ctx.update(PREFIX);
337    ctx.update(data);
338    ctx.finalize().into()
339}
340
341/// Encode data to SS58 format.
342#[cfg(feature = "alloc")]
343pub fn encode(data: &[u8]) -> Result<String, Error> {
344    let raw_address = RawSs58Address::try_from(data).map_err(|_| Error::InvalidSliceLength)?;
345    let address = raw_address.to_ss58check()?;
346    Ok(address.to_string())
347}
348
349/// Decode data from SS58 format.
350#[cfg(feature = "alloc")]
351pub fn decode(encoded: &str) -> Result<Vec<u8>, Error> {
352    let raw_address: [u8; BODY_LEN] = RawSs58Address::from_ss58check(encoded)?.into();
353    Ok(raw_address.to_vec())
354}
355
356/// Re-encoding a ss58 address in the current [`default_ss58_version()`].
357#[cfg(feature = "alloc")]
358pub fn recode(encoded: &str) -> Result<String, Error> {
359    self::encode(&self::decode(encoded)?)
360}