Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
SS58 versioning (Network IDs) (#3147)
Browse files Browse the repository at this point in the history
* Introduce network IDs for SS58

* Fix

* Allow numeric overrides.

* Improve docs

* String rather than str

* Comment out code that will become valid after other PR

* Fix
  • Loading branch information
gavofyork committed Jul 20, 2019
1 parent aa8c06a commit cf98501
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 22 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions core/primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ hex = { version = "0.3", optional = true }
regex = { version = "1.1", optional = true }
num-traits = { version = "0.2", default-features = false }
zeroize = { version = "0.9.2", default-features = false }
lazy_static = { version = "1.3", optional = true }
parking_lot = { version = "0.8", optional = true }

[dev-dependencies]
substrate-serializer = { path = "../serializer" }
Expand All @@ -47,6 +49,8 @@ bench = false
default = ["std"]
std = [
"wasmi",
"lazy_static",
"parking_lot",
"primitive-types/std",
"primitive-types/serde",
"primitive-types/byteorder",
Expand Down
152 changes: 141 additions & 11 deletions core/primitives/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
//! Cryptographic utilities.
// end::description[]

#[cfg(feature = "std")]
use std::convert::{TryFrom, TryInto};
#[cfg(feature = "std")]
use parking_lot::Mutex;
#[cfg(feature = "std")]
use rand::{RngCore, rngs::OsRng};
#[cfg(feature = "std")]
Expand Down Expand Up @@ -243,12 +247,36 @@ pub enum PublicError {
#[cfg(feature = "std")]
pub trait Ss58Codec: Sized {
/// Some if the string is a properly encoded SS58Check address.
fn from_ss58check(s: &str) -> Result<Self, PublicError>;
fn from_ss58check(s: &str) -> Result<Self, PublicError> {
Self::from_ss58check_with_version(s)
.and_then(|(r, v)| match v {
Ss58AddressFormat::SubstrateAccountDirect => Ok(r),
v if v == *DEFAULT_VERSION.lock() => Ok(r),
_ => Err(PublicError::UnknownVersion),
})
}
/// Some if the string is a properly encoded SS58Check address.
fn from_ss58check_with_version(s: &str) -> Result<(Self, Ss58AddressFormat), PublicError>;
/// Some if the string is a properly encoded SS58Check address, optionally with
/// a derivation path following.
fn from_string(s: &str) -> Result<Self, PublicError> { Self::from_ss58check(s) }
fn from_string(s: &str) -> Result<Self, PublicError> {
Self::from_string_with_version(s)
.and_then(|(r, v)| match v {
Ss58AddressFormat::SubstrateAccountDirect => Ok(r),
v if v == *DEFAULT_VERSION.lock() => Ok(r),
_ => Err(PublicError::UnknownVersion),
})
}

/// Return the ss58-check string for this key.
fn to_ss58check(&self) -> String;
fn to_ss58check_with_version(&self, version: Ss58AddressFormat) -> String;
/// Return the ss58-check string for this key.
fn to_ss58check(&self) -> String { self.to_ss58check_with_version(*DEFAULT_VERSION.lock()) }
/// Some if the string is a properly encoded SS58Check address, optionally with
/// a derivation path following.
fn from_string_with_version(s: &str) -> Result<(Self, Ss58AddressFormat), PublicError> {
Self::from_ss58check_with_version(s)
}
}

#[cfg(feature = "std")]
Expand All @@ -273,31 +301,111 @@ fn ss58hash(data: &[u8]) -> blake2_rfc::blake2b::Blake2bResult {
context.finalize()
}

#[cfg(feature = "std")]
lazy_static::lazy_static! {
static ref DEFAULT_VERSION: Mutex<Ss58AddressFormat>
= Mutex::new(Ss58AddressFormat::SubstrateAccountDirect);
}

/// A known address (sub)format/network ID for SS58.
#[cfg(feature = "std")]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Ss58AddressFormat {
/// Any Substrate network, direct checksum, standard account (*25519).
SubstrateAccountDirect,
/// Polkadot Relay-chain, direct checksum, standard account (*25519).
PolkadotAccountDirect,
/// Kusama Relay-chain, direct checksum, standard account (*25519).
KusamaAccountDirect,
/// Use a manually provided numeric value.
Custom(u8),
}

#[cfg(feature = "std")]
impl From<Ss58AddressFormat> for u8 {
fn from(x: Ss58AddressFormat) -> u8 {
match x {
Ss58AddressFormat::SubstrateAccountDirect => 42,
Ss58AddressFormat::PolkadotAccountDirect => 0,
Ss58AddressFormat::KusamaAccountDirect => 2,
Ss58AddressFormat::Custom(n) => n,
}
}
}

#[cfg(feature = "std")]
impl TryFrom<u8> for Ss58AddressFormat {
type Error = ();
fn try_from(x: u8) -> Result<Ss58AddressFormat, ()> {
match x {
42 => Ok(Ss58AddressFormat::SubstrateAccountDirect),
0 => Ok(Ss58AddressFormat::PolkadotAccountDirect),
2 => Ok(Ss58AddressFormat::KusamaAccountDirect),
_ => Err(()),
}
}
}

#[cfg(feature = "std")]
impl<'a> TryFrom<&'a str> for Ss58AddressFormat {
type Error = ();
fn try_from(x: &'a str) -> Result<Ss58AddressFormat, ()> {
match x {
"substrate" => Ok(Ss58AddressFormat::SubstrateAccountDirect),
"polkadot" => Ok(Ss58AddressFormat::PolkadotAccountDirect),
"kusama" => Ok(Ss58AddressFormat::KusamaAccountDirect),
a => a.parse::<u8>().map(Ss58AddressFormat::Custom).map_err(|_| ()),
}
}
}

#[cfg(feature = "std")]
impl From<Ss58AddressFormat> for String {
fn from(x: Ss58AddressFormat) -> String {
match x {
Ss58AddressFormat::SubstrateAccountDirect => "substrate".into(),
Ss58AddressFormat::PolkadotAccountDirect => "polkadot".into(),
Ss58AddressFormat::KusamaAccountDirect => "kusama".into(),
Ss58AddressFormat::Custom(x) => x.to_string(),
}
}
}

/// Set the default "version" (actually, this is a bit of a misnomer and the version byte is
/// typically used not just to encode format/version but also network identity) that is used for
/// encoding and decoding SS58 addresses. If an unknown version is provided then it fails.
///
/// Current known "versions" are:
/// - 0 direct (payload) checksum for 32-byte *25519 Polkadot addresses.
/// - 2 direct (payload) checksum for 32-byte *25519 Polkadot Milestone 'K' addresses.
/// - 42 direct (payload) checksum for 32-byte *25519 addresses on any Substrate-based network.
#[cfg(feature = "std")]
pub fn set_default_ss58_version(version: Ss58AddressFormat) {
*DEFAULT_VERSION.lock() = version
}

#[cfg(feature = "std")]
impl<T: AsMut<[u8]> + AsRef<[u8]> + Default + Derive> Ss58Codec for T {
fn from_ss58check(s: &str) -> Result<Self, PublicError> {
fn from_ss58check_with_version(s: &str) -> Result<(Self, Ss58AddressFormat), PublicError> {
let mut res = T::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);
}
if d[0] != 42 {
// Invalid version.
return Err(PublicError::UnknownVersion);
}
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] {
// Invalid checksum.
return Err(PublicError::InvalidChecksum);
}
res.as_mut().copy_from_slice(&d[1..len+1]);
Ok(res)
Ok((res, ver))
}

fn to_ss58check(&self) -> String {
let mut v = vec![42u8];
fn to_ss58check_with_version(&self, version: Ss58AddressFormat) -> String {
let mut v = vec![version.into()];
v.extend(self.as_ref());
let r = ss58hash(&v);
v.extend(&r.as_bytes()[0..2]);
Expand All @@ -324,6 +432,28 @@ impl<T: AsMut<[u8]> + AsRef<[u8]> + Default + Derive> Ss58Codec for T {
.ok_or(PublicError::InvalidPath)
}
}

fn from_string_with_version(s: &str) -> Result<(Self, Ss58AddressFormat), PublicError> {
let re = Regex::new(r"^(?P<ss58>[\w\d]+)?(?P<path>(//?[^/]+)*)$")
.expect("constructed from known-good static value; qed");
let cap = re.captures(s).ok_or(PublicError::InvalidFormat)?;
let re_junction = Regex::new(r"/(/?[^/]+)")
.expect("constructed from known-good static value; qed");
let (addr, v) = Self::from_ss58check_with_version(
cap.name("ss58")
.map(|r| r.as_str())
.unwrap_or(DEV_ADDRESS)
)?;
if cap["path"].is_empty() {
Ok((addr, v))
} else {
let path = re_junction.captures_iter(&cap["path"])
.map(|f| DeriveJunction::from(&f[1]));
addr.derive(path)
.ok_or(PublicError::InvalidPath)
.map(|a| (a, v))
}
}
}

/// Trait suitable for typical cryptographic PKI key public type.
Expand Down
6 changes: 6 additions & 0 deletions subkey/src/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ args:
takes_value: true
required: false
help: The password for the key
- network:
short: n
long: network
takes_value: true
required: false
help: Specify a network. One of substrate (default), polkadot and kusama.
subcommands:
- generate:
about: Generate a random account
Expand Down
32 changes: 21 additions & 11 deletions subkey/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
#[cfg(feature = "bench")]
extern crate test;

use std::{str::FromStr, io::{stdin, Read}};
use std::{str::FromStr, io::{stdin, Read}, convert::TryInto};
use hex_literal::hex;
use clap::load_yaml;
use bip39::{Mnemonic, Language, MnemonicType};
use substrate_primitives::{ed25519, sr25519, hexdisplay::HexDisplay, Pair, Public, crypto::Ss58Codec, blake2_256};
use substrate_primitives::{
ed25519, sr25519, hexdisplay::HexDisplay, Pair, Public,
crypto::{Ss58Codec, set_default_ss58_version, Ss58AddressFormat}, blake2_256
};
use parity_codec::{Encode, Decode, Compact};
use sr_primitives::generic::Era;
use node_primitives::{Balance, Index, Hash};
use node_runtime::{Call, UncheckedExtrinsic, BalancesCall};
use node_runtime::{Call, UncheckedExtrinsic, /*CheckNonce, TakeFees, */BalancesCall};

mod vanity;

Expand All @@ -52,11 +55,12 @@ trait Crypto {
HexDisplay::from(&Self::public_from_pair(&pair)),
Self::ss58_from_pair(&pair)
);
} else if let Ok(public) = <Self::Pair as Pair>::Public::from_string(uri) {
println!("Public Key URI `{}` is account:\n Public key (hex): 0x{}\n Address (SS58): {}",
} else if let Ok((public, v)) = <Self::Pair as Pair>::Public::from_string_with_version(uri) {
println!("Public Key URI `{}` is account:\n Network ID/version: {}\n Public key (hex): 0x{}\n Address (SS58): {}",
uri,
String::from(Ss58AddressFormat::from(v)),
HexDisplay::from(&public.as_ref()),
public.to_ss58check()
public.to_ss58check_with_version(v)
);
} else {
println!("Invalid phrase/URI given");
Expand Down Expand Up @@ -87,6 +91,12 @@ fn execute<C: Crypto>(matches: clap::ArgMatches) where
<<C as Crypto>::Pair as Pair>::Public: Sized + AsRef<[u8]> + Ss58Codec + AsRef<<<C as Crypto>::Pair as Pair>::Public>,
{
let password = matches.value_of("password");
let maybe_network = matches.value_of("network");
if let Some(network) = maybe_network {
let v = network.try_into()
.expect("Invalid network name: must be polkadot/substrate/kusama");
set_default_ss58_version(v);
}
match matches.subcommand() {
("generate", Some(matches)) => {
// create a new randomly generated mnemonic phrase
Expand Down Expand Up @@ -120,7 +130,7 @@ fn execute<C: Crypto>(matches: clap::ArgMatches) where
let sig = pair.sign(&message);
println!("{}", hex::encode(&sig));
}
("transfer", Some(matches)) => {
/*("transfer", Some(matches)) => {
let signer = matches.value_of("from")
.expect("parameter is required; thus it can't be None; qed");
let signer = Sr25519::pair_from_suri(signer, password);
Expand All @@ -147,7 +157,7 @@ fn execute<C: Crypto>(matches: clap::ArgMatches) where
"elm" => hex!["10c08714a10c7da78f40a60f6f732cf0dba97acfb5e2035445b032386157d5c3"].into(),
"alex" => hex!["dcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b"].into(),
h => hex::decode(h).ok().and_then(|x| Decode::decode(&mut &x[..]))
.expect("Invalid genesis hash or unrecognised chain identifier"),
.expect("Invalid genesis hash or unrecognised chain identifier"),
};
println!("Using a genesis hash of {}", HexDisplay::from(&genesis_hash.as_ref()));
Expand All @@ -161,11 +171,11 @@ fn execute<C: Crypto>(matches: clap::ArgMatches) where
signer.sign(payload)
});
let extrinsic = UncheckedExtrinsic::new_signed(
index,
raw_payload.1,
signer.public().into(),
signature.into(),
era,
(CheckNonce(index), TakeFees(0)),
);
println!("0x{}", hex::encode(&extrinsic.encode()));
}
Expand Down Expand Up @@ -202,15 +212,15 @@ fn execute<C: Crypto>(matches: clap::ArgMatches) where
);
let extrinsic = UncheckedExtrinsic::new_signed(
index,
raw_payload.1,
signer.public().into(),
signature.into(),
era,
(CheckNonce(index), TakeFees(0)),
);
println!("0x{}", hex::encode(&extrinsic.encode()));
}
}*/
("verify", Some(matches)) => {
let sig_data = matches.value_of("sig")
.expect("signature parameter is required; thus it can't be None; qed");
Expand Down

0 comments on commit cf98501

Please sign in to comment.