Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
117 changes: 80 additions & 37 deletions primitives/core/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,19 +210,30 @@ pub enum PublicError {
BadBase58,
/// Bad length.
BadLength,
/// Unknown version.
/// Unknown identifier for the encoding.
UnknownVersion,
/// Invalid checksum.
InvalidChecksum,
/// Invalid format.
InvalidFormat,
/// Invalid derivation path.
InvalidPath,
/// Disallowed SS58 Address Format for this datatype.
FormatNotAllowed,
}

/// Key that can be encoded to/from SS58.
///
/// See https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)#address-type
/// for information on the codec.
#[cfg(feature = "full_crypto")]
pub trait Ss58Codec: Sized + AsMut<[u8]> + AsRef<[u8]> + Default {
/// A format filterer, can be used to ensure that `from_ss58check` family only decode for
/// allowed identifiers. By default just refuses the two reserved identifiers.
fn format_is_allowed(f: Ss58AddressFormat) -> bool {
!matches!(f, Ss58AddressFormat::Reserved46 | Ss58AddressFormat::Reserved47)
}

/// Some if the string is a properly encoded SS58Check address.
#[cfg(feature = "std")]
fn from_ss58check(s: &str) -> Result<Self, PublicError> {
Expand All @@ -233,25 +244,46 @@ pub trait Ss58Codec: Sized + AsMut<[u8]> + AsRef<[u8]> + Default {
_ => Err(PublicError::UnknownVersion),
})
}

/// Some if the string is a properly encoded SS58Check address.
#[cfg(feature = "std")]
fn from_ss58check_with_version(s: &str) -> Result<(Self, Ss58AddressFormat), PublicError> {
const CHECKSUM_LEN: usize = 2;
let mut res = Self::default();
let len = res.as_mut().len();
let d = s.from_base58().map_err(|_| PublicError::BadBase58)?; // failure here would be invalid encoding.
if d.len() != len + 3 {
// Invalid length.
return Err(PublicError::BadLength);
}
let ver = d[0].try_into().map_err(|_: ()| PublicError::UnknownVersion)?;

if d[len + 1..len + 3] != ss58hash(&d[0..len + 1]).as_bytes()[0..2] {
// Must decode to our type.
let body_len = res.as_mut().len();

let data = s.from_base58().map_err(|_| PublicError::BadBase58)?;
if data.len() < 2 { return Err(PublicError::BadLength); }
let (prefix_len, ident) = match data[0] {
0..=63 => (1, data[0] as u16),
64..=127 => {
// weird bit manipulation owing to the combination of LE encoding and missing two bits
// from the left.
// d[0] d[1] are: 01aaaaaa bbcccccc
// they make the LE-encoded 16-bit value: aaaaaabb 00cccccc
// so the lower byte is formed of aaaaaabb and the higher byte is 00cccccc
let lower = (data[0] << 2) | (data[1] >> 6);
let upper = data[1] & 0b00111111;
(2, (lower as u16) | ((upper as u16) << 8))
}
_ => Err(PublicError::UnknownVersion)?,
};
if data.len() != prefix_len + body_len + CHECKSUM_LEN { return Err(PublicError::BadLength) }
let format = ident.try_into().map_err(|_: ()| PublicError::UnknownVersion)?;
if !Self::format_is_allowed(format) { return Err(PublicError::FormatNotAllowed) }

let hash = ss58hash(&data[0..body_len + prefix_len]);
let checksum = &hash.as_bytes()[0..CHECKSUM_LEN];
if data[body_len + prefix_len..body_len + prefix_len + CHECKSUM_LEN] != *checksum {
// Invalid checksum.
return Err(PublicError::InvalidChecksum);
}
res.as_mut().copy_from_slice(&d[1..len + 1]);
Ok((res, ver))
res.as_mut().copy_from_slice(&data[prefix_len..body_len + prefix_len]);
Ok((res, format))
}

/// Some if the string is a properly encoded SS58Check address, optionally with
/// a derivation path following.
#[cfg(feature = "std")]
Expand All @@ -267,7 +299,20 @@ pub trait Ss58Codec: Sized + AsMut<[u8]> + AsRef<[u8]> + Default {
/// Return the ss58-check string for this key.
#[cfg(feature = "std")]
fn to_ss58check_with_version(&self, version: Ss58AddressFormat) -> String {
let mut v = vec![version.into()];
// We mask out the upper two bits of the ident - SS58 Prefix currently only supports 14-bits
let ident: u16 = u16::from(version) & 0b00111111_11111111;
let mut v = match ident {
0..=63 => vec![ident as u8],
64..=16_383 => {
// upper six bits of the lower byte(!)
let first = ((ident & 0b00000000_11111100) as u8) >> 2;
// lower two bits of the lower byte in the high pos,
// lower bits of the upper byte in the low pos
let second = ((ident >> 8) as u8) | ((ident & 0b00000000_00000011) as u8) << 6;
vec![first | 0b01000000, second]
}
_ => unreachable!("masked out the upper two bits; qed"),
};
v.extend(self.as_ref());
let r = ss58hash(&v);
v.extend(&r.as_bytes()[0..2]);
Expand Down Expand Up @@ -321,8 +366,8 @@ macro_rules! ss58_address_format {
#[derive(Copy, Clone, PartialEq, Eq, crate::RuntimeDebug)]
pub enum Ss58AddressFormat {
$(#[doc = $desc] $identifier),*,
/// Use a manually provided numeric value.
Custom(u8),
/// Use a manually provided numeric value as a standard identifier
Custom(u16),
}

#[cfg(feature = "std")]
Expand Down Expand Up @@ -363,31 +408,30 @@ macro_rules! ss58_address_format {
}
}

impl From<Ss58AddressFormat> for u8 {
fn from(x: Ss58AddressFormat) -> u8 {
impl TryFrom<u8> for Ss58AddressFormat {
type Error = ();

fn try_from(x: u8) -> Result<Ss58AddressFormat, ()> {
Ss58AddressFormat::try_from(x as u16)
}
}

impl From<Ss58AddressFormat> for u16 {
fn from(x: Ss58AddressFormat) -> u16 {
match x {
$(Ss58AddressFormat::$identifier => $number),*,
Ss58AddressFormat::Custom(n) => n,
}
}
}

impl TryFrom<u8> for Ss58AddressFormat {
impl TryFrom<u16> for Ss58AddressFormat {
type Error = ();

fn try_from(x: u8) -> Result<Ss58AddressFormat, ()> {
fn try_from(x: u16) -> Result<Ss58AddressFormat, ()> {
match x {
$($number => Ok(Ss58AddressFormat::$identifier)),*,
_ => {
#[cfg(feature = "std")]
match Ss58AddressFormat::default() {
Ss58AddressFormat::Custom(n) if n == x => Ok(Ss58AddressFormat::Custom(x)),
_ => Err(()),
}

#[cfg(not(feature = "std"))]
Err(())
},
_ => Ok(Ss58AddressFormat::Custom(x)),
}
}
}
Expand All @@ -403,7 +447,7 @@ macro_rules! ss58_address_format {
fn try_from(x: &'a str) -> Result<Ss58AddressFormat, Self::Error> {
match x {
$($name => Ok(Ss58AddressFormat::$identifier)),*,
a => a.parse::<u8>().map(Ss58AddressFormat::Custom).map_err(|_| ParseError),
a => a.parse::<u16>().map(Ss58AddressFormat::Custom).map_err(|_| ParseError),
}
}
}
Expand Down Expand Up @@ -444,12 +488,12 @@ macro_rules! ss58_address_format {
ss58_address_format!(
PolkadotAccount =>
(0, "polkadot", "Polkadot Relay-chain, standard account (*25519).")
Reserved1 =>
(1, "reserved1", "Reserved for future use (1).")
BareSr25519 =>
(1, "sr25519", "Bare 32-bit Schnorr/Ristretto 25519 (S/R 25519) key.")
KusamaAccount =>
(2, "kusama", "Kusama Relay-chain, standard account (*25519).")
Reserved3 =>
(3, "reserved3", "Reserved for future use (3).")
BareEd25519 =>
(3, "ed25519", "Bare 32-bit Edwards Ed25519 key.")
KatalChainAccount =>
(4, "katalchain", "Katal Chain, standard account (*25519).")
PlasmAccount =>
Expand Down Expand Up @@ -501,7 +545,7 @@ ss58_address_format!(
SubsocialAccount =>
(28, "subsocial", "Subsocial network, standard account (*25519).")
DhiwayAccount =>
(29, "cord", "Dhiway CORD network, standard account (*25519).")
(29, "cord", "Dhiway CORD network, standard account (*25519).")
PhalaAccount =>
(30, "phala", "Phala Network, standard account (*25519).")
LitentryAccount =>
Expand All @@ -522,8 +566,8 @@ ss58_address_format!(
(41, "poli", "Polimec Chain mainnet, standard account (*25519).")
SubstrateAccount =>
(42, "substrate", "Any Substrate network, standard account (*25519).")
Reserved43 =>
(43, "reserved43", "Reserved for future use (43).")
BareSecp256k1 =>
(43, "secp256k1", "Bare ECDSA SECP256k1 key.")
ChainXAccount =>
(44, "chainx", "ChainX mainnet, standard account (*25519).")
UniartsAccount =>
Expand All @@ -532,7 +576,6 @@ ss58_address_format!(
(46, "reserved46", "Reserved for future use (46).")
Reserved47 =>
(47, "reserved47", "Reserved for future use (47).")
// Note: 48 and above are reserved.
);

/// Set the default "version" (actually, this is a bit of a misnomer and the version byte is
Expand Down
33 changes: 32 additions & 1 deletion primitives/core/src/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ mod test {
use hex_literal::hex;
use crate::crypto::{DEV_PHRASE, set_default_ss58_version};
use serde_json;
use crate::crypto::PublicError;

#[test]
fn default_phrase_should_be_used() {
Expand Down Expand Up @@ -676,6 +677,34 @@ mod test {
assert_eq!(cmp, public);
}

#[test]
fn ss58check_format_check_works() {
use crate::crypto::Ss58AddressFormat;
let pair = Pair::from_seed(b"12345678901234567890123456789012");
let public = pair.public();
let format = Ss58AddressFormat::Reserved46;
let s = public.to_ss58check_with_version(format);
assert_eq!(Public::from_ss58check_with_version(&s), Err(PublicError::FormatNotAllowed));
}

#[test]
fn ss58check_full_roundtrip_works() {
use crate::crypto::Ss58AddressFormat;
let pair = Pair::from_seed(b"12345678901234567890123456789012");
let public = pair.public();
let format = Ss58AddressFormat::PolkadotAccount;
let s = public.to_ss58check_with_version(format);
let (k, f) = Public::from_ss58check_with_version(&s).unwrap();
assert_eq!(k, public);
assert_eq!(f, format);

let format = Ss58AddressFormat::Custom(64);
let s = public.to_ss58check_with_version(format);
let (k, f) = Public::from_ss58check_with_version(&s).unwrap();
assert_eq!(k, public);
assert_eq!(f, format);
}

#[test]
fn ss58check_custom_format_works() {
// We need to run this test in its own process to not interfere with other tests running in
Expand All @@ -685,10 +714,12 @@ mod test {
// temp save default format version
let default_format = Ss58AddressFormat::default();
// set current ss58 version is custom "200" `Ss58AddressFormat::Custom(200)`

set_default_ss58_version(Ss58AddressFormat::Custom(200));
// custom addr encoded by version 200
let addr = "2X64kMNEWAW5KLZMSKcGKEc96MyuaRsRUku7vomuYxKgqjVCRj";
let addr = "4pbsSkWcBaYoFHrKJZp5fDVUKbqSYD9dhZZGvpp3vQ5ysVs5ybV";
Public::from_ss58check(&addr).unwrap();

set_default_ss58_version(default_format);
// set current ss58 version to default version
let addr = "KWAfgC2aRG5UVD6CpbPQXCx4YZZUhvWqqAJE6qcYc9Rtr6g5C";
Expand Down
21 changes: 6 additions & 15 deletions ss58-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
},
{
"prefix": 1,
"network": "reserved1",
"displayName": "This prefix is reserved.",
"network": null,
"displayName": "Bare 32-bit Schnorr/Ristretto (S/R 25519) public key.",
"symbols": null,
"decimals": null,
"standardAccount": null,
Expand All @@ -39,8 +39,8 @@
},
{
"prefix": 3,
"network": "reserved3",
"displayName": "This prefix is reserved.",
"network": null,
"displayName": "Bare 32-bit Ed25519 public key.",
"symbols": null,
"decimals": null,
"standardAccount": null,
Expand Down Expand Up @@ -390,8 +390,8 @@
},
{
"prefix": 43,
"network": "reserved43",
"displayName": "This prefix is reserved.",
"network": null,
"displayName": "Bare 32-bit ECDSA SECP-256k1 public key.",
"symbols": null,
"decimals": null,
"standardAccount": null,
Expand Down Expand Up @@ -432,15 +432,6 @@
"decimals": null,
"standardAccount": null,
"website": null
},
{
"prefix": 48,
"network": "reserved48",
"displayName": "All prefixes 48 and higher are reserved and cannot be allocated.",
"symbols": null,
"decimals": null,
"standardAccount": null,
"website": null
}
]
}