mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-11 20:01:08 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user