feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo
- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt - Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk - Convert git dependencies to path dependencies - Add vendor crates to workspace members - Remove test/example crates from vendor (not needed for SDK) - Fix feature propagation issues detected by zepter - Fix workspace inheritance for internal dependencies - All 606 crates now in workspace - All 6919 internal dependency links verified correct - No git dependencies remaining
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
// This code is taken from sp_core::crypto::DeriveJunction. The logic should be identical,
|
||||
// though the API is tweaked a touch.
|
||||
|
||||
/// The length of the junction identifier. Note that this is also referred to as the
|
||||
/// `CHAIN_CODE_LENGTH` in the context of Schnorrkel.
|
||||
pub const JUNCTION_ID_LEN: usize = 32;
|
||||
|
||||
/// A since derivation junction description. It is the single parameter used when creating
|
||||
/// a new secret key from an existing secret key and, in the case of `SoftRaw` and `SoftIndex`
|
||||
/// a new public key from an existing public key.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Encode, Decode)]
|
||||
pub enum DeriveJunction {
|
||||
/// Soft (vanilla) derivation. Public keys have a correspondent derivation.
|
||||
Soft([u8; JUNCTION_ID_LEN]),
|
||||
/// Hard ("hardened") derivation. Public keys do not have a correspondent derivation.
|
||||
Hard([u8; JUNCTION_ID_LEN]),
|
||||
}
|
||||
|
||||
impl DeriveJunction {
|
||||
/// Consume self to return a soft derive junction with the same chain code.
|
||||
pub fn soften(self) -> Self {
|
||||
DeriveJunction::Soft(self.into_inner())
|
||||
}
|
||||
|
||||
/// Consume self to return a hard derive junction with the same chain code.
|
||||
pub fn harden(self) -> Self {
|
||||
DeriveJunction::Hard(self.into_inner())
|
||||
}
|
||||
|
||||
/// Create a new soft (vanilla) DeriveJunction from a given, encodable, value.
|
||||
///
|
||||
/// If you need a hard junction, use `hard()`.
|
||||
pub fn soft<T: Encode>(index: T) -> Self {
|
||||
let mut cc: [u8; JUNCTION_ID_LEN] = Default::default();
|
||||
index.using_encoded(|data| {
|
||||
if data.len() > JUNCTION_ID_LEN {
|
||||
cc.copy_from_slice(&pezsp_crypto_hashing::blake2_256(data));
|
||||
} else {
|
||||
cc[0..data.len()].copy_from_slice(data);
|
||||
}
|
||||
});
|
||||
DeriveJunction::Soft(cc)
|
||||
}
|
||||
|
||||
/// Create a new hard (hardened) DeriveJunction from a given, encodable, value.
|
||||
///
|
||||
/// If you need a soft junction, use `soft()`.
|
||||
pub fn hard<T: Encode>(index: T) -> Self {
|
||||
Self::soft(index).harden()
|
||||
}
|
||||
|
||||
/// Consume self to return the chain code.
|
||||
pub fn into_inner(self) -> [u8; JUNCTION_ID_LEN] {
|
||||
match self {
|
||||
DeriveJunction::Hard(c) | DeriveJunction::Soft(c) => c,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the inner junction id.
|
||||
pub fn inner(&self) -> &[u8; JUNCTION_ID_LEN] {
|
||||
match self {
|
||||
DeriveJunction::Hard(c) | DeriveJunction::Soft(c) => c,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the junction is soft.
|
||||
pub fn is_soft(&self) -> bool {
|
||||
matches!(*self, DeriveJunction::Soft(_))
|
||||
}
|
||||
|
||||
/// Return `true` if the junction is hard.
|
||||
pub fn is_hard(&self) -> bool {
|
||||
matches!(*self, DeriveJunction::Hard(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> From<T> for DeriveJunction {
|
||||
fn from(j: T) -> DeriveJunction {
|
||||
let j = j.as_ref();
|
||||
let (code, hard) =
|
||||
if let Some(stripped) = j.strip_prefix('/') { (stripped, true) } else { (j, false) };
|
||||
|
||||
let res = if let Ok(n) = str::parse::<u64>(code) {
|
||||
// number
|
||||
DeriveJunction::soft(n)
|
||||
} else {
|
||||
// something else
|
||||
DeriveJunction::soft(code)
|
||||
};
|
||||
|
||||
if hard { res.harden() } else { res }
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
// The crypto module contains code adapted from sp_core::crypto.
|
||||
|
||||
mod derive_junction;
|
||||
mod secret_uri;
|
||||
|
||||
// No need for the cfg other than to avoid an unused_imports lint warning.
|
||||
#[cfg(any(feature = "sr25519", feature = "ecdsa"))]
|
||||
mod seed_from_entropy;
|
||||
|
||||
pub use derive_junction::DeriveJunction;
|
||||
pub use secret_uri::{DEV_PHRASE, SecretUri, SecretUriError};
|
||||
|
||||
#[cfg(any(feature = "sr25519", feature = "ecdsa"))]
|
||||
pub use seed_from_entropy::seed_from_entropy;
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::DeriveJunction;
|
||||
use alloc::{string::ToString, vec::Vec};
|
||||
use regex::Regex;
|
||||
use secrecy::SecretString;
|
||||
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
// This code is taken from sp_core::crypto::DeriveJunction. The logic should be identical,
|
||||
// though the code is tweaked a touch!
|
||||
|
||||
/// A secret uri (`SURI`) that can be used to generate a key pair.
|
||||
///
|
||||
/// The `SURI` can be parsed from a string. The string takes this form:
|
||||
///
|
||||
/// ```text
|
||||
/// phrase/path0/path1///password
|
||||
/// 111111 22222 22222 33333333
|
||||
/// ```
|
||||
///
|
||||
/// Where:
|
||||
/// - 1 denotes a phrase or hex string. If this is not provided, the [`DEV_PHRASE`] is used instead.
|
||||
/// - 2's denote optional "derivation junctions" which are used to derive keys. Each of these is
|
||||
/// separated by "/". A derivation junction beginning with "/" (ie "//" in the original string) is
|
||||
/// a "hard" path.
|
||||
/// - 3 denotes an optional password which is used in conjunction with the phrase provided in 1 to
|
||||
/// generate an initial key. If hex is provided for 1, it's ignored.
|
||||
///
|
||||
/// Notes:
|
||||
/// - If 1 is a `0x` prefixed 64-digit hex string, then we'll interpret it as hex, and treat the hex
|
||||
/// bytes as a seed/MiniSecretKey directly, ignoring any password.
|
||||
/// - Else if the phrase part is a valid BIP-39 phrase, we'll use the phrase (and password, if
|
||||
/// provided) to generate a seed/MiniSecretKey.
|
||||
/// - Uris like "//Alice" correspond to keys derived from a DEV_PHRASE, since no phrase part is
|
||||
/// given.
|
||||
///
|
||||
/// There is no correspondence mapping between `SURI` strings and the keys they represent.
|
||||
/// Two different non-identical strings can actually lead to the same secret being derived.
|
||||
/// Notably, integer junction indices may be legally prefixed with arbitrary number of zeros.
|
||||
/// Similarly an empty password (ending the `SURI` with `///`) is perfectly valid and will
|
||||
/// generally be equivalent to no password at all.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Parse [`DEV_PHRASE`] secret URI with junction:
|
||||
///
|
||||
/// ```
|
||||
/// # use pezkuwi_subxt_signer::{SecretUri, DeriveJunction, DEV_PHRASE, ExposeSecret};
|
||||
/// # use std::str::FromStr;
|
||||
/// let suri = SecretUri::from_str("//Alice").expect("Parse SURI");
|
||||
///
|
||||
/// assert_eq!(vec![DeriveJunction::from("Alice").harden()], suri.junctions);
|
||||
/// assert_eq!(DEV_PHRASE, suri.phrase.expose_secret());
|
||||
/// assert!(suri.password.is_none());
|
||||
/// ```
|
||||
///
|
||||
/// Parse [`DEV_PHRASE`] secret URI with junction and password:
|
||||
///
|
||||
/// ```
|
||||
/// # use pezkuwi_subxt_signer::{SecretUri, DeriveJunction, DEV_PHRASE, ExposeSecret};
|
||||
/// # use std::str::FromStr;
|
||||
/// let suri = SecretUri::from_str("//Alice///SECRET_PASSWORD").expect("Parse SURI");
|
||||
///
|
||||
/// assert_eq!(vec![DeriveJunction::from("Alice").harden()], suri.junctions);
|
||||
/// assert_eq!(DEV_PHRASE, suri.phrase.expose_secret());
|
||||
/// assert_eq!("SECRET_PASSWORD", suri.password.unwrap().expose_secret());
|
||||
/// ```
|
||||
///
|
||||
/// Parse [`DEV_PHRASE`] secret URI with hex phrase and junction:
|
||||
///
|
||||
/// ```
|
||||
/// # use pezkuwi_subxt_signer::{SecretUri, DeriveJunction, DEV_PHRASE, ExposeSecret};
|
||||
/// # use std::str::FromStr;
|
||||
/// let suri = SecretUri::from_str("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a//Alice").expect("Parse SURI");
|
||||
///
|
||||
/// assert_eq!(vec![DeriveJunction::from("Alice").harden()], suri.junctions);
|
||||
/// assert_eq!("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a", suri.phrase.expose_secret());
|
||||
/// assert!(suri.password.is_none());
|
||||
/// ```
|
||||
pub struct SecretUri {
|
||||
/// The phrase to derive the private key.
|
||||
///
|
||||
/// This can either be a 64-bit hex string or a BIP-39 key phrase.
|
||||
pub phrase: SecretString,
|
||||
/// Optional password as given as part of the uri.
|
||||
pub password: Option<SecretString>,
|
||||
/// The junctions as part of the uri.
|
||||
pub junctions: Vec<DeriveJunction>,
|
||||
}
|
||||
|
||||
impl core::str::FromStr for SecretUri {
|
||||
type Err = SecretUriError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let cap = secret_phrase_regex().captures(s).ok_or(SecretUriError::InvalidFormat)?;
|
||||
|
||||
let junctions = junction_regex()
|
||||
.captures_iter(&cap["path"])
|
||||
.map(|f| DeriveJunction::from(&f[1]))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let phrase = cap.name("phrase").map(|r| r.as_str()).unwrap_or(DEV_PHRASE);
|
||||
let password = cap.name("password");
|
||||
|
||||
Ok(Self {
|
||||
phrase: SecretString::from(phrase.to_string()),
|
||||
password: password.map(|v| SecretString::from(v.as_str().to_string())),
|
||||
junctions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// This is returned if `FromStr` cannot parse a string into a `SecretUri`.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, DeriveError)]
|
||||
pub enum SecretUriError {
|
||||
/// Parsing the secret URI from a string failed; wrong format.
|
||||
#[error("Invalid secret phrase format")]
|
||||
InvalidFormat,
|
||||
}
|
||||
|
||||
once_static_cloned! {
|
||||
/// Interpret a phrase like:
|
||||
///
|
||||
/// ```text
|
||||
/// foo bar wibble /path0/path1///password
|
||||
/// 11111111111111 222222222222 33333333
|
||||
/// ```
|
||||
/// Where 1 is the phrase, 2 the path and 3 the password.
|
||||
/// Taken from `sp_core::crypto::SECRET_PHRASE_REGEX`.
|
||||
fn secret_phrase_regex() -> regex::Regex {
|
||||
Regex::new(r"^(?P<phrase>[\d\w ]+)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$").unwrap()
|
||||
}
|
||||
|
||||
/// Interpret a part of a path into a "junction":
|
||||
///
|
||||
/// ```text
|
||||
/// //foo/bar/wibble
|
||||
/// 1111 222 333333
|
||||
/// ```
|
||||
/// Where the numbers denote matching junctions.
|
||||
///
|
||||
/// The leading "/" deliminates each part, and then a "/" beginning
|
||||
/// a path piece denotes that it's a "hard" path. Taken from
|
||||
/// `sp_core::crypto::JUNCTION_REGEX`.
|
||||
fn junction_regex() -> regex::Regex {
|
||||
Regex::new(r"/(/?[^/]+)").unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// The root phrase for our publicly known keys.
|
||||
pub const DEV_PHRASE: &str =
|
||||
"bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use alloc::string::String;
|
||||
use hmac::Hmac;
|
||||
use pbkdf2::pbkdf2;
|
||||
use sha2::Sha512;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// This is taken from `bizinikiwi-bip39` so that we can keep dependencies in line, and
|
||||
/// is the same logic that sp-core uses to go from mnemonic entropy to seed. Returns
|
||||
/// `None` if invalid length.
|
||||
#[allow(dead_code)]
|
||||
pub fn seed_from_entropy(entropy: &[u8], password: &str) -> Option<[u8; 64]> {
|
||||
if entropy.len() < 16 || entropy.len() > 32 || entropy.len() % 4 != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut salt = String::with_capacity(8 + password.len());
|
||||
salt.push_str("mnemonic");
|
||||
salt.push_str(password);
|
||||
|
||||
let mut seed = [0u8; 64];
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(entropy, salt.as_bytes(), 2048, &mut seed).ok()?;
|
||||
|
||||
salt.zeroize();
|
||||
|
||||
Some(seed)
|
||||
}
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! An ecdsa keypair implementation.
|
||||
use codec::Encode;
|
||||
|
||||
use crate::crypto::{DeriveJunction, SecretUri, seed_from_entropy};
|
||||
use core::str::FromStr;
|
||||
use hex::FromHex;
|
||||
use secp256k1::{Message, Secp256k1, SecretKey, ecdsa::RecoverableSignature};
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
const SECRET_KEY_LENGTH: usize = 32;
|
||||
|
||||
/// Seed bytes used to generate a key pair.
|
||||
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
|
||||
|
||||
/// A signature generated by [`Keypair::sign()`]. These bytes are equivalent
|
||||
/// to a Bizinikiwi `MultiSignature::Ecdsa(bytes)`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Signature(pub [u8; 65]);
|
||||
|
||||
impl AsRef<[u8]> for Signature {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The (compressed) public key for an [`Keypair`] key pair.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PublicKey(pub [u8; 33]);
|
||||
|
||||
impl AsRef<[u8]> for PublicKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An ecdsa keypair implementation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Keypair(pub secp256k1::Keypair);
|
||||
|
||||
impl Keypair {
|
||||
/// Create an ecdsa keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ SecretUri, ecdsa::Keypair };
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// let uri = SecretUri::from_str("//Alice").unwrap();
|
||||
/// let keypair = Keypair::from_uri(&uri).unwrap();
|
||||
///
|
||||
/// keypair.sign(b"Hello world!");
|
||||
/// ```
|
||||
pub fn from_uri(uri: &SecretUri) -> Result<Self, Error> {
|
||||
let SecretUri { junctions, phrase, password } = uri;
|
||||
|
||||
// If the phrase is hex, convert bytes directly into a seed, ignoring password.
|
||||
// Else, parse the phrase string taking the password into account. This is
|
||||
// the same approach taken in sp_core::crypto::Pair::from_string_with_seed.
|
||||
let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") {
|
||||
let seed = SecretKeyBytes::from_hex(hex_str)?;
|
||||
Self::from_secret_key(seed)?
|
||||
} else {
|
||||
let phrase = bip39::Mnemonic::from_str(phrase.expose_secret())?;
|
||||
let pass_str = password.as_ref().map(|p| p.expose_secret());
|
||||
Self::from_phrase(&phrase, pass_str)?
|
||||
};
|
||||
|
||||
// Now, use any "junctions" to derive a new key from this root key.
|
||||
key.derive(junctions.iter().copied())
|
||||
}
|
||||
|
||||
/// Create an ecdsa keypair from a BIP-39 mnemonic phrase and optional password.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair };
|
||||
///
|
||||
/// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
||||
/// let mnemonic = Mnemonic::parse(phrase).unwrap();
|
||||
/// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap();
|
||||
///
|
||||
/// keypair.sign(b"Hello world!");
|
||||
/// ```
|
||||
pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result<Self, Error> {
|
||||
let (arr, len) = mnemonic.to_entropy_array();
|
||||
let big_seed =
|
||||
seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?;
|
||||
|
||||
let secret_key_bytes: SecretKeyBytes =
|
||||
big_seed[..SECRET_KEY_LENGTH].try_into().expect("should be valid Seed");
|
||||
|
||||
Self::from_secret_key(secret_key_bytes)
|
||||
}
|
||||
|
||||
/// Turn a 32 byte seed into a keypair.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This will only be secure if the seed is secure!
|
||||
pub fn from_secret_key(secret_key: SecretKeyBytes) -> Result<Self, Error> {
|
||||
let secret = SecretKey::from_slice(&secret_key).map_err(|_| Error::InvalidSeed)?;
|
||||
Ok(Self(secp256k1::Keypair::from_secret_key(&Secp256k1::signing_only(), &secret)))
|
||||
}
|
||||
|
||||
/// Derive a child key from this one given a series of junctions.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair, DeriveJunction };
|
||||
///
|
||||
/// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
||||
/// let mnemonic = Mnemonic::parse(phrase).unwrap();
|
||||
/// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap();
|
||||
///
|
||||
/// // Equivalent to the URI path '//Alice//stash':
|
||||
/// let new_keypair = keypair.derive([
|
||||
/// DeriveJunction::hard("Alice"),
|
||||
/// DeriveJunction::hard("stash")
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn derive<Js: IntoIterator<Item = DeriveJunction>>(
|
||||
&self,
|
||||
junctions: Js,
|
||||
) -> Result<Self, Error> {
|
||||
let mut acc = self.0.secret_key().clone().secret_bytes();
|
||||
for junction in junctions {
|
||||
match junction {
|
||||
DeriveJunction::Soft(_) => return Err(Error::SoftJunction),
|
||||
DeriveJunction::Hard(junction_bytes) =>
|
||||
acc = ("Secp256k1HDKD", acc, junction_bytes)
|
||||
.using_encoded(pezsp_crypto_hashing::blake2_256),
|
||||
}
|
||||
}
|
||||
Self::from_secret_key(acc)
|
||||
}
|
||||
|
||||
/// Obtain the [`PublicKey`] part of this key pair, which can be used in calls to [`verify()`].
|
||||
/// or otherwise converted into an address. In case of ECDSA, the public key bytes are not
|
||||
/// equivalent to a Bizinikiwi `AccountId32`. They have to be hashed to obtain `AccountId32`.
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
PublicKey(self.0.public_key().serialize())
|
||||
}
|
||||
|
||||
/// Obtain the [`SecretKey`] part of this key pair. This should be kept secret.
|
||||
pub fn secret_key(&self) -> SecretKeyBytes {
|
||||
*self.0.secret_key().as_ref()
|
||||
}
|
||||
|
||||
/// Sign some message. These bytes can be used directly in a Bizinikiwi
|
||||
/// `MultiSignature::Ecdsa(..)`.
|
||||
pub fn sign(&self, message: &[u8]) -> Signature {
|
||||
self.sign_prehashed(&pezsp_crypto_hashing::blake2_256(message))
|
||||
}
|
||||
|
||||
/// Signs a pre-hashed message.
|
||||
pub fn sign_prehashed(&self, message_hash: &[u8; 32]) -> Signature {
|
||||
let wrapped = Message::from_digest_slice(message_hash).expect("Message is 32 bytes; qed");
|
||||
Signature(internal::sign(&self.0.secret_key(), &wrapped))
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that some signature for a message was created by the owner of the [`PublicKey`].
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, ecdsa };
|
||||
///
|
||||
/// let keypair = ecdsa::dev::alice();
|
||||
/// let message = b"Hello!";
|
||||
///
|
||||
/// let signature = keypair.sign(message);
|
||||
/// let public_key = keypair.public_key();
|
||||
/// assert!(ecdsa::verify(&signature, message, &public_key));
|
||||
/// ```
|
||||
pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool {
|
||||
let message_hash = pezsp_crypto_hashing::blake2_256(message.as_ref());
|
||||
let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed");
|
||||
|
||||
internal::verify(&sig.0, &wrapped, pubkey)
|
||||
}
|
||||
|
||||
pub(crate) mod internal {
|
||||
use super::*;
|
||||
|
||||
pub fn sign(secret_key: &secp256k1::SecretKey, message: &Message) -> [u8; 65] {
|
||||
let recsig: RecoverableSignature =
|
||||
Secp256k1::signing_only().sign_ecdsa_recoverable(message, secret_key);
|
||||
let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact();
|
||||
let mut signature_bytes: [u8; 65] = [0; 65];
|
||||
signature_bytes[..64].copy_from_slice(&sig);
|
||||
signature_bytes[64] = (i32::from(recid) & 0xFF) as u8;
|
||||
signature_bytes
|
||||
}
|
||||
|
||||
pub fn verify(sig: &[u8; 65], message: &Message, pubkey: &PublicKey) -> bool {
|
||||
let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig[..64]) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(public) = secp256k1::PublicKey::from_slice(&pubkey.0) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
Secp256k1::verification_only()
|
||||
.verify_ecdsa(message, &signature, &public)
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// An error handed back if creating a keypair fails.
|
||||
#[derive(Debug, PartialEq, DeriveError)]
|
||||
pub enum Error {
|
||||
/// Invalid seed.
|
||||
#[error("Invalid seed (was it the wrong length?)")]
|
||||
InvalidSeed,
|
||||
/// Invalid seed.
|
||||
#[error("Invalid seed for ECDSA, contained soft junction")]
|
||||
SoftJunction,
|
||||
/// Invalid phrase.
|
||||
#[error("Cannot parse phrase: {0}")]
|
||||
Phrase(bip39::Error),
|
||||
/// Invalid hex.
|
||||
#[error("Cannot parse hex string: {0}")]
|
||||
Hex(hex::FromHexError),
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for Error {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
Error::Hex(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bip39::Error> for Error {
|
||||
fn from(err: bip39::Error) -> Self {
|
||||
Error::Phrase(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dev accounts, helpful for testing but not to be used in production,
|
||||
/// since the secret keys are known.
|
||||
pub mod dev {
|
||||
use super::*;
|
||||
|
||||
once_static_cloned! {
|
||||
/// Equivalent to `{DEV_PHRASE}//Alice`.
|
||||
pub fn alice() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Alice").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Bob`.
|
||||
pub fn bob() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Bob").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Charlie`.
|
||||
pub fn charlie() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Charlie").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Dave`.
|
||||
pub fn dave() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Dave").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Eve`.
|
||||
pub fn eve() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Eve").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Ferdie`.
|
||||
pub fn ferdie() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Ferdie").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//One`.
|
||||
pub fn one() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//One").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Two`.
|
||||
pub fn two() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Two").unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make `Keypair` usable to sign transactions in Subxt. This is optional so that
|
||||
// `subxt-signer` can be used entirely independently of Subxt.
|
||||
#[cfg(feature = "subxt")]
|
||||
mod subxt_compat {
|
||||
use super::*;
|
||||
|
||||
use pezkuwi_subxt_core::{
|
||||
config::Config,
|
||||
tx::signer::Signer as SignerT,
|
||||
utils::{AccountId32, MultiAddress, MultiSignature},
|
||||
};
|
||||
|
||||
impl From<Signature> for MultiSignature {
|
||||
fn from(value: Signature) -> Self {
|
||||
MultiSignature::Ecdsa(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for AccountId32 {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
value.to_account_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<PublicKey> for MultiAddress<AccountId32, T> {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
value.to_address()
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// A shortcut to obtain an [`AccountId32`] from a [`PublicKey`].
|
||||
/// We often want this type, and using this method avoids any
|
||||
/// ambiguous type resolution issues.
|
||||
pub fn to_account_id(self) -> AccountId32 {
|
||||
AccountId32(pezsp_crypto_hashing::blake2_256(&self.0))
|
||||
}
|
||||
/// A shortcut to obtain a [`MultiAddress`] from a [`PublicKey`].
|
||||
/// We often want this type, and using this method avoids any
|
||||
/// ambiguous type resolution issues.
|
||||
pub fn to_address<T>(self) -> MultiAddress<AccountId32, T> {
|
||||
MultiAddress::Id(self.to_account_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> SignerT<T> for Keypair
|
||||
where
|
||||
T::AccountId: From<PublicKey>,
|
||||
T::Address: From<PublicKey>,
|
||||
T::Signature: From<Signature>,
|
||||
{
|
||||
fn account_id(&self) -> T::AccountId {
|
||||
self.public_key().into()
|
||||
}
|
||||
|
||||
fn sign(&self, signer_payload: &[u8]) -> T::Signature {
|
||||
self.sign(signer_payload).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
use sp_core::{self, crypto::Pair as _, ecdsa::Pair as SpPair};
|
||||
|
||||
#[test]
|
||||
fn check_from_phrase_matches() {
|
||||
for _ in 0..20 {
|
||||
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None);
|
||||
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
|
||||
let pair = Keypair::from_phrase(&phrase, None).expect("should be valid");
|
||||
|
||||
assert_eq!(sp_pair.public().0, pair.public_key().0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_from_phrase_with_password_matches() {
|
||||
for _ in 0..20 {
|
||||
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
|
||||
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
|
||||
let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
|
||||
|
||||
assert_eq!(sp_pair.public().0, pair.public_key().0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_from_secret_uri_matches() {
|
||||
// Some derive junctions to check that the logic there aligns:
|
||||
let uri_paths = ["//bar", "//0001", "//1", "//0001", "//foo//bar//wibble"];
|
||||
|
||||
for i in 0..2 {
|
||||
for path in &uri_paths {
|
||||
// Build an sp_core::Pair that includes a phrase, path and password:
|
||||
let password = format!("Testing{i}");
|
||||
let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some(&password));
|
||||
let uri = format!("{phrase}{path}///{password}");
|
||||
let sp_pair = SpPair::from_string(&uri, None).expect("should be valid");
|
||||
|
||||
// Now build a local Keypair using the equivalent API:
|
||||
let uri = SecretUri::from_str(&uri).expect("should be valid secret URI");
|
||||
let pair = Keypair::from_uri(&uri).expect("should be valid");
|
||||
|
||||
// They should match:
|
||||
assert_eq!(sp_pair.public().0, pair.public_key().0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_derive_errs_with_soft_junction() {
|
||||
let uri_paths = ["/bar", "/1", "//foo//bar/wibble"];
|
||||
for path in &uri_paths {
|
||||
let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None);
|
||||
let uri = format!("{phrase}{path}");
|
||||
let uri = SecretUri::from_str(&uri).expect("should be valid secret URI");
|
||||
let result = Keypair::from_uri(&uri);
|
||||
assert_eq!(result.err(), Some(Error::SoftJunction));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_signing_and_verifying_matches() {
|
||||
use sp_core::ecdsa::Signature as SpSignature;
|
||||
|
||||
for _ in 0..20 {
|
||||
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
|
||||
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
|
||||
let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
|
||||
|
||||
let message = b"Hello world";
|
||||
let sp_sig = sp_pair.sign(message).0;
|
||||
let sig: [u8; 65] = pair.sign(message).0;
|
||||
|
||||
assert!(SpPair::verify(&SpSignature::from(sig), message, &sp_pair.public(),));
|
||||
assert!(verify(&Signature(sp_sig), message, &pair.public_key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_hex_uris() {
|
||||
// Hex URIs seem to ignore the password on sp_core and here. Check that this is consistent.
|
||||
let uri_str =
|
||||
"0x1122334455667788112233445566778811223344556677881122334455667788///SomePassword";
|
||||
|
||||
let uri = SecretUri::from_str(uri_str).expect("should be valid");
|
||||
let pair = Keypair::from_uri(&uri).expect("should be valid");
|
||||
let sp_pair = SpPair::from_string(uri_str, None).expect("should be valid");
|
||||
|
||||
assert_eq!(pair.public_key().0, sp_pair.public().0);
|
||||
}
|
||||
}
|
||||
+741
@@ -0,0 +1,741 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! An ethereum keypair implementation.
|
||||
|
||||
use crate::ecdsa;
|
||||
use alloc::format;
|
||||
use core::str::FromStr;
|
||||
use keccak_hash::keccak;
|
||||
use secp256k1::Message;
|
||||
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
const SECRET_KEY_LENGTH: usize = 32;
|
||||
|
||||
/// Bytes representing a private key.
|
||||
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
|
||||
|
||||
/// The public key for an [`Keypair`] key pair. This is the uncompressed variant of
|
||||
/// [`ecdsa::PublicKey`].
|
||||
pub struct PublicKey(pub [u8; 65]);
|
||||
|
||||
impl AsRef<[u8]> for PublicKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An ethereum keypair implementation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Keypair(ecdsa::Keypair);
|
||||
|
||||
impl From<ecdsa::Keypair> for Keypair {
|
||||
fn from(kp: ecdsa::Keypair) -> Self {
|
||||
Self(kp)
|
||||
}
|
||||
}
|
||||
|
||||
impl Keypair {
|
||||
/// Create a keypair from a BIP-39 mnemonic phrase, optional password, account index, and
|
||||
/// derivation type.
|
||||
///
|
||||
/// **Note:** if the `std` feature is not enabled, we won't attempt to normalize the provided
|
||||
/// password to NFKD first, and so this is your responsibility. This is not a concern if only
|
||||
/// ASCII characters are used in the password.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, eth::{ Keypair, DerivationPath } };
|
||||
///
|
||||
/// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
||||
/// let mnemonic = Mnemonic::parse(phrase).unwrap();
|
||||
/// let keypair = Keypair::from_phrase(&mnemonic, None, DerivationPath::eth(0,0)).unwrap();
|
||||
///
|
||||
/// keypair.sign(b"Hello world!");
|
||||
/// ```
|
||||
pub fn from_phrase(
|
||||
mnemonic: &bip39::Mnemonic,
|
||||
password: Option<&str>,
|
||||
derivation_path: DerivationPath,
|
||||
) -> Result<Self, Error> {
|
||||
// `to_seed` isn't available unless std is enabled in bip39.
|
||||
#[cfg(feature = "std")]
|
||||
let seed = mnemonic.to_seed(password.unwrap_or(""));
|
||||
#[cfg(not(feature = "std"))]
|
||||
let seed = mnemonic.to_seed_normalized(password.unwrap_or(""));
|
||||
|
||||
// TODO: Currently, we use bip32 to derive private keys which under the hood uses
|
||||
// the Rust k256 crate. We _also_ use the secp256k1 crate (which is very similar).
|
||||
// It'd be great if we could 100% use just one of the two crypto libs. bip32 has
|
||||
// a feature flag to use secp256k1, but it's unfortunately a different version (older)
|
||||
// than ours.
|
||||
let private = bip32::XPrv::derive_from_path(seed, &derivation_path.inner)
|
||||
.map_err(|_| Error::DeriveFromPath)?;
|
||||
|
||||
Keypair::from_secret_key(private.to_bytes())
|
||||
}
|
||||
|
||||
/// Turn a 16, 32 or 64 byte seed into a keypair.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This will only be secure if the seed is secure!
|
||||
pub fn from_seed(seed: &[u8]) -> Result<Self, Error> {
|
||||
let private = bip32::XPrv::new(seed).map_err(|_| Error::InvalidSeed)?;
|
||||
Keypair::from_secret_key(private.to_bytes())
|
||||
}
|
||||
|
||||
/// Turn a 32 byte secret key into a keypair.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This will only be secure if the secret key is secure!
|
||||
pub fn from_secret_key(secret_key: SecretKeyBytes) -> Result<Self, Error> {
|
||||
ecdsa::Keypair::from_secret_key(secret_key)
|
||||
.map(Self)
|
||||
.map_err(|_| Error::InvalidSeed)
|
||||
}
|
||||
|
||||
/// Obtain the [`ecdsa::SecretKeyBytes`] of this keypair.
|
||||
pub fn secret_key(&self) -> SecretKeyBytes {
|
||||
self.0.secret_key()
|
||||
}
|
||||
|
||||
/// Obtain the [`eth::PublicKey`] of this keypair.
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
let uncompressed = self.0.0.public_key().serialize_uncompressed();
|
||||
PublicKey(uncompressed)
|
||||
}
|
||||
|
||||
/// Signs an arbitrary message payload.
|
||||
pub fn sign(&self, signer_payload: &[u8]) -> Signature {
|
||||
self.sign_prehashed(&keccak(signer_payload).0)
|
||||
}
|
||||
|
||||
/// Signs a pre-hashed message.
|
||||
pub fn sign_prehashed(&self, message_hash: &[u8; 32]) -> Signature {
|
||||
Signature(self.0.sign_prehashed(message_hash).0)
|
||||
}
|
||||
}
|
||||
/// A derivation path. This can be parsed from a valid derivation path string like
|
||||
/// `"m/44'/60'/0'/0/0"`, or we can construct one using the helpers [`DerivationPath::empty()`]
|
||||
/// and [`DerivationPath::eth()`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DerivationPath {
|
||||
inner: bip32::DerivationPath,
|
||||
}
|
||||
|
||||
impl DerivationPath {
|
||||
/// An empty derivation path (in other words, just use the master-key as is).
|
||||
pub fn empty() -> Self {
|
||||
let inner = bip32::DerivationPath::from_str("m").unwrap();
|
||||
DerivationPath { inner }
|
||||
}
|
||||
|
||||
/// A BIP44 Ethereum compatible derivation using the path "m/44'/60'/account'/0/address_index".
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the `account` or `address_index` provided are >= 2^31.
|
||||
pub fn eth(account: u32, address_index: u32) -> Self {
|
||||
assert!(account < bip32::ChildNumber::HARDENED_FLAG, "account must be less than 2^31");
|
||||
assert!(
|
||||
address_index < bip32::ChildNumber::HARDENED_FLAG,
|
||||
"address_index must be less than 2^31"
|
||||
);
|
||||
|
||||
let derivation_string = format!("m/44'/60'/{account}'/0/{address_index}");
|
||||
let inner = bip32::DerivationPath::from_str(&derivation_string).unwrap();
|
||||
DerivationPath { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DerivationPath {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let inner = bip32::DerivationPath::from_str(s).map_err(|_| Error::DeriveFromPath)?;
|
||||
Ok(DerivationPath { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// A signature generated by [`Keypair::sign()`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode, codec::Decode)]
|
||||
pub struct Signature(pub [u8; 65]);
|
||||
|
||||
impl AsRef<[u8; 65]> for Signature {
|
||||
fn as_ref(&self) -> &[u8; 65] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that some signature for a message was created by the owner of the [`PublicKey`].
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, eth };
|
||||
///
|
||||
/// let keypair = eth::dev::alith();
|
||||
/// let message = b"Hello!";
|
||||
///
|
||||
/// let signature = keypair.sign(message);
|
||||
/// let public_key = keypair.public_key();
|
||||
/// assert!(eth::verify(&signature, message, &public_key));
|
||||
/// ```
|
||||
pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool {
|
||||
let message_hash = keccak(message.as_ref());
|
||||
let wrapped =
|
||||
Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed");
|
||||
let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.as_ref()[..64]) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(pk) = secp256k1::PublicKey::from_slice(&pubkey.0) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
secp256k1::Secp256k1::verification_only()
|
||||
.verify_ecdsa(&wrapped, &signature, &pk)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// An error handed back if creating a keypair fails.
|
||||
#[derive(Debug, PartialEq, DeriveError)]
|
||||
pub enum Error {
|
||||
/// Invalid seed.
|
||||
#[error("Invalid seed (was it the wrong length?)")]
|
||||
InvalidSeed,
|
||||
/// Invalid derivation path.
|
||||
#[error("Could not derive from path; some values in the path may have been >= 2^31?")]
|
||||
DeriveFromPath,
|
||||
}
|
||||
|
||||
/// Dev accounts, helpful for testing but not to be used in production,
|
||||
/// since the secret keys are known.
|
||||
pub mod dev {
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::DEV_PHRASE;
|
||||
|
||||
use super::*;
|
||||
|
||||
once_static_cloned! {
|
||||
pub fn alith() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 0)).unwrap()
|
||||
}
|
||||
pub fn baltathar() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 1)).unwrap()
|
||||
}
|
||||
pub fn charleth() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 2)).unwrap()
|
||||
}
|
||||
pub fn dorothy() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 3)).unwrap()
|
||||
}
|
||||
pub fn ethan() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 4)).unwrap()
|
||||
}
|
||||
pub fn faith() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 5)).unwrap()
|
||||
}
|
||||
pub fn gareth() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 6)).unwrap()
|
||||
}
|
||||
pub fn heather() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 7)).unwrap()
|
||||
}
|
||||
pub fn ithelia() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 8)).unwrap()
|
||||
}
|
||||
pub fn jethro() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 9)).unwrap()
|
||||
}
|
||||
pub fn keith() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 10)).unwrap()
|
||||
}
|
||||
pub fn luther() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 11)).unwrap()
|
||||
}
|
||||
pub fn martha() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 12)).unwrap()
|
||||
}
|
||||
pub fn nathan() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 13)).unwrap()
|
||||
}
|
||||
pub fn othello() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 14)).unwrap()
|
||||
}
|
||||
pub fn perth() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 15)).unwrap()
|
||||
}
|
||||
pub fn ruth() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 16)).unwrap()
|
||||
}
|
||||
pub fn seth() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 17)).unwrap()
|
||||
}
|
||||
pub fn thomas() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 18)).unwrap()
|
||||
}
|
||||
pub fn uthman() -> Keypair {
|
||||
Keypair::from_phrase(
|
||||
&bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 19)).unwrap()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "subxt")]
|
||||
mod subxt_compat {
|
||||
use super::*;
|
||||
use pezkuwi_subxt_core::{
|
||||
config::Config,
|
||||
tx::signer::Signer as SignerT,
|
||||
utils::{AccountId20, MultiAddress},
|
||||
};
|
||||
|
||||
impl<T: Config> SignerT<T> for Keypair
|
||||
where
|
||||
T::AccountId: From<PublicKey>,
|
||||
T::Address: From<PublicKey>,
|
||||
T::Signature: From<Signature>,
|
||||
{
|
||||
fn account_id(&self) -> T::AccountId {
|
||||
self.public_key().into()
|
||||
}
|
||||
|
||||
fn sign(&self, signer_payload: &[u8]) -> T::Signature {
|
||||
self.sign(signer_payload).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// Obtains the public address of the account by taking the last 20 bytes
|
||||
/// of the Keccak-256 hash of the public key.
|
||||
pub fn to_account_id(&self) -> AccountId20 {
|
||||
let hash = keccak(&self.0[1..]).0;
|
||||
let hash20 = hash[12..].try_into().expect("should be 20 bytes");
|
||||
AccountId20(hash20)
|
||||
}
|
||||
/// A shortcut to obtain a [`MultiAddress`] from a [`PublicKey`].
|
||||
/// We often want this type, and using this method avoids any
|
||||
/// ambiguous type resolution issues.
|
||||
pub fn to_address<T>(self) -> MultiAddress<AccountId20, T> {
|
||||
MultiAddress::Address20(self.to_account_id().0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for AccountId20 {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
value.to_account_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<PublicKey> for MultiAddress<AccountId20, T> {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
let address: AccountId20 = value.into();
|
||||
MultiAddress::Address20(address.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "subxt")]
|
||||
mod test {
|
||||
use bip39::Mnemonic;
|
||||
use pezkuwi_subxt_core::utils::AccountId20;
|
||||
use proptest::prelude::*;
|
||||
use secp256k1::Secp256k1;
|
||||
|
||||
use pezkuwi_subxt_core::{config::*, tx::signer::Signer as SignerT};
|
||||
|
||||
use super::*;
|
||||
|
||||
enum StubEthRuntimeConfig {}
|
||||
|
||||
impl Config for StubEthRuntimeConfig {
|
||||
type AccountId = AccountId20;
|
||||
type Address = AccountId20;
|
||||
type Signature = Signature;
|
||||
type Hasher = bizinikiwi::BlakeTwo256;
|
||||
type Header = bizinikiwi::BizinikiwiHeader<u32, bizinikiwi::BlakeTwo256>;
|
||||
type ExtrinsicParams = BizinikiwiExtrinsicParams<Self>;
|
||||
type AssetId = u32;
|
||||
}
|
||||
|
||||
type SubxtSigner = dyn SignerT<StubEthRuntimeConfig>;
|
||||
|
||||
prop_compose! {
|
||||
fn keypair()(seed in any::<[u8; 32]>()) -> Keypair {
|
||||
let secret = secp256k1::SecretKey::from_slice(&seed).expect("valid secret key");
|
||||
let inner = secp256k1::Keypair::from_secret_key(
|
||||
&Secp256k1::new(),
|
||||
&secret,
|
||||
);
|
||||
|
||||
Keypair(ecdsa::Keypair(inner))
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn check_from_phrase(
|
||||
entropy in any::<[u8; 32]>(),
|
||||
password in any::<Option<String>>(),
|
||||
address in 1..(i32::MAX as u32),
|
||||
account_idx in 1..(i32::MAX as u32),
|
||||
) {
|
||||
let mnemonic = bip39::Mnemonic::from_entropy(&entropy).expect("valid mnemonic");
|
||||
let derivation_path = format!("m/44'/60'/{address}'/0/{account_idx}").parse().expect("valid derivation path");
|
||||
let private = bip32::XPrv::derive_from_path(
|
||||
mnemonic.to_seed(password.clone().unwrap_or("".to_string())),
|
||||
&derivation_path,
|
||||
).expect("valid private");
|
||||
|
||||
// Creating our own keypairs should be equivalent to using bip32 crate to do it:
|
||||
assert_eq!(
|
||||
Keypair::from_phrase(&mnemonic, password.as_deref(), DerivationPath::eth(address, account_idx)).expect("valid keypair"),
|
||||
Keypair(ecdsa::Keypair::from_secret_key(private.to_bytes()).expect("valid ecdsa keypair"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_from_phrase_bad_index(
|
||||
address in (i32::MAX as u32)..=u32::MAX,
|
||||
account_idx in (i32::MAX as u32)..=u32::MAX,
|
||||
) {
|
||||
let derivation_path_err = format!("m/44'/60'/{address}'/0/{account_idx}").parse::<DerivationPath>().expect_err("bad path expected");
|
||||
|
||||
// Creating invalid derivation paths (ie values too large) will result in an error.
|
||||
assert_eq!(
|
||||
derivation_path_err,
|
||||
Error::DeriveFromPath
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_pezkuwi_subxt_signer_implementation_matches(keypair in keypair(), msg in ".*") {
|
||||
let msg_as_bytes = msg.as_bytes();
|
||||
|
||||
assert_eq!(SubxtSigner::account_id(&keypair), keypair.public_key().to_account_id());
|
||||
assert_eq!(SubxtSigner::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_account_id(keypair in keypair()) {
|
||||
// https://github.com/ethereumbook/ethereumbook/blob/develop/04keys-addresses.asciidoc#ethereum-addresses
|
||||
let account_id = {
|
||||
let uncompressed = keypair.0.0.public_key().serialize_uncompressed();
|
||||
let hash = keccak(&uncompressed[1..]).0;
|
||||
let hash20 = hash[12..].try_into().expect("should be 20 bytes");
|
||||
AccountId20(hash20)
|
||||
};
|
||||
let account_id_derived_from_pk: AccountId20 = keypair.public_key().to_account_id();
|
||||
assert_eq!(account_id_derived_from_pk, account_id);
|
||||
assert_eq!(keypair.public_key().to_account_id(), account_id);
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") {
|
||||
let sig = SubxtSigner::sign(&keypair, msg.as_bytes());
|
||||
|
||||
assert!(verify(
|
||||
&sig,
|
||||
msg,
|
||||
&keypair.public_key())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that the dev accounts match those listed in the moonbeam README.
|
||||
/// https://github.com/moonbeam-foundation/moonbeam/blob/96cf8898874509d529b03c4da0e07b2787bacb18/README.md
|
||||
#[test]
|
||||
fn check_dev_accounts_match() {
|
||||
let cases = [
|
||||
(
|
||||
dev::alith(),
|
||||
"0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac",
|
||||
"0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133",
|
||||
),
|
||||
(
|
||||
dev::baltathar(),
|
||||
"0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0",
|
||||
"0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b",
|
||||
),
|
||||
(
|
||||
dev::charleth(),
|
||||
"0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc",
|
||||
"0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b",
|
||||
),
|
||||
(
|
||||
dev::dorothy(),
|
||||
"0x773539d4Ac0e786233D90A233654ccEE26a613D9",
|
||||
"0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68",
|
||||
),
|
||||
(
|
||||
dev::ethan(),
|
||||
"0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB",
|
||||
"0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4",
|
||||
),
|
||||
(
|
||||
dev::faith(),
|
||||
"0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d",
|
||||
"0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df",
|
||||
),
|
||||
(
|
||||
dev::gareth(),
|
||||
"0x7BF369283338E12C90514468aa3868A551AB2929",
|
||||
"0x96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18",
|
||||
),
|
||||
(
|
||||
dev::heather(),
|
||||
"0x931f3600a299fd9B24cEfB3BfF79388D19804BeA",
|
||||
"0x0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab",
|
||||
),
|
||||
(
|
||||
dev::ithelia(),
|
||||
"0xC41C5F1123ECCd5ce233578B2e7ebd5693869d73",
|
||||
"0x4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8",
|
||||
),
|
||||
(
|
||||
dev::jethro(),
|
||||
"0x2898FE7a42Be376C8BC7AF536A940F7Fd5aDd423",
|
||||
"0x94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b",
|
||||
),
|
||||
(
|
||||
dev::keith(),
|
||||
"0x583E6CFb24Ae212A36Db2766597fF8e6AC796751",
|
||||
"0xff0071dbd1edf21f40baf55718b2c3b032027c202b57afbe10720aa751a9f40c",
|
||||
),
|
||||
(
|
||||
dev::luther(),
|
||||
"0xbb827670B9dCb162Daa8DbF3dFF63a71c844d17d",
|
||||
"0x194f2d0cfcfecac3a224af9f534a5fe9f49ff4f28d939539e8bd244ce6fa79e4",
|
||||
),
|
||||
(
|
||||
dev::martha(),
|
||||
"0xD9E8D42eDD3Bc20871fA6662E069E71483fC167A",
|
||||
"0x268896eff609f44f711db60441104f099dccfa5678171a71a61afb14047aefbb",
|
||||
),
|
||||
(
|
||||
dev::nathan(),
|
||||
"0x9702DF55600140d8E197AAdfffa622F2A80564fd",
|
||||
"0x22bcd7b28c2d741f9b6d1afb84db16f39bdf6e6289b9adedf93ffa3763f62e31",
|
||||
),
|
||||
(
|
||||
dev::othello(),
|
||||
"0x9FC969aCe16Fe2757E04a8BD32136a9EC258db6D",
|
||||
"0xd711903ccdbcb2e87ac43132a2ffd5f189057d535ddb2802d71fa77767a059ea",
|
||||
),
|
||||
(
|
||||
dev::perth(),
|
||||
"0xFe25AaD37c57C4b6Bc85d96a4349dac5046A06EE",
|
||||
"0x26914ef14ae113743e48f24344146851036ff2ab663543947a366e36e781d79c",
|
||||
),
|
||||
(
|
||||
dev::ruth(),
|
||||
"0x11E8697Ef0f4BF2A4076ff46e42a0FdD8C4d6C41",
|
||||
"0xd9ab86105fd3a2c3d7055ff0427564c3c30bb9175780b3bd1842b37f93227778",
|
||||
),
|
||||
(
|
||||
dev::seth(),
|
||||
"0x001eB6957Eae09433A380504b11f807611686669",
|
||||
"0xd2e4efe30dd3a7aa9ea48efa838244515e74f210a6a3f2a1b4fd45631014502c",
|
||||
),
|
||||
(
|
||||
dev::thomas(),
|
||||
"0x0A2e55fd44d1cEe5fD482a2062A11C548C492E25",
|
||||
"0xe0a97dde04b09d3c2d3e8959eae318a01ef45a3c8d2f56258bc847a84fb80fe0",
|
||||
),
|
||||
(
|
||||
dev::uthman(),
|
||||
"0x1B948eD0bbacC2ca68eEcb5A9FC9Ba2755669faF",
|
||||
"0x1073cd4baa42f59545928c7e56bb5e14e31e4f5e911f9f2d99a1e092eab45f74",
|
||||
),
|
||||
];
|
||||
|
||||
for (case_idx, (keypair, exp_account_id, exp_priv_key)) in cases.into_iter().enumerate() {
|
||||
let act_account_id = keypair.public_key().to_account_id().checksum();
|
||||
let act_priv_key = format!("0x{}", &keypair.0.0.display_secret());
|
||||
|
||||
assert_eq!(exp_account_id, act_account_id, "account ID mismatch in {case_idx}");
|
||||
assert_eq!(exp_priv_key, act_priv_key, "private key mismatch in {case_idx}");
|
||||
}
|
||||
}
|
||||
|
||||
// This is a part of the test set linked in BIP39 and copied from https://github.com/trezor/python-mnemonic/blob/f5a975ab10c035596d65d854d21164266ffed284/vectors.json.
|
||||
// The passphrase is always TREZOR. We check that keys generated with the mnemonic (and no
|
||||
// derivation path) line up with the seeds given.
|
||||
#[test]
|
||||
fn check_basic_bip39_compliance() {
|
||||
let mnemonics_and_seeds = [
|
||||
(
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
|
||||
),
|
||||
(
|
||||
"legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607",
|
||||
),
|
||||
(
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
|
||||
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8",
|
||||
),
|
||||
(
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
|
||||
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069",
|
||||
),
|
||||
(
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
|
||||
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa",
|
||||
),
|
||||
(
|
||||
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
|
||||
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd",
|
||||
),
|
||||
(
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
|
||||
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65",
|
||||
),
|
||||
(
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
|
||||
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528",
|
||||
),
|
||||
(
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8",
|
||||
),
|
||||
(
|
||||
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
|
||||
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87",
|
||||
),
|
||||
(
|
||||
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
|
||||
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f",
|
||||
),
|
||||
(
|
||||
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
|
||||
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad",
|
||||
),
|
||||
(
|
||||
"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
|
||||
"274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028",
|
||||
),
|
||||
(
|
||||
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
|
||||
"628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac",
|
||||
),
|
||||
(
|
||||
"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
|
||||
"64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440",
|
||||
),
|
||||
(
|
||||
"scheme spot photo card baby mountain device kick cradle pact join borrow",
|
||||
"ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612",
|
||||
),
|
||||
(
|
||||
"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
|
||||
"fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d",
|
||||
),
|
||||
(
|
||||
"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
|
||||
"72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d",
|
||||
),
|
||||
(
|
||||
"cat swing flag economy stadium alone churn speed unique patch report train",
|
||||
"deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5",
|
||||
),
|
||||
(
|
||||
"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
|
||||
"4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02",
|
||||
),
|
||||
(
|
||||
"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
|
||||
"26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d",
|
||||
),
|
||||
(
|
||||
"vessel ladder alter error federal sibling chat ability sun glass valve picture",
|
||||
"2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f",
|
||||
),
|
||||
(
|
||||
"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
|
||||
"7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88",
|
||||
),
|
||||
(
|
||||
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
|
||||
"01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998",
|
||||
),
|
||||
];
|
||||
|
||||
for (idx, (m, s)) in mnemonics_and_seeds.into_iter().enumerate() {
|
||||
let m = Mnemonic::parse(m).expect("mnemonic should be valid");
|
||||
let pair1 = Keypair::from_phrase(&m, Some("TREZOR"), DerivationPath::empty()).unwrap();
|
||||
let s = hex::decode(s).expect("seed hex should be valid");
|
||||
let pair2 = Keypair::from_seed(&s).unwrap();
|
||||
|
||||
assert_eq!(pair1, pair2, "pair1 and pair2 at index {idx} don't match");
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the same accounts from moonbeam so we know for sure that this implementation is working
|
||||
/// https://github.com/moonbeam-foundation/moonbeam/blob/e70ee0d427dfee8987d5a5671a66416ee6ec38aa/primitives/account/src/lib.rs#L217
|
||||
mod moonbeam_sanity_tests {
|
||||
use hex_literal::hex;
|
||||
|
||||
use super::*;
|
||||
|
||||
const KEY_1: [u8; 32] =
|
||||
hex!("502f97299c472b88754accd412b7c9a6062ef3186fba0c0388365e1edec24875");
|
||||
const KEY_2: [u8; 32] =
|
||||
hex!("0f02ba4d7f83e59eaa32eae9c3c4d99b68ce76decade21cdab7ecce8f4aef81a");
|
||||
const KEY_3: [u8; 32] =
|
||||
hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470");
|
||||
|
||||
#[test]
|
||||
fn test_account_derivation_1() {
|
||||
let kp = Keypair::from_secret_key(KEY_1).expect("valid keypair");
|
||||
assert_eq!(
|
||||
kp.public_key().to_account_id().checksum(),
|
||||
"0x976f8456E4e2034179B284A23C0e0c8f6d3da50c"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_derivation_2() {
|
||||
let kp = Keypair::from_secret_key(KEY_2).expect("valid keypair");
|
||||
assert_eq!(
|
||||
kp.public_key().to_account_id().checksum(),
|
||||
"0x420e9F260B40aF7E49440ceAd3069f8e82A5230f"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_derivation_3() {
|
||||
let kp = Keypair::from_secret_key(KEY_3).expect("valid keypair");
|
||||
assert_eq!(
|
||||
kp.public_key().to_account_id().checksum(),
|
||||
"0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # Subxt-signer
|
||||
//!
|
||||
//! The main output from this crate is the [`sr25519::Keypair`], which can
|
||||
//! be constructed from a bip39 phrase, secret URI or raw seed, and used to
|
||||
//! sign and verify arbitrary messages. This crate is aligned with how Bizinikiwi's
|
||||
//! `sp_core` crate constructs and signs keypairs, but is lighter on dependencies
|
||||
//! and can support compilation to WASM with the `web` feature.
|
||||
//!
|
||||
//! Enable the `subxt` feature to enable use of this [`sr25519::Keypair`] in signing
|
||||
//! subxt transactions for chains supporting sr25519 signatures.
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[macro_use]
|
||||
mod utils;
|
||||
mod crypto;
|
||||
|
||||
// An sr25519 key pair implementation.
|
||||
#[cfg(feature = "sr25519")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "sr25519")))]
|
||||
pub mod sr25519;
|
||||
|
||||
// An ecdsa key pair implementation.
|
||||
#[cfg(feature = "ecdsa")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "ecdsa")))]
|
||||
pub mod ecdsa;
|
||||
|
||||
// An ethereum signer implementation.
|
||||
#[cfg(feature = "unstable-eth")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-eth")))]
|
||||
pub mod eth;
|
||||
|
||||
/// A pezkuwi-js account json loader.
|
||||
#[cfg(feature = "pezkuwi-js-compat")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pezkuwi-js-compat")))]
|
||||
pub mod pezkuwi_js_compat;
|
||||
|
||||
// Re-export useful bits and pieces for generating a Pair from a phrase,
|
||||
// namely the Mnemonic struct.
|
||||
pub use bip39;
|
||||
|
||||
// Used to hold strings in a more secure manner in memory for a little extra
|
||||
// protection.
|
||||
pub use secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
// SecretUri's can be parsed from strings and used to generate key pairs.
|
||||
// DeriveJunctions are the "path" part of these SecretUris.
|
||||
pub use crypto::{DEV_PHRASE, DeriveJunction, SecretUri, SecretUriError};
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! A Pezkuwi-JS account loader.
|
||||
|
||||
use base64::Engine;
|
||||
use crypto_secretbox::{
|
||||
Key, Nonce, XSalsa20Poly1305,
|
||||
aead::{Aead, KeyInit},
|
||||
};
|
||||
use pezkuwi_subxt_core::utils::AccountId32;
|
||||
use serde::Deserialize;
|
||||
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
use crate::sr25519;
|
||||
|
||||
/// Given a JSON keypair as exported from Pezkuwi-JS, this returns an [`sr25519::Keypair`]
|
||||
pub fn decrypt_json(json: &str, password: &str) -> Result<sr25519::Keypair, Error> {
|
||||
let pair_json: KeyringPairJson = serde_json::from_str(json)?;
|
||||
Ok(pair_json.decrypt(password)?)
|
||||
}
|
||||
|
||||
/// Error
|
||||
#[derive(Debug, DeriveError)]
|
||||
pub enum Error {
|
||||
/// Error decoding JSON.
|
||||
#[error("Invalid JSON: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
/// The keypair has an unsupported encoding.
|
||||
#[error("Unsupported encoding.")]
|
||||
UnsupportedEncoding,
|
||||
/// Base64 decoding error.
|
||||
#[error("Base64 decoding error: {0}")]
|
||||
Base64(#[from] base64::DecodeError),
|
||||
/// Wrong Scrypt parameters
|
||||
#[error("Unsupported Scrypt parameters: N: {n}, p: {p}, r: {r}")]
|
||||
UnsupportedScryptParameters {
|
||||
/// N
|
||||
n: u32,
|
||||
/// p
|
||||
p: u32,
|
||||
/// r
|
||||
r: u32,
|
||||
},
|
||||
/// Decryption error.
|
||||
#[error("Decryption error: {0}")]
|
||||
Secretbox(#[from] crypto_secretbox::Error),
|
||||
/// sr25519 keypair error.
|
||||
#[error(transparent)]
|
||||
Sr25519(#[from] sr25519::Error),
|
||||
/// The decrypted keys are not valid.
|
||||
#[error("The decrypted keys are not valid.")]
|
||||
InvalidKeys,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EncryptionMetadata {
|
||||
/// Descriptor for the content
|
||||
content: Vec<String>,
|
||||
/// The encoding (in current/latest versions this is always an array)
|
||||
r#type: Vec<String>,
|
||||
/// The version of encoding applied
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// https://github.com/pezkuwi-js/common/blob/37fa211fdb141d4f6eb32e8f377a4651ed2d9068/packages/keyring/src/types.ts#L67
|
||||
#[derive(Deserialize)]
|
||||
struct KeyringPairJson {
|
||||
/// The encoded string
|
||||
encoded: String,
|
||||
/// The encoding used
|
||||
encoding: EncryptionMetadata,
|
||||
/// The ss58 encoded address or the hex-encoded version (the latter is for ETH-compat chains)
|
||||
address: AccountId32,
|
||||
}
|
||||
|
||||
// This can be removed once split_array is stabilized.
|
||||
fn slice_to_u32(slice: &[u8]) -> u32 {
|
||||
u32::from_le_bytes(slice.try_into().expect("Slice should be 4 bytes."))
|
||||
}
|
||||
|
||||
impl KeyringPairJson {
|
||||
/// Decrypt JSON keypair.
|
||||
fn decrypt(self, password: &str) -> Result<sr25519::Keypair, Error> {
|
||||
// Check encoding.
|
||||
// https://github.com/pezkuwi-js/common/blob/37fa211fdb141d4f6eb32e8f377a4651ed2d9068/packages/keyring/src/keyring.ts#L166
|
||||
if self.encoding.version != "3" ||
|
||||
!self.encoding.content.contains(&"pkcs8".to_owned()) ||
|
||||
!self.encoding.content.contains(&"sr25519".to_owned()) ||
|
||||
!self.encoding.r#type.contains(&"scrypt".to_owned()) ||
|
||||
!self.encoding.r#type.contains(&"xsalsa20-poly1305".to_owned())
|
||||
{
|
||||
return Err(Error::UnsupportedEncoding);
|
||||
}
|
||||
|
||||
// Decode from Base64.
|
||||
let decoded = base64::engine::general_purpose::STANDARD.decode(self.encoded)?;
|
||||
let params: [u8; 68] = decoded[..68].try_into().map_err(|_| Error::UnsupportedEncoding)?;
|
||||
|
||||
// Extract scrypt parameters.
|
||||
// https://github.com/pezkuwi-js/common/blob/master/packages/util-crypto/src/scrypt/fromU8a.ts
|
||||
let salt = ¶ms[0..32];
|
||||
let n = slice_to_u32(¶ms[32..36]);
|
||||
let p = slice_to_u32(¶ms[36..40]);
|
||||
let r = slice_to_u32(¶ms[40..44]);
|
||||
|
||||
// FIXME At this moment we assume these to be fixed params, this is not a great idea
|
||||
// since we lose flexibility and updates for greater security. However we need some
|
||||
// protection against carefully-crafted params that can eat up CPU since these are user
|
||||
// inputs. So we need to get very clever here, but atm we only allow the defaults
|
||||
// and if no match, bail out.
|
||||
if n != 32768 || p != 1 || r != 8 {
|
||||
return Err(Error::UnsupportedScryptParameters { n, p, r });
|
||||
}
|
||||
|
||||
// Hash password.
|
||||
let scrypt_params =
|
||||
scrypt::Params::new(15, 8, 1, 32).expect("Provided parameters should be valid.");
|
||||
let mut key = Key::default();
|
||||
scrypt::scrypt(password.as_bytes(), salt, &scrypt_params, &mut key)
|
||||
.expect("Key should be 32 bytes.");
|
||||
|
||||
// Decrypt keys.
|
||||
// https://github.com/pezkuwi-js/common/blob/master/packages/util-crypto/src/json/decryptData.ts
|
||||
let cipher = XSalsa20Poly1305::new(&key);
|
||||
let nonce = Nonce::from_slice(¶ms[44..68]);
|
||||
let ciphertext = &decoded[68..];
|
||||
let plaintext = cipher.decrypt(nonce, ciphertext)?;
|
||||
|
||||
// https://github.com/pezkuwi-js/common/blob/master/packages/keyring/src/pair/decode.ts
|
||||
if plaintext.len() != 117 {
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
let header = &plaintext[0..16];
|
||||
let secret_key = &plaintext[16..80];
|
||||
let div = &plaintext[80..85];
|
||||
let public_key = &plaintext[85..117];
|
||||
|
||||
if header != [48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32] ||
|
||||
div != [161, 35, 3, 33, 0]
|
||||
{
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
// Generate keypair.
|
||||
let keypair = sr25519::Keypair::from_ed25519_bytes(secret_key)?;
|
||||
|
||||
// Ensure keys are correct.
|
||||
if keypair.public_key().0 != public_key ||
|
||||
keypair.public_key().to_account_id() != self.address
|
||||
{
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
Ok(keypair)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_keypair_sr25519() {
|
||||
let json = r#"
|
||||
{
|
||||
"encoded": "DumgApKCTqoCty1OZW/8WS+sgo6RdpHhCwAkA2IoDBMAgAAAAQAAAAgAAAB6IG/q24EeVf0JqWqcBd5m2tKq5BlyY84IQ8oamLn9DZe9Ouhgunr7i36J1XxUnTI801axqL/ym1gil0U8440Qvj0lFVKwGuxq38zuifgoj0B3Yru0CI6QKEvQPU5xxj4MpyxdSxP+2PnTzYao0HDH0fulaGvlAYXfqtU89xrx2/z9z7IjSwS3oDFPXRQ9kAdDebtyCVreZ9Otw9v3",
|
||||
"encoding": {
|
||||
"content": [
|
||||
"pkcs8",
|
||||
"sr25519"
|
||||
],
|
||||
"type": [
|
||||
"scrypt",
|
||||
"xsalsa20-poly1305"
|
||||
],
|
||||
"version": "3"
|
||||
},
|
||||
"address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
"meta": {
|
||||
"genesisHash": "",
|
||||
"name": "Alice",
|
||||
"whenCreated": 1718265838755
|
||||
}
|
||||
}
|
||||
"#;
|
||||
decrypt_json(json, "whoisalice").unwrap();
|
||||
}
|
||||
}
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! An sr25519 keypair implementation.
|
||||
//!
|
||||
//! **Note:** This implementation requires the `getrandom` dependency to obtain randomness,
|
||||
//! and will not compile on targets that it does not support. See the supported `getrandom`
|
||||
//! targets here: <https://docs.rs/getrandom/latest/getrandom/#supported-targets>.
|
||||
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::crypto::{DeriveJunction, SecretUri, seed_from_entropy};
|
||||
|
||||
use hex::FromHex;
|
||||
use schnorrkel::{
|
||||
ExpansionMode, MiniSecretKey,
|
||||
derive::{ChainCode, Derivation},
|
||||
};
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
const SECRET_KEY_LENGTH: usize = schnorrkel::keys::MINI_SECRET_KEY_LENGTH;
|
||||
const SIGNING_CTX: &[u8] = b"bizinikiwi";
|
||||
|
||||
/// Seed bytes used to generate a key pair.
|
||||
pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
|
||||
|
||||
/// A signature generated by [`Keypair::sign()`]. These bytes are equivalent
|
||||
/// to a Bizinikiwi `MultiSignature::sr25519(bytes)`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Signature(pub [u8; 64]);
|
||||
|
||||
impl AsRef<[u8]> for Signature {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The public key for an [`Keypair`] key pair. This is equivalent to a
|
||||
/// Bizinikiwi `AccountId32`.
|
||||
pub struct PublicKey(pub [u8; 32]);
|
||||
|
||||
impl AsRef<[u8]> for PublicKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An sr25519 keypair implementation. While the API is slightly different, the logic for
|
||||
/// this has been taken from `sp_core::sr25519` and we test against this to ensure conformity.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Keypair(schnorrkel::Keypair);
|
||||
|
||||
impl Keypair {
|
||||
/// Create am sr25519 keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ SecretUri, sr25519::Keypair };
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// let uri = SecretUri::from_str("//Alice").unwrap();
|
||||
/// let keypair = Keypair::from_uri(&uri).unwrap();
|
||||
///
|
||||
/// keypair.sign(b"Hello world!");
|
||||
/// ```
|
||||
pub fn from_uri(uri: &SecretUri) -> Result<Self, Error> {
|
||||
let SecretUri { junctions, phrase, password } = uri;
|
||||
|
||||
// If the phrase is hex, convert bytes directly into a seed, ignoring password.
|
||||
// Else, parse the phrase string taking the password into account. This is
|
||||
// the same approach taken in sp_core::crypto::Pair::from_string_with_seed.
|
||||
let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") {
|
||||
let seed = SecretKeyBytes::from_hex(hex_str)?;
|
||||
Self::from_secret_key(seed)?
|
||||
} else {
|
||||
let phrase = bip39::Mnemonic::from_str(phrase.expose_secret())?;
|
||||
let pass_str = password.as_ref().map(|p| p.expose_secret());
|
||||
Self::from_phrase(&phrase, pass_str)?
|
||||
};
|
||||
|
||||
// Now, use any "junctions" to derive a new key from this root key.
|
||||
Ok(key.derive(junctions.iter().copied()))
|
||||
}
|
||||
|
||||
/// Create am sr25519 keypair from a BIP-39 mnemonic phrase and optional password.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, sr25519::Keypair };
|
||||
///
|
||||
/// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
||||
/// let mnemonic = Mnemonic::parse(phrase).unwrap();
|
||||
/// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap();
|
||||
///
|
||||
/// keypair.sign(b"Hello world!");
|
||||
/// ```
|
||||
pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result<Self, Error> {
|
||||
let (arr, len) = mnemonic.to_entropy_array();
|
||||
let big_seed =
|
||||
seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?;
|
||||
|
||||
let seed: SecretKeyBytes =
|
||||
big_seed[..SECRET_KEY_LENGTH].try_into().expect("should be valid Seed");
|
||||
|
||||
Self::from_secret_key(seed)
|
||||
}
|
||||
|
||||
/// Turn a 32 byte secret key into a keypair.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This will only be secure if the seed is secure!
|
||||
pub fn from_secret_key(secret_key_bytes: SecretKeyBytes) -> Result<Self, Error> {
|
||||
let keypair = MiniSecretKey::from_bytes(&secret_key_bytes)
|
||||
.map_err(|_| Error::InvalidSeed)?
|
||||
.expand_to_keypair(ExpansionMode::Ed25519);
|
||||
|
||||
Ok(Keypair(keypair))
|
||||
}
|
||||
|
||||
/// Construct a keypair from a slice of bytes, corresponding to
|
||||
/// an Ed25519 expanded secret key.
|
||||
#[cfg(feature = "pezkuwi-js-compat")]
|
||||
pub(crate) fn from_ed25519_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
let secret_key = schnorrkel::SecretKey::from_ed25519_bytes(bytes)?;
|
||||
|
||||
Ok(Keypair(schnorrkel::Keypair { public: secret_key.to_public(), secret: secret_key }))
|
||||
}
|
||||
|
||||
/// Derive a child key from this one given a series of junctions.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, sr25519::Keypair, DeriveJunction };
|
||||
///
|
||||
/// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
||||
/// let mnemonic = Mnemonic::parse(phrase).unwrap();
|
||||
/// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap();
|
||||
///
|
||||
/// // Equivalent to the URI path '//Alice/stash':
|
||||
/// let new_keypair = keypair.derive([
|
||||
/// DeriveJunction::hard("Alice"),
|
||||
/// DeriveJunction::soft("stash")
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn derive<Js: IntoIterator<Item = DeriveJunction>>(&self, junctions: Js) -> Self {
|
||||
let init = self.0.secret.clone();
|
||||
let result = junctions.into_iter().fold(init, |acc, j| match j {
|
||||
DeriveJunction::Soft(cc) => acc.derived_key_simple(ChainCode(cc), []).0,
|
||||
DeriveJunction::Hard(cc) => {
|
||||
let seed = acc.hard_derive_mini_secret_key(Some(ChainCode(cc)), b"").0;
|
||||
seed.expand(ExpansionMode::Ed25519)
|
||||
},
|
||||
});
|
||||
Self(result.into())
|
||||
}
|
||||
|
||||
/// Obtain the [`PublicKey`] part of this key pair, which can be used in calls to [`verify()`].
|
||||
/// or otherwise converted into an address. The public key bytes are equivalent to a Bizinikiwi
|
||||
/// `AccountId32`.
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
PublicKey(self.0.public.to_bytes())
|
||||
}
|
||||
|
||||
/// Sign some message. These bytes can be used directly in a Bizinikiwi
|
||||
/// `MultiSignature::sr25519(..)`.
|
||||
pub fn sign(&self, message: &[u8]) -> Signature {
|
||||
let context = schnorrkel::signing_context(SIGNING_CTX);
|
||||
let signature = self.0.sign(context.bytes(message));
|
||||
Signature(signature.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that some signature for a message was created by the owner of the [`PublicKey`].
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use pezkuwi_subxt_signer::{ bip39::Mnemonic, sr25519 };
|
||||
///
|
||||
/// let keypair = sr25519::dev::alice();
|
||||
/// let message = b"Hello!";
|
||||
///
|
||||
/// let signature = keypair.sign(message);
|
||||
/// let public_key = keypair.public_key();
|
||||
/// assert!(sr25519::verify(&signature, message, &public_key));
|
||||
/// ```
|
||||
pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool {
|
||||
let Ok(signature) = schnorrkel::Signature::from_bytes(&sig.0) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(public) = schnorrkel::PublicKey::from_bytes(&pubkey.0) else {
|
||||
return false;
|
||||
};
|
||||
public.verify_simple(SIGNING_CTX, message.as_ref(), &signature).is_ok()
|
||||
}
|
||||
|
||||
/// An error handed back if creating a keypair fails.
|
||||
#[derive(Debug, DeriveError)]
|
||||
pub enum Error {
|
||||
/// Invalid seed.
|
||||
#[error("Invalid seed (was it the wrong length?)")]
|
||||
InvalidSeed,
|
||||
/// Invalid phrase.
|
||||
#[error("Cannot parse phrase: {0}")]
|
||||
Phrase(bip39::Error),
|
||||
/// Invalid hex.
|
||||
#[error("Cannot parse hex string: {0}")]
|
||||
Hex(hex::FromHexError),
|
||||
/// Signature error.
|
||||
#[error("Signature error: {0}")]
|
||||
Signature(schnorrkel::SignatureError),
|
||||
}
|
||||
|
||||
impl From<schnorrkel::SignatureError> for Error {
|
||||
fn from(value: schnorrkel::SignatureError) -> Self {
|
||||
Error::Signature(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for Error {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
Error::Hex(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bip39::Error> for Error {
|
||||
fn from(err: bip39::Error) -> Self {
|
||||
Error::Phrase(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dev accounts, helpful for testing but not to be used in production,
|
||||
/// since the secret keys are known.
|
||||
pub mod dev {
|
||||
use super::*;
|
||||
|
||||
once_static_cloned! {
|
||||
/// Equivalent to `{DEV_PHRASE}//Alice`.
|
||||
pub fn alice() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Alice").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Bob`.
|
||||
pub fn bob() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Bob").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Charlie`.
|
||||
pub fn charlie() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Charlie").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Dave`.
|
||||
pub fn dave() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Dave").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Eve`.
|
||||
pub fn eve() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Eve").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Ferdie`.
|
||||
pub fn ferdie() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Ferdie").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//One`.
|
||||
pub fn one() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//One").unwrap()).unwrap()
|
||||
}
|
||||
/// Equivalent to `{DEV_PHRASE}//Two`.
|
||||
pub fn two() -> Keypair {
|
||||
Keypair::from_uri(&SecretUri::from_str("//Two").unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make `Keypair` usable to sign transactions in Subxt. This is optional so that
|
||||
// `subxt-signer` can be used entirely independently of Subxt.
|
||||
#[cfg(feature = "subxt")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "subxt")))]
|
||||
mod subxt_compat {
|
||||
use super::*;
|
||||
|
||||
use pezkuwi_subxt_core::{
|
||||
Config,
|
||||
tx::signer::Signer as SignerT,
|
||||
utils::{AccountId32, MultiAddress, MultiSignature},
|
||||
};
|
||||
|
||||
impl From<Signature> for MultiSignature {
|
||||
fn from(value: Signature) -> Self {
|
||||
MultiSignature::Sr25519(value.0)
|
||||
}
|
||||
}
|
||||
impl From<PublicKey> for AccountId32 {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
value.to_account_id()
|
||||
}
|
||||
}
|
||||
impl<T> From<PublicKey> for MultiAddress<AccountId32, T> {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
value.to_address()
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// A shortcut to obtain an [`AccountId32`] from a [`PublicKey`].
|
||||
/// We often want this type, and using this method avoids any
|
||||
/// ambiguous type resolution issues.
|
||||
pub fn to_account_id(self) -> AccountId32 {
|
||||
AccountId32(self.0)
|
||||
}
|
||||
/// A shortcut to obtain a [`MultiAddress`] from a [`PublicKey`].
|
||||
/// We often want this type, and using this method avoids any
|
||||
/// ambiguous type resolution issues.
|
||||
pub fn to_address<T>(self) -> MultiAddress<AccountId32, T> {
|
||||
MultiAddress::Id(self.to_account_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> SignerT<T> for Keypair
|
||||
where
|
||||
T::AccountId: From<PublicKey>,
|
||||
T::Address: From<PublicKey>,
|
||||
T::Signature: From<Signature>,
|
||||
{
|
||||
fn account_id(&self) -> T::AccountId {
|
||||
self.public_key().into()
|
||||
}
|
||||
|
||||
fn sign(&self, signer_payload: &[u8]) -> T::Signature {
|
||||
self.sign(signer_payload).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
use sp_core::{self, crypto::Pair as _, sr25519::Pair as SpPair};
|
||||
|
||||
#[test]
|
||||
fn check_from_phrase_matches() {
|
||||
for _ in 0..20 {
|
||||
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None);
|
||||
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
|
||||
let pair = Keypair::from_phrase(&phrase, None).expect("should be valid");
|
||||
|
||||
assert_eq!(sp_pair.public().0, pair.public_key().0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_from_phrase_with_password_matches() {
|
||||
for _ in 0..20 {
|
||||
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
|
||||
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
|
||||
let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
|
||||
|
||||
assert_eq!(sp_pair.public().0, pair.public_key().0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_from_secret_uri_matches() {
|
||||
// Some derive junctions to check that the logic there aligns:
|
||||
let uri_paths = [
|
||||
"/foo",
|
||||
"//bar",
|
||||
"/1",
|
||||
"/0001",
|
||||
"//1",
|
||||
"//0001",
|
||||
"//foo//bar/wibble",
|
||||
"//foo//001/wibble",
|
||||
];
|
||||
|
||||
for i in 0..2 {
|
||||
for path in &uri_paths {
|
||||
// Build an sp_core::Pair that includes a phrase, path and password:
|
||||
let password = format!("Testing{i}");
|
||||
let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some(&password));
|
||||
let uri = format!("{phrase}{path}///{password}");
|
||||
let sp_pair = SpPair::from_string(&uri, None).expect("should be valid");
|
||||
|
||||
// Now build a local Keypair using the equivalent API:
|
||||
let uri = SecretUri::from_str(&uri).expect("should be valid secret URI");
|
||||
let pair = Keypair::from_uri(&uri).expect("should be valid");
|
||||
|
||||
// They should match:
|
||||
assert_eq!(sp_pair.public().0, pair.public_key().0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_dev_accounts_match() {
|
||||
use sp_keyring::sr25519::Keyring::*;
|
||||
|
||||
assert_eq!(dev::alice().public_key().0, Alice.public().0);
|
||||
assert_eq!(dev::bob().public_key().0, Bob.public().0);
|
||||
assert_eq!(dev::charlie().public_key().0, Charlie.public().0);
|
||||
assert_eq!(dev::dave().public_key().0, Dave.public().0);
|
||||
assert_eq!(dev::eve().public_key().0, Eve.public().0);
|
||||
assert_eq!(dev::ferdie().public_key().0, Ferdie.public().0);
|
||||
assert_eq!(dev::one().public_key().0, One.public().0);
|
||||
assert_eq!(dev::two().public_key().0, Two.public().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_signing_and_verifying_matches() {
|
||||
use sp_core::sr25519::Signature as SpSignature;
|
||||
|
||||
for _ in 0..20 {
|
||||
let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
|
||||
let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
|
||||
let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
|
||||
|
||||
let message = b"Hello world";
|
||||
let sp_sig = sp_pair.sign(message).0;
|
||||
let sig = pair.sign(message).0;
|
||||
|
||||
assert!(SpPair::verify(&SpSignature::from(sig), message, &sp_pair.public()));
|
||||
assert!(verify(&Signature(sp_sig), message, &pair.public_key()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_hex_uris() {
|
||||
// Hex URIs seem to ignore the password on sp_core and here. Check that this is consistent.
|
||||
let uri_str =
|
||||
"0x1122334455667788112233445566778811223344556677881122334455667788///SomePassword";
|
||||
|
||||
let uri = SecretUri::from_str(uri_str).expect("should be valid");
|
||||
let pair = Keypair::from_uri(&uri).expect("should be valid");
|
||||
let sp_pair = SpPair::from_string(uri_str, None).expect("should be valid");
|
||||
|
||||
assert_eq!(pair.public_key().0, sp_pair.public().0);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
#![allow(unused_macros)]
|
||||
|
||||
/// Use like:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// once_static_cloned!{
|
||||
/// /// Some documentation.
|
||||
/// fn foo() -> Vec<u8> {
|
||||
/// vec![1,2,3,4]
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Clones the item out of static storage. Useful if it
|
||||
/// takes a while to create the item but cloning it is fairly cheap.
|
||||
macro_rules! once_static_cloned {
|
||||
($($(#[$attr:meta])* $vis:vis fn $name:ident() -> $ty:ty { $expr:expr } )+) => {
|
||||
$(
|
||||
$(#[$attr])*
|
||||
#[allow(missing_docs)]
|
||||
$vis fn $name() -> $ty {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "std")] {
|
||||
static VAR: std::sync::OnceLock<$ty> = std::sync::OnceLock::new();
|
||||
VAR.get_or_init(|| { $expr }).clone()
|
||||
} else {
|
||||
{ $expr }
|
||||
}
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user