fix: Convert vendor/pezkuwi-subxt from submodule to regular directory

This commit is contained in:
2025-12-19 16:45:24 +03:00
parent 9a52edf0df
commit fdd023c499
393 changed files with 154124 additions and 1 deletions
@@ -0,0 +1,102 @@
// 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
View File
@@ -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;
+157
View File
@@ -0,0 +1,157 @@
// 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 `substrate-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)
}
+453
View File
@@ -0,0 +1,453 @@
// 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 Substrate `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 Substrate `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 Substrate `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;
use pezkuwi_subxt_core::tx::signer::Signer as SignerT;
use pezkuwi_subxt_core::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);
}
}
+747
View File
@@ -0,0 +1,747 @@
// 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;
use pezkuwi_subxt_core::tx::signer::Signer as SignerT;
use pezkuwi_subxt_core::utils::AccountId20;
use pezkuwi_subxt_core::utils::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 proptest::prelude::*;
use secp256k1::Secp256k1;
use pezkuwi_subxt_core::utils::AccountId20;
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 = substrate::BlakeTwo256;
type Header = substrate::SubstrateHeader<u32, substrate::BlakeTwo256>;
type ExtrinsicParams = SubstrateExtrinsicParams<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
View File
@@ -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 Substrate'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 polkadot-js account json loader.
#[cfg(feature = "polkadot-js-compat")]
#[cfg_attr(docsrs, doc(cfg(feature = "polkadot-js-compat")))]
pub mod polkadot_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};
+197
View File
@@ -0,0 +1,197 @@
// 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 Polkadot-JS account loader.
use base64::Engine;
use crypto_secretbox::{
Key, Nonce, XSalsa20Poly1305,
aead::{Aead, KeyInit},
};
use serde::Deserialize;
use pezkuwi_subxt_core::utils::AccountId32;
use thiserror::Error as DeriveError;
use crate::sr25519;
/// Given a JSON keypair as exported from Polkadot-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/polkadot-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/polkadot-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/polkadot-js/common/blob/master/packages/util-crypto/src/scrypt/fromU8a.ts
let salt = &params[0..32];
let n = slice_to_u32(&params[32..36]);
let p = slice_to_u32(&params[36..40]);
let r = slice_to_u32(&params[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/polkadot-js/common/blob/master/packages/util-crypto/src/json/decryptData.ts
let cipher = XSalsa20Poly1305::new(&key);
let nonce = Nonce::from_slice(&params[44..68]);
let ciphertext = &decoded[68..];
let plaintext = cipher.decrypt(nonce, ciphertext)?;
// https://github.com/polkadot-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();
}
}
+457
View File
@@ -0,0 +1,457 @@
// 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"substrate";
/// 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 Substrate `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
/// Substrate `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 = "polkadot-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 Substrate
/// `AccountId32`.
pub fn public_key(&self) -> PublicKey {
PublicKey(self.0.public.to_bytes())
}
/// Sign some message. These bytes can be used directly in a Substrate `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
View File
@@ -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 }
}
}
}
)+
};
}