Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SS58 encoding and decoding #691

Merged
merged 2 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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