From 121473823b8867a533aa993645a0f44ff8113f3e Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Wed, 7 Jun 2023 17:48:45 +0000 Subject: [PATCH] Implement SS58 encoding and decoding (#691) * Implement SS58 encoding and decoding * PR link --- lib/src/identity.rs | 3 +- lib/src/identity/ss58.rs | 210 ++++++++++++++++++++++++++++++++++++ lib/src/json_rpc/methods.rs | 23 ++-- wasm-node/CHANGELOG.md | 1 + 4 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 lib/src/identity/ss58.rs diff --git a/lib/src/identity.rs b/lib/src/identity.rs index 6d603b0b84..eacabdf818 100644 --- a/lib/src/identity.rs +++ b/lib/src/identity.rs @@ -85,5 +85,4 @@ pub mod keystore; pub mod seed_phrase; - -// TODO: implement ss58 +pub mod ss58; diff --git a/lib/src/identity/ss58.rs b/lib/src/identity/ss58.rs new file mode 100644 index 0000000000..56aba710a9 --- /dev/null +++ b/lib/src/identity/ss58.rs @@ -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 . + +use alloc::{string::String, vec::Vec}; +use core::fmt; + +/// Decoded version of an SS58 address. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Decoded

{ + /// Identifier indicating which chain is concerned. + /// + /// The mapping between chains and this prefix can be found in this central registry: + /// . + 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: +/// . +/// +/// 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 for ChainPrefix { + type Error = PrefixTooLargeError; + + fn try_from(prefix: u16) -> Result { + 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 for ChainPrefix { + fn from(prefix: u8) -> ChainPrefix { + ChainPrefix(u16::from(prefix)) + } +} + +impl From for u16 { + fn from(prefix: ChainPrefix) -> u16 { + prefix.0 + } +} + +/// Turns a decoded SS58 address into a string. +pub fn encode(decoded: Decoded>) -> 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>, 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, 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); + } +} diff --git a/lib/src/json_rpc/methods.rs b/lib/src/json_rpc/methods.rs index 06f130b211..c74fe4f5c1 100644 --- a/lib/src/json_rpc/methods.rs +++ b/lib/src/json_rpc/methods.rs @@ -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, @@ -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); impl serde::Serialize for AccountId { fn serialize(&self, _: S) -> Result @@ -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())) } } diff --git a/wasm-node/CHANGELOG.md b/wasm-node/CHANGELOG.md index ed21252963..9c3b0c9639 100644 --- a/wasm-node/CHANGELOG.md +++ b/wasm-node/CHANGELOG.md @@ -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