Skip to content

Commit

Permalink
Implement SS58 encoding and decoding (#691)
Browse files Browse the repository at this point in the history
* Implement SS58 encoding and decoding

* PR link
  • Loading branch information
tomaka authored Jun 7, 2023
1 parent 24f8063 commit 1214738
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 19 deletions.
3 changes: 1 addition & 2 deletions lib/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,4 @@
pub mod keystore;
pub mod seed_phrase;

// TODO: implement ss58
pub mod ss58;
210 changes: 210 additions & 0 deletions lib/src/identity/ss58.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Smoldot
// Copyright (C) 2023 Pierre Krieger
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use alloc::{string::String, vec::Vec};
use core::fmt;

/// Decoded version of an SS58 address.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Decoded<P> {
/// Identifier indicating which chain is concerned.
///
/// The mapping between chains and this prefix can be found in this central registry:
/// <https://github.com/paritytech/ss58-registry/blob/main/ss58-registry.json>.
pub chain_prefix: ChainPrefix,

/// Public key of the account.
pub public_key: P,
}

/// Identifier indicating which chain is concerned.
///
/// The mapping between chains and this prefix can be found in this central registry:
/// <https://github.com/paritytech/ss58-registry/blob/main/ss58-registry.json>.
///
/// This prefix is a 14 bits unsigned integer.
//
// Implementation note: the `u16` is guaranteed to be only up to 14 bits long. The upper two bits
// are always 0.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct ChainPrefix(u16);

impl fmt::Debug for ChainPrefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}

impl TryFrom<u16> for ChainPrefix {
type Error = PrefixTooLargeError;

fn try_from(prefix: u16) -> Result<Self, Self::Error> {
if (prefix >> 14) == 0 {
Ok(ChainPrefix(prefix))
} else {
Err(PrefixTooLargeError())
}
}
}

/// Integer is too large to be a valid prefix
#[derive(Debug, Clone, derive_more::Display)]
pub struct PrefixTooLargeError();

impl From<u8> for ChainPrefix {
fn from(prefix: u8) -> ChainPrefix {
ChainPrefix(u16::from(prefix))
}
}

impl From<ChainPrefix> for u16 {
fn from(prefix: ChainPrefix) -> u16 {
prefix.0
}
}

/// Turns a decoded SS58 address into a string.
pub fn encode(decoded: Decoded<impl AsRef<[u8]>>) -> String {
let prefix = decoded.chain_prefix.0;
let public_key = decoded.public_key.as_ref();

let mut bytes = Vec::with_capacity(2 + public_key.len() + 2);

if prefix < 64 {
bytes.push(prefix as u8);
} else {
// This encoding is plain weird.
bytes.push(((prefix & 0b0000_0000_1111_1100) as u8) >> 2 | 0b01000000);
bytes.push(((prefix >> 8) as u8) | ((prefix & 0b0000_0000_0000_0011) as u8) << 6);
}

bytes.extend_from_slice(public_key);

let checksum = calculate_checksum(&bytes);
bytes.extend_from_slice(&checksum);

bs58::encode(&bytes).into_string()
}

/// Decodes an SS58 address from a string.
pub fn decode(encoded: &'_ str) -> Result<Decoded<impl AsRef<[u8]>>, DecodeError> {
let mut bytes = bs58::decode(encoded)
.into_vec()
.map_err(|err| DecodeError::InvalidBs58(Bs58DecodeError(err)))?;

if bytes.len() < 4 {
return Err(DecodeError::TooShort);
}

// Verify the checksum.
let expected_checksum = calculate_checksum(&bytes[..bytes.len() - 2]);
if expected_checksum[..] != bytes[bytes.len() - 2..] {
return Err(DecodeError::InvalidChecksum);
}
bytes.truncate(bytes.len() - 2);

// Grab and remove the prefix.
let (prefix_len, chain_prefix) = if bytes[0] < 64 {
(1, ChainPrefix(u16::from(bytes[0])))
} else if bytes[0] < 128 {
let prefix = u16::from_be_bytes([bytes[1] & 0b00111111, (bytes[0] << 2) | (bytes[1] >> 6)]);
(2, ChainPrefix(prefix))
} else {
return Err(DecodeError::InvalidPrefix);
};

// Rather than remove the prefix from the beginning of `bytes`, we adjust the `AsRef`
// implementation to skip the prefix.
let public_key = {
struct Adjust(Vec<u8>, usize);
impl AsRef<[u8]> for Adjust {
fn as_ref(&self) -> &[u8] {
&self.0[self.1..]
}
}
Adjust(bytes, prefix_len)
};

Ok(Decoded {
chain_prefix,
public_key,
})
}

/// Error while decoding an SS58 address.
#[derive(Debug, Clone, derive_more::Display)]
pub enum DecodeError {
/// SS58 is too short to possibly be valid.
TooShort,
/// Invalid SS58 prefix encoding.
InvalidPrefix,
/// Invalid BS58 format.
#[display(fmt = "{_0}")]
InvalidBs58(Bs58DecodeError),
/// Calculated checksum doesn't match the one provided.
InvalidChecksum,
}

/// Error when decoding Base58 encoding.
#[derive(Debug, Clone, derive_more::Display, derive_more::From)]
pub struct Bs58DecodeError(bs58::decode::Error);

fn calculate_checksum(data: &[u8]) -> [u8; 2] {
let mut hasher = blake2_rfc::blake2b::Blake2b::new(64);
hasher.update(b"SS58PRE");
hasher.update(data);

let hash = hasher.finalize();
*<&[u8; 2]>::try_from(&hash.as_bytes()[..2]).unwrap_or_else(|_| unreachable!())
}

#[cfg(test)]
mod tests {
#[test]
fn alice_polkadot() {
let encoded = "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5";

let decoded = super::decode(encoded).unwrap();
assert_eq!(u16::from(decoded.chain_prefix), 0);
assert_eq!(
decoded.public_key.as_ref(),
&[
212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44,
133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125
][..]
);

assert_eq!(super::encode(decoded), encoded);
}

#[test]
fn sora_default_seed_phrase() {
let encoded = "cnT6GtrVo7AYsRc2LgfTgW8Gu4gpZpxhaaKMm7zH8Ry14pJ8b";

let decoded = super::decode(encoded).unwrap();
assert_eq!(u16::from(decoded.chain_prefix), 69);
assert_eq!(
decoded.public_key.as_ref(),
&[
70, 235, 221, 239, 140, 217, 187, 22, 125, 195, 8, 120, 215, 17, 59, 126, 22, 142,
111, 6, 70, 190, 255, 215, 125, 105, 211, 155, 173, 118, 180, 122
][..]
);

assert_eq!(super::encode(decoded), encoded);
}
}
23 changes: 6 additions & 17 deletions lib/src/json_rpc/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
//! List of requests and how to answer them.
use super::parse;
use crate::header;
use crate::{header, identity::ss58};

use alloc::{
borrow::Cow,
Expand Down Expand Up @@ -614,7 +614,7 @@ pub enum RemoveMetadataLengthPrefixError {
///
/// The deserialization involves decoding an SS58 address into this public key.
#[derive(Debug, Clone)]
pub struct AccountId(pub [u8; 32]);
pub struct AccountId(pub Vec<u8>);

impl serde::Serialize for AccountId {
fn serialize<S>(&self, _: S) -> Result<S::Ok, S::Error>
Expand All @@ -632,25 +632,14 @@ impl<'a> serde::Deserialize<'a> for AccountId {
D: serde::Deserializer<'a>,
{
let string = <&str>::deserialize(deserializer)?;
let decoded = match bs58::decode(&string).into_vec() {
// TODO: don't use into_vec
let decoded = match ss58::decode(string) {
Ok(d) => d,
Err(_) => return Err(serde::de::Error::custom("AccountId isn't in base58 format")),
Err(err) => return Err(serde::de::Error::custom(err.to_string())),
};

// TODO: retrieve the actual prefix length of the current chain
if decoded.len() < 35 {
return Err(serde::de::Error::custom("unexpected length for AccountId"));
}

// TODO: finish implementing this properly ; must notably check checksum
// see https://github.com/paritytech/substrate/blob/74a50abd6cbaad1253daf3585d5cdaa4592e9184/primitives/core/src/crypto.rs#L228

// TODO: retrieve and use the actual prefix length of the current chain
let account_id =
<[u8; 32]>::try_from(&decoded[(decoded.len() - 34)..(decoded.len() - 2)]).unwrap();
// TODO: check the prefix against the one of the current chain?

Ok(AccountId(account_id))
Ok(AccountId(decoded.public_key.as_ref().to_vec()))
}
}

Expand Down
1 change: 1 addition & 0 deletions wasm-node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- Add support for child tries, meaning that errors will no longer be returned when performing runtime calls on chains that use child tries. In practice, this typically concerns contracts chains. ([#680](https://github.com/smol-dot/smoldot/pull/680), [#684](https://github.com/smol-dot/smoldot/pull/684))
- The checksum of the SS58 address passed to the `system_accountNextIndex` JSON-RPC function is now verified. Note that its prefix isn't compared against the one of the current chain, because there is no straight-forward way for smoldot to determine the SS58 prefix of the chain that it is running. ([#691](https://github.com/smol-dot/smoldot/pull/691))

### Fixed

Expand Down

0 comments on commit 1214738

Please sign in to comment.