Extend SS58 network identifiers (#8039)

* Initial draft

* Fixes

* Fixes

* Fixes

* Fixes

* Fixes

* Improve readability, add format filter.

* Link

* Fixes

* Update primitives/core/src/crypto.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Suggestions from review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
Gavin Wood
2021-02-06 16:00:10 +01:00
committed by GitHub
parent 1b31f7c5d1
commit 4b1460f61f
3 changed files with 118 additions and 53 deletions
+80 -37
View File
@@ -210,7 +210,7 @@ pub enum PublicError {
BadBase58,
/// Bad length.
BadLength,
/// Unknown version.
/// Unknown identifier for the encoding.
UnknownVersion,
/// Invalid checksum.
InvalidChecksum,
@@ -218,11 +218,22 @@ pub enum PublicError {
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> {
@@ -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")]
@@ -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]);
@@ -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")]
@@ -363,8 +408,16 @@ 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,
@@ -372,22 +425,13 @@ macro_rules! ss58_address_format {
}
}
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)),
}
}
}
@@ -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),
}
}
}
@@ -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 =>
@@ -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 =>
@@ -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 =>
@@ -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
+32 -1
View File
@@ -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() {
@@ -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
@@ -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";
+6 -15
View File
@@ -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,
@@ -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,
@@ -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,
@@ -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
}
]
}