use codec::{Decode, Encode};
use ed25519_zebra::{SigningKey, VerificationKey};
use hkdf::Hkdf;
use sha2::Sha256;
use sp_core::{crypto::Pair as _, ed25519, sr25519};
use subtensor_hashed::{account_id, key_commitment, Descriptor, Proof, Scheme, SignerAddress};
use zeroize::Zeroizing;
use super::{crypto_err, Keypair, KeypairInner, CRYPTO_HASHED, CRYPTO_HASHED_ED25519};
use crate::error::CoreError;
const VERSION: u8 = 1;
const DERIVATION_SALT: &[u8] = b"bittensor/hashed/v1/derive";
pub fn verify_hashed_authorization(
encoded: &[u8],
payload: &[u8],
descriptor: &[u8],
generation: u64,
expected_commitment: &[u8; 32],
) -> Result<bool, CoreError> {
let mut input = descriptor;
let descriptor = Descriptor::decode(&mut input)
.map_err(|_| crypto_err("invalid hashed account descriptor"))?;
if !input.is_empty() || !descriptor.is_supported() {
return Err(crypto_err("invalid hashed account descriptor"));
}
if encoded.len() != 8 + 32 + 32 + 64 || generation == u64::MAX {
return Ok(false);
}
let proof = Proof::decode(&mut &encoded[..])
.map_err(|_| crypto_err("invalid hashed authorization proof"))?;
if proof.generation != generation
|| key_commitment(descriptor.scheme, &proof.public_key) != *expected_commitment
|| proof.next_commitment == [0; 32]
|| proof.next_commitment == *expected_commitment
{
return Ok(false);
}
let address = SignerAddress {
account: account_id(&descriptor),
version: descriptor.version,
scheme: descriptor.scheme,
generation,
public_key: proof.public_key,
next_commitment: proof.next_commitment,
};
let message = address.signing_hash(payload);
Ok(match descriptor.scheme {
Scheme::Sr25519 => sr25519::Pair::verify(
&sr25519::Signature::from_raw(proof.signature),
message,
&sr25519::Public::from_raw(proof.public_key),
),
Scheme::Ed25519 => ed25519::Pair::verify(
&ed25519::Signature::from_raw(proof.signature),
message,
&ed25519::Public::from_raw(proof.public_key),
),
})
}
pub struct HashedKeypair {
descriptor: Descriptor,
master_seed: Option<Zeroizing<[u8; 32]>>,
generation: u64,
}
impl HashedKeypair {
pub(super) fn from_seed(seed: &[u8], crypto_type: u8) -> Result<Self, CoreError> {
if seed.len() != 32 {
return Err(crypto_err("hashed master seed must be exactly 32 bytes"));
}
let mut master_seed = Zeroizing::new([0u8; 32]);
master_seed.copy_from_slice(seed);
let scheme = match crypto_type {
CRYPTO_HASHED => Scheme::Sr25519,
CRYPTO_HASHED_ED25519 => Scheme::Ed25519,
_ => return Err(crypto_err("unsupported hashed signing scheme")),
};
let version = VERSION;
let initial_commitment = match scheme {
Scheme::Sr25519 => key_commitment(scheme, &Self::derive(&master_seed, 0)?.public().0),
Scheme::Ed25519 => {
key_commitment(scheme, &ed25519_public(&Self::derive_ed(&master_seed, 0)?))
}
};
Ok(Self {
descriptor: Descriptor {
version,
scheme,
initial_commitment,
},
master_seed: Some(master_seed),
generation: 0,
})
}
fn derive(master: &[u8; 32], generation: u64) -> Result<sr25519::Pair, CoreError> {
let kdf = Hkdf::<Sha256>::new(Some(DERIVATION_SALT), master);
let info = (VERSION, Scheme::Sr25519, generation).encode();
let mut seed = Zeroizing::new([0u8; 32]);
kdf.expand(&info, seed.as_mut())
.map_err(|_| crypto_err("hashed signing-key derivation failed"))?;
Ok(sr25519::Pair::from_seed(&seed))
}
fn derive_ed(master: &[u8; 32], generation: u64) -> Result<Zeroizing<SigningKey>, CoreError> {
let kdf = Hkdf::<Sha256>::new(Some(DERIVATION_SALT), master);
let mut seed = Zeroizing::new([0u8; 32]);
kdf.expand(
&(VERSION, Scheme::Ed25519, generation).encode(),
seed.as_mut(),
)
.map_err(|_| crypto_err("Ed25519 signing-key derivation failed"))?;
let secret = SigningKey::try_from(&seed[..])
.map_err(|_| crypto_err("Ed25519 signing-key derivation failed"))?;
Ok(Zeroizing::new(secret))
}
fn ed_at(&self, generation: u64) -> Result<Zeroizing<SigningKey>, CoreError> {
let master = self
.master_seed()
.ok_or_else(|| crypto_err("signing requires a private wallet"))?;
Self::derive_ed(master, generation)
}
pub(super) fn crypto_type(&self) -> u8 {
match self.descriptor.scheme {
Scheme::Sr25519 => CRYPTO_HASHED,
Scheme::Ed25519 => CRYPTO_HASHED_ED25519,
}
}
pub(super) fn master_seed(&self) -> Option<&[u8; 32]> {
self.master_seed.as_deref()
}
pub(super) fn account_id(&self) -> [u8; 32] {
account_id(&self.descriptor)
}
fn pair_at(&self, generation: u64) -> Result<sr25519::Pair, CoreError> {
let master = self.master_seed().ok_or_else(|| {
crypto_err("hashed signing-key derivation requires the mnemonic or master seed")
})?;
Self::derive(master, generation)
}
}
impl Keypair {
fn hashed(&self) -> Result<&HashedKeypair, CoreError> {
match &self.inner {
KeypairInner::Hashed(hashed) => Ok(hashed),
_ => Err(crypto_err("operation requires a hashed key descriptor")),
}
}
pub fn from_hashed_descriptor(encoded: &[u8], ss58_format: u16) -> Result<Self, CoreError> {
if encoded.first() == Some(&subtensor_hashed::mldsa::VERSION) {
return Self::from_mldsa_descriptor(encoded, ss58_format);
}
let mut input = encoded;
let descriptor = Descriptor::decode(&mut input)
.map_err(|_| crypto_err("invalid or unsupported hashed account descriptor"))?;
if !input.is_empty() || !descriptor.is_supported() {
return Err(crypto_err(
"invalid or unsupported hashed account descriptor",
));
}
Ok(Self {
inner: KeypairInner::Hashed(HashedKeypair {
descriptor,
master_seed: None,
generation: 0,
}),
ss58_format,
mnemonic: None,
seed: None,
})
}
pub fn public_only(&self) -> Result<Self, CoreError> {
if matches!(&self.inner, KeypairInner::Mldsa(_)) {
return self.mldsa_public_only();
}
if let KeypairInner::Hashed(hashed) = &self.inner {
let mut public =
Self::from_hashed_descriptor(&hashed.descriptor.encode(), self.ss58_format)?;
if let KeypairInner::Hashed(copy) = &mut public.inner {
copy.generation = hashed.generation;
}
return Ok(public);
}
Self::new(
None,
Some(&self.public_key_bytes()),
self.crypto_type(),
self.ss58_format,
)
}
pub fn at_generation(&self, generation: u64) -> Result<Self, CoreError> {
if super::is_pq_crypto(self.crypto_type()) {
return self.mldsa_at_generation(generation);
}
let hashed = self.hashed()?;
if hashed.master_seed.is_none() {
return Err(crypto_err(
"hashed generation selection requires a private wallet",
));
}
Ok(Self {
inner: KeypairInner::Hashed(HashedKeypair {
descriptor: hashed.descriptor,
master_seed: hashed.master_seed.clone(),
generation,
}),
ss58_format: self.ss58_format,
mnemonic: self.mnemonic.clone(),
seed: self.seed.clone(),
})
}
pub fn hashed_descriptor(&self) -> Result<Vec<u8>, CoreError> {
Ok(self.hashed()?.descriptor.encode())
}
pub fn hashed_generation(&self) -> Result<u64, CoreError> {
Ok(self.hashed()?.generation)
}
pub fn hashed_public_key(&self) -> Result<[u8; 32], CoreError> {
let hashed = self.hashed()?;
if hashed.descriptor.scheme == Scheme::Ed25519 {
return Ok(ed25519_public(&hashed.ed_at(hashed.generation)?));
}
Ok(hashed.pair_at(hashed.generation)?.public().0)
}
pub fn hashed_commitment(&self, generation: u64) -> Result<[u8; 32], CoreError> {
let hashed = self.hashed()?;
if generation == 0 {
return Ok(hashed.descriptor.initial_commitment);
}
Ok(match hashed.descriptor.scheme {
Scheme::Sr25519 => {
key_commitment(Scheme::Sr25519, &hashed.pair_at(generation)?.public().0)
}
Scheme::Ed25519 => {
key_commitment(Scheme::Ed25519, &ed25519_public(&hashed.ed_at(generation)?))
}
})
}
pub fn hashed_current_commitment(&self) -> Result<[u8; 32], CoreError> {
self.hashed_commitment(self.hashed_generation()?)
}
pub fn hashed_next_commitment(&self) -> Result<[u8; 32], CoreError> {
let next = self
.hashed_generation()?
.checked_add(1)
.ok_or_else(|| crypto_err("hashed signing generation exhausted"))?;
self.hashed_commitment(next)
}
pub fn sign_hashed(&self, payload: &[u8]) -> Result<Vec<u8>, CoreError> {
let hashed = self.hashed()?;
let address = SignerAddress {
version: VERSION,
scheme: hashed.descriptor.scheme,
account: self.public_key_bytes(),
generation: hashed.generation,
public_key: self.hashed_public_key()?,
next_commitment: self.hashed_next_commitment()?,
};
let payload = address.signing_hash(payload);
let signature = match hashed.descriptor.scheme {
Scheme::Sr25519 => hashed.pair_at(hashed.generation)?.sign(&payload).0,
Scheme::Ed25519 => hashed.ed_at(hashed.generation)?.sign(&payload).to_bytes(),
};
Ok(Proof {
generation: hashed.generation,
public_key: self.hashed_public_key()?,
next_commitment: self.hashed_next_commitment()?,
signature,
}
.encode())
}
pub fn hashed_signing_public_key(&self) -> Result<Vec<u8>, CoreError> {
let hashed = self.hashed()?;
Ok(match hashed.descriptor.scheme {
Scheme::Sr25519 => hashed.pair_at(hashed.generation)?.public().0.to_vec(),
Scheme::Ed25519 => ed25519_public(&hashed.ed_at(hashed.generation)?).to_vec(),
})
}
pub(super) fn sign_hashed_message(&self, message: &[u8]) -> Result<Vec<u8>, CoreError> {
let hashed = self.hashed()?;
let payload = message_payload(&hashed.account_id(), message);
let mut envelope = MESSAGE_MAGIC.to_vec();
envelope.extend(hashed.descriptor.encode());
match hashed.descriptor.scheme {
Scheme::Sr25519 => {
if hashed.generation == 0 {
return Err(crypto_err("hashed message signing requires generation zero to be retired on chain first"));
}
let pair = hashed.pair_at(0)?;
envelope.extend_from_slice(&pair.public().0);
envelope.extend_from_slice(&pair.sign(&payload).0);
}
Scheme::Ed25519 => {
if hashed.generation == 0 {
return Err(crypto_err("hashed message signing requires generation zero to be retired on chain first"));
}
let secret = hashed.ed_at(0)?;
envelope.extend_from_slice(&ed25519_public(&secret));
envelope.extend_from_slice(&secret.sign(&payload).to_bytes());
}
}
Ok(envelope)
}
}
const MESSAGE_MAGIC: &[u8] = b"BTHMSG01";
fn ed25519_public(secret: &Zeroizing<SigningKey>) -> [u8; 32] {
let secret: &SigningKey = secret;
VerificationKey::from(secret).into()
}
fn message_payload(account: &[u8; 32], message: &[u8]) -> Vec<u8> {
let mut payload = b"bittensor/hashed/v1/message".to_vec();
(account, message).encode_to(&mut payload);
payload
}
pub(super) fn verify_message(
crypto_type: u8,
account: &[u8; 32],
message: &[u8],
envelope: &[u8],
) -> bool {
let expected_len = 8 + 34 + 32 + 64;
if envelope.len() != expected_len || !envelope.starts_with(MESSAGE_MAGIC) {
return false;
}
let mut input = &envelope[8..];
let Ok(descriptor) = Descriptor::decode(&mut input) else {
return false;
};
let scheme = if crypto_type == CRYPTO_HASHED_ED25519 {
Scheme::Ed25519
} else {
Scheme::Sr25519
};
if !descriptor.is_supported()
|| descriptor.scheme != scheme
|| account_id(&descriptor) != *account
{
return false;
}
let payload = message_payload(account, message);
match scheme {
Scheme::Sr25519 => {
let (Ok(public), Ok(signature)) = (
<[u8; 32]>::try_from(&input[..32]),
<[u8; 64]>::try_from(&input[32..]),
) else {
return false;
};
descriptor.initial_commitment == key_commitment(scheme, &public)
&& sr25519::Pair::verify(
&sr25519::Signature::from_raw(signature),
&payload,
&sr25519::Public::from_raw(public),
)
}
Scheme::Ed25519 => {
let (Ok(public), Ok(signature)) = (
<[u8; 32]>::try_from(&input[..32]),
<[u8; 64]>::try_from(&input[32..]),
) else {
return false;
};
descriptor.initial_commitment == key_commitment(scheme, &public)
&& ed25519::Pair::verify(
&ed25519::Signature::from_raw(signature),
&payload,
&ed25519::Public::from_raw(public),
)
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::keys::{CRYPTO_HASHED, DEFAULT_SS58_FORMAT};
fn transaction_payload(
account: &[u8; 32],
scheme: Scheme,
generation: u64,
next: &[u8; 32],
public: &[u8; 32],
call_and_extensions: &[u8],
) -> Vec<u8> {
SignerAddress {
version: 1,
scheme,
account: *account,
generation,
public_key: *public,
next_commitment: *next,
}
.signing_hash(call_and_extensions)
.to_vec()
}
fn sign_test_transaction(
key: &Keypair,
call_and_extensions: &[u8],
) -> Result<Vec<u8>, CoreError> {
key.sign_hashed(call_and_extensions)
}
#[test]
fn composable_schemes_preserve_mode_recovery_and_signatures() {
use crate::keys::{account_crypto_type, CRYPTO_ED25519, CRYPTO_SR25519};
let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
let mut addresses = std::collections::BTreeSet::new();
for scheme in [CRYPTO_SR25519, CRYPTO_ED25519] {
for mode in ["standard", "hashed"] {
let code = account_crypto_type(scheme, Some(mode)).unwrap();
let key = Keypair::from_mnemonic(phrase, code, None).unwrap();
assert_eq!(key.account_type(), mode);
assert!(addresses.insert(key.ss58_address()));
let recovered = Keypair::from_seed(key.seed_bytes().unwrap(), code).unwrap();
assert_eq!(recovered.public_key_bytes(), key.public_key_bytes());
if !super::super::is_hashed_crypto(code) {
let signature = key.sign(b"message").unwrap();
assert!(recovered.verify(b"message", &signature).unwrap());
continue;
}
let public = key.public_only().unwrap();
assert_eq!(public.crypto_type(), code);
assert_eq!(
key.hashed_descriptor().unwrap(),
recovered.hashed_descriptor().unwrap()
);
assert!(public.sign_hashed(b"implication").is_err());
let initial = key.hashed_signing_public_key().unwrap();
let later = key.at_generation(42).unwrap();
assert_eq!(
initial == later.hashed_signing_public_key().unwrap(),
mode == "standard"
);
assert_eq!(
later.hashed_current_commitment().unwrap()
== later.hashed_next_commitment().unwrap(),
mode == "standard"
);
assert_eq!(later.public_key_bytes(), key.public_key_bytes());
let signature = later.sign(b"message").unwrap();
assert!(public.verify(b"message", &signature).unwrap());
assert!(!public.verify(b"changed", &signature).unwrap());
let wrong_mode = account_crypto_type(
scheme,
Some(if mode == "standard" {
"hashed"
} else {
"standard"
}),
)
.unwrap();
assert!(!super::super::verify_with_crypto(
wrong_mode,
&key.public_key_bytes(),
b"message",
&signature
)
.unwrap_or(false));
}
}
}
#[test]
fn hashed_ed_proof_is_scheme_bound_and_retires_message_key() {
let key = Keypair::from_seed(&[31; 32], CRYPTO_HASHED_ED25519).unwrap();
assert!(key.sign(b"message").is_err());
let signer = key.at_generation(8).unwrap();
let bytes = sign_test_transaction(&signer, b"implication").unwrap();
let proof = Proof::decode(&mut bytes.as_slice()).unwrap();
assert_eq!(proof.generation, 8);
let payload = transaction_payload(
&key.public_key_bytes(),
Scheme::Ed25519,
8,
&proof.next_commitment,
&proof.public_key,
b"implication",
);
assert!(ed25519::Pair::verify(
&ed25519::Signature::from_raw(proof.signature),
payload,
&ed25519::Public::from_raw(proof.public_key)
));
let wrong = transaction_payload(
&key.public_key_bytes(),
Scheme::Sr25519,
8,
&proof.next_commitment,
&proof.public_key,
b"implication",
);
assert!(!ed25519::Pair::verify(
&ed25519::Signature::from_raw(proof.signature),
wrong,
&ed25519::Public::from_raw(proof.public_key)
));
}
#[test]
fn retired_sr_message_key_never_reveals_current_spending_key() {
let key = Keypair::from_seed(&[43; 32], CRYPTO_HASHED).unwrap();
assert!(key.sign(b"challenge").is_err());
let current = key.at_generation(1).unwrap();
let envelope = current.sign(b"challenge").unwrap();
assert_eq!(&envelope[42..74], &key.hashed_public_key().unwrap());
assert_ne!(&envelope[42..74], ¤t.hashed_public_key().unwrap());
assert!(key
.public_only()
.unwrap()
.verify(b"challenge", &envelope)
.unwrap());
assert!(!key.verify(b"changed challenge", &envelope).unwrap());
assert!(!Keypair::from_seed(&[44; 32], CRYPTO_HASHED)
.unwrap()
.verify(b"challenge", &envelope)
.unwrap());
}
const PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
#[test]
fn recovery_derivation_v1_is_pinned() {
let key = Keypair::from_seed(&[3; 32], CRYPTO_HASHED).unwrap();
assert_eq!(
key.ss58_address(),
"5Du5D4jstmEn3QPytQLqgowz2huycvq7XvUFpY8u5PJTx6Dd"
);
assert_eq!(
hex::encode(key.hashed_descriptor().unwrap()),
"010123c2153ab6fc4d0a3f3cf4b2ffcb1bc0ecc5d6737ecb32addbc1f20fadb3223a"
);
assert_eq!(
hex::encode(key.hashed_public_key().unwrap()),
"4220dab7c75d2b0a2088639dc3d314d22f82493cfc86a0efa36e194bc5443612"
);
assert_eq!(
hex::encode(key.at_generation(872).unwrap().hashed_public_key().unwrap()),
"18885ee3441c25bd1c0ac0986959fcf33169ec2a6300e7a7a4572010463a7366"
);
}
#[test]
fn restore_any_generation_from_mnemonic_without_signing_history() {
let wallet = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None).unwrap();
assert_eq!(
wallet.ss58_address(),
"5ELvzckdbaShVHktHtSa7gH77AWawQSyfeRLXT1sbvquNjha"
);
let advanced = wallet.at_generation(872).unwrap();
let restored = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None)
.unwrap()
.at_generation(872)
.unwrap();
assert_eq!(wallet.public_key_bytes(), advanced.public_key_bytes());
assert_eq!(advanced.ss58_address(), restored.ss58_address());
assert_eq!(
advanced.hashed_public_key().unwrap(),
restored.hashed_public_key().unwrap()
);
assert_eq!(
advanced.hashed_next_commitment().unwrap(),
restored.hashed_next_commitment().unwrap()
);
assert_eq!(wallet.hashed_generation().unwrap(), 0);
assert_ne!(
wallet.hashed_public_key().unwrap(),
advanced.hashed_public_key().unwrap()
);
assert_ne!(
wallet.hashed_current_commitment().unwrap(),
advanced.hashed_current_commitment().unwrap()
);
}
#[test]
fn proof_binds_next_generation_and_full_transaction() {
let key = Keypair::from_seed(&[9; 32], CRYPTO_HASHED)
.unwrap()
.at_generation(4)
.unwrap();
let implication = b"\x01call extensions genesis runtime versions";
let encoded = sign_test_transaction(&key, implication).unwrap();
assert_eq!(encoded.len(), 136);
let proof = Proof::decode(&mut &encoded[..]).unwrap();
let payload = transaction_payload(
&key.public_key_bytes(),
Scheme::Sr25519,
4,
&proof.next_commitment,
&proof.public_key,
implication,
);
assert!(sr25519::Pair::verify(
&sr25519::Signature::from_raw(proof.signature),
payload,
&sr25519::Public::from_raw(proof.public_key)
));
for altered in [
transaction_payload(
&key.public_key_bytes(),
Scheme::Sr25519,
5,
&proof.next_commitment,
&proof.public_key,
implication,
),
transaction_payload(
&key.public_key_bytes(),
Scheme::Sr25519,
4,
&[0; 32],
&proof.public_key,
implication,
),
transaction_payload(
&[0; 32],
Scheme::Sr25519,
4,
&proof.next_commitment,
&proof.public_key,
implication,
),
transaction_payload(
&key.public_key_bytes(),
Scheme::Sr25519,
4,
&proof.next_commitment,
&proof.public_key,
b"different call",
),
] {
assert!(!sr25519::Pair::verify(
&sr25519::Signature::from_raw(proof.signature),
altered,
&sr25519::Public::from_raw(proof.public_key)
));
}
}
#[test]
fn restored_current_key_verifies_but_old_and_sibling_keys_do_not() {
let original = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None).unwrap();
let previous = original.at_generation(40).unwrap();
let current = original.at_generation(41).unwrap();
let recovered = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None)
.unwrap()
.at_generation(41)
.unwrap();
let implication = b"\x01restored device transaction";
let proof =
Proof::decode(&mut &sign_test_transaction(&recovered, implication).unwrap()[..])
.unwrap();
let payload = transaction_payload(
¤t.public_key_bytes(),
Scheme::Sr25519,
proof.generation,
&proof.next_commitment,
&proof.public_key,
implication,
);
assert_eq!(proof.generation, 41);
assert_eq!(
previous.hashed_next_commitment().unwrap(),
current.hashed_current_commitment().unwrap()
);
assert_eq!(
proof.next_commitment,
original.hashed_commitment(42).unwrap()
);
let signature = sr25519::Signature::from_raw(proof.signature);
assert!(sr25519::Pair::verify(
&signature,
payload,
&sr25519::Public::from_raw(current.hashed_public_key().unwrap())
));
let sibling = Keypair::from_seed(&[19; 32], CRYPTO_HASHED)
.unwrap()
.at_generation(41)
.unwrap();
for other in [previous, sibling] {
assert_ne!(
key_commitment(Scheme::Sr25519, &other.hashed_public_key().unwrap()),
current.hashed_current_commitment().unwrap()
);
let other_proof =
Proof::decode(&mut &sign_test_transaction(&other, implication).unwrap()[..])
.unwrap();
let other_signature = sr25519::Signature::from_raw(other_proof.signature);
let other_payload = transaction_payload(
&other.public_key_bytes(),
Scheme::Sr25519,
other_proof.generation,
&other_proof.next_commitment,
&other_proof.public_key,
implication,
);
assert!(sr25519::Pair::verify(
&other_signature,
other_payload,
&sr25519::Public::from_raw(other_proof.public_key)
));
let expected_payload = transaction_payload(
¤t.public_key_bytes(),
Scheme::Sr25519,
41,
&other_proof.next_commitment,
&other_proof.public_key,
implication,
);
assert!(!sr25519::Pair::verify(
&other_signature,
expected_payload,
&sr25519::Public::from_raw(current.hashed_public_key().unwrap())
));
}
assert_eq!(recovered.hashed_generation().unwrap(), 41);
let retry =
Proof::decode(&mut &recovered.sign_hashed(b"replacement transaction").unwrap()[..])
.unwrap();
assert_eq!(retry.generation, 41);
assert_eq!(retry.public_key, proof.public_key);
assert_eq!(retry.next_commitment, proof.next_commitment);
}
#[test]
fn recovery_requires_the_same_mnemonic_and_derivation_passphrase() {
const OTHER_PHRASE: &str =
"legal winner thank year wave sausage worth useful legal winner thank yellow";
let original = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, Some("correct passphrase"))
.unwrap()
.at_generation(73)
.unwrap();
let recovered = Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, Some("correct passphrase"))
.unwrap()
.at_generation(73)
.unwrap();
assert_eq!(
recovered.hashed_descriptor().unwrap(),
original.hashed_descriptor().unwrap()
);
assert_eq!(
recovered.hashed_current_commitment().unwrap(),
original.hashed_current_commitment().unwrap()
);
for incorrect in [
Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, None).unwrap(),
Keypair::from_mnemonic(PHRASE, CRYPTO_HASHED, Some("wrong passphrase")).unwrap(),
Keypair::from_mnemonic(OTHER_PHRASE, CRYPTO_HASHED, Some("correct passphrase"))
.unwrap(),
] {
assert_ne!(incorrect.ss58_address(), original.ss58_address());
assert_ne!(
incorrect.hashed_commitment(73).unwrap(),
original.hashed_current_commitment().unwrap()
);
}
assert!(Keypair::from_mnemonic("not a valid mnemonic", CRYPTO_HASHED, None).is_err());
}
#[test]
fn final_usable_generation_commits_terminal_key_without_wrapping() {
let wallet = Keypair::from_seed(&[23; 32], CRYPTO_HASHED).unwrap();
let last = wallet.at_generation(u64::MAX - 1).unwrap();
let terminal = wallet.at_generation(u64::MAX).unwrap();
let proof =
Proof::decode(&mut &last.sign_hashed(b"last authorization").unwrap()[..]).unwrap();
assert_eq!(proof.generation, u64::MAX - 1);
assert_eq!(
proof.next_commitment,
terminal.hashed_current_commitment().unwrap()
);
assert_ne!(
proof.next_commitment,
wallet.hashed_current_commitment().unwrap()
);
assert!(terminal.hashed_next_commitment().is_err());
assert!(terminal
.sign_hashed(b"must never wrap to generation zero")
.is_err());
assert_eq!(wallet.hashed_generation().unwrap(), 0);
}
#[test]
fn public_descriptor_preserves_identity_without_exposing_signer() {
let key = Keypair::from_seed(&[3; 32], CRYPTO_HASHED).unwrap();
let public = key.public_only().unwrap();
assert_eq!(key.public_key_bytes(), public.public_key_bytes());
assert_eq!(
key.hashed_descriptor().unwrap(),
public.hashed_descriptor().unwrap()
);
assert_eq!(
key.hashed_current_commitment().unwrap(),
public.hashed_current_commitment().unwrap()
);
assert!(public.private_key_bytes().is_none());
assert!(public.hashed_public_key().is_err());
assert!(public.sign_hashed(b"message").is_err());
assert!(key.sign(b"message").is_err());
}
#[test]
fn descriptors_fail_closed_and_derivation_is_separated() {
let key = Keypair::from_seed(&[3; 32], CRYPTO_HASHED).unwrap();
let raw = Keypair::from_seed(&[3; 32], super::super::CRYPTO_SR25519).unwrap();
assert_ne!(key.hashed_public_key().unwrap(), raw.public_key_bytes());
for offset in [0, 1] {
let mut descriptor = key.hashed_descriptor().unwrap();
descriptor[offset] = 255;
assert!(Keypair::from_hashed_descriptor(&descriptor, DEFAULT_SS58_FORMAT).is_err());
}
let mut trailing = key.hashed_descriptor().unwrap();
trailing.push(0);
assert!(Keypair::from_hashed_descriptor(&trailing, DEFAULT_SS58_FORMAT).is_err());
assert!(Keypair::from_uri("//Alice", CRYPTO_HASHED).is_err());
}
#[test]
fn hashed_ed25519_signing_key_has_a_wiping_owner_and_keeps_vectors() {
fn owner(secret: Zeroizing<SigningKey>) -> Zeroizing<SigningKey> {
secret
}
let secret = owner(HashedKeypair::derive_ed(&[31; 32], 8).unwrap());
assert_eq!(
hex::encode(ed25519_public(&secret)),
"8b9c4ae043290453eb92a40d48f5074178031bc6623e60544ae4a2f5ab6542a1"
);
let key = Keypair::from_seed(&[31; 32], CRYPTO_HASHED_ED25519).unwrap();
assert_eq!(
key.ss58_address(),
"5EPuttWmgUjN8Wq2qKELKso6WrfRpBpgZpuNRzAwyAgQTXzo"
);
assert_eq!(
hex::encode(key.hashed_signing_public_key().unwrap()),
"87ba9efaab5ed270bf9fa5e8a4101283cd6ba1541f9166177778f1ca47aa88c7"
);
let signer = key.at_generation(8).unwrap();
assert_eq!(
hex::encode(signer.sign_hashed(b"implication").unwrap()),
"08000000000000008b9c4ae043290453eb92a40d48f5074178031bc6623e60544ae4a2f5ab6542a17d11decb0793d6c942bd149f0111f6e89a809b9438a355647213877069580c7952715702d31e438a3af305cf648a1ca1d376a5e371ac711d0faa883810639d3df553bbdf37044ad2b656d94d56011574b6673aab2222d22f9750780117a54f07"
);
assert_eq!(
hex::encode(key.at_generation(1).unwrap().sign(b"challenge").unwrap()),
"4254484d534730310103d49382c67690fe7a07e595daedccb680629a6102d4c8c6986a7ea0fae84548d787ba9efaab5ed270bf9fa5e8a4101283cd6ba1541f9166177778f1ca47aa88c7b0c5ac55ee389545d3086578a4f4d91119b9080d6f36e239c8bc6e8f3bc49825212c1e8f06de0a4c9386c109ca233d0b7b6dd5c82858fe3beef01ea954503404"
);
assert!(HashedKeypair::from_seed(&[31; 31], CRYPTO_HASHED_ED25519).is_err());
}
}