code/primitives/hashed/src/lib.rs
//! Fixed, versioned wire format for a hashed account's rotating authorization.
//!
//! Hashing hides an unused signing public key. It does not make sr25519
//! post-quantum secure after the key is disclosed, including in the mempool.
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
/// Original Falcon padded signatures with immutable account profiles.
pub mod falcon;
/// Quantum-resistant account protocol, independent of the immutable v1 format.
pub mod mldsa;
/// Registration-free composite address derivation.
pub mod safe;
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
pub const VERSION: u8 = 1;
pub const ACCOUNT_DOMAIN: &[u8] = b"bittensor/hashed/v1/account";
pub const KEY_DOMAIN: &[u8] = b"bittensor/hashed/v1/key";
pub const TRANSACTION_DOMAIN: &[u8] = b"bittensor/hashed/v1/transaction";
/// Fixed-size authorization address carried by standard MultiAddress::Raw.
/// The call and signed-extension encodings remain unchanged.
#[derive(
Clone, Debug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen,
)]
pub struct SignerAddress {
// Keep the permanent AccountId first: existing explorer account displays
// decode a Raw signer as AccountId32 and use this prefix.
pub account: [u8; 32],
pub version: u8,
pub scheme: Scheme,
pub generation: u64,
pub public_key: [u8; 32],
pub next_commitment: [u8; 32],
}
impl SignerAddress {
pub const ENCODED_LEN: usize = 106;
pub fn from_bytes(bytes: &[u8]) -> Result<Self, codec::Error> {
if bytes.len() != Self::ENCODED_LEN {
return Err("Invalid hashed signer address length".into());
}
let address = Self::decode(&mut &bytes[..])?;
if address.version != VERSION {
return Err("Unsupported hashed signer address version".into());
}
Ok(address)
}
/// Bind every address field to the complete, unshortened Substrate payload
/// (call, encoded extensions and implicit data, including genesis and era).
/// Always hash with a dedicated domain; an ordinary signature is never an
/// authorization to rotate a permanent account.
pub fn signing_hash(&self, payload: &[u8]) -> [u8; 32] {
let mut bytes = TRANSACTION_DOMAIN.to_vec();
self.encode_to(&mut bytes);
bytes.extend_from_slice(payload);
sp_core::hashing::blake2_256(&bytes)
}
}
/// The underlying signature scheme, separate from the hashed-account wrapper.
/// New schemes require an explicit protocol addition; unknown tags fail decoding.
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Encode,
Decode,
DecodeWithMemTracking,
TypeInfo,
MaxEncodedLen,
)]
pub enum Scheme {
#[codec(index = 1)]
Sr25519,
#[codec(index = 3)]
Ed25519,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Encode,
Decode,
DecodeWithMemTracking,
TypeInfo,
MaxEncodedLen,
)]
pub struct Descriptor {
pub version: u8,
pub scheme: Scheme,
pub initial_commitment: [u8; 32],
}
impl Descriptor {
/// Version 1 rotates the selected classical signature scheme.
pub fn rotates(&self) -> bool {
self.version == VERSION
}
pub fn is_supported(&self) -> bool {
(self.version == VERSION) && self.initial_commitment != [0; 32]
}
}
/// Registration API for every supported account authentication scheme.
///
/// This enum selects the registration policy only. Its discriminant is never
/// included in either protocol's permanent account ID or receiving descriptor.
/// Both schemes use the original `register` and `check_registered` dispatchables.
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Encode,
Decode,
DecodeWithMemTracking,
TypeInfo,
MaxEncodedLen,
)]
pub enum RegistrationDescriptor {
#[codec(index = 0)]
Classical(Descriptor),
#[codec(index = 1)]
MlDsa65(mldsa::Descriptor),
#[codec(index = 2)]
Falcon(mldsa::Descriptor),
}
impl From<Descriptor> for RegistrationDescriptor {
fn from(descriptor: Descriptor) -> Self {
Self::Classical(descriptor)
}
}
impl From<mldsa::Descriptor> for RegistrationDescriptor {
fn from(descriptor: mldsa::Descriptor) -> Self {
if falcon::is_account_profile(descriptor.algorithm) {
Self::Falcon(descriptor)
} else {
Self::MlDsa65(descriptor)
}
}
}
/// Bounded internal Sr25519/Ed25519 signing result; not an extrinsic format.
#[derive(
Clone, Debug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen,
)]
pub struct AuthorizationProof<const PUBLIC: usize, const SIGNATURE: usize> {
pub generation: u64,
pub public_key: [u8; PUBLIC],
pub next_commitment: [u8; 32],
pub signature: [u8; SIGNATURE],
}
fn domain_hash(domain: &[u8], value: impl Encode) -> [u8; 32] {
let mut bytes = domain.to_vec();
value.encode_to(&mut bytes);
sp_core::hashing::blake2_256(&bytes)
}
pub fn account_id(descriptor: &Descriptor) -> [u8; 32] {
domain_hash(ACCOUNT_DOMAIN, descriptor)
}
pub fn key_commitment<const N: usize>(scheme: Scheme, public_key: &[u8; N]) -> [u8; 32] {
domain_hash(KEY_DOMAIN, (VERSION, scheme, public_key))
}
/// Internal SDK proof data, split into a standard v4 signature and raw signer address.
pub type Proof = AuthorizationProof<32, 64>;
#[cfg(all(test, feature = "verify"))]
mod tests {
#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use super::*;
#[test]
fn unknown_schemes_and_versions_are_rejected() {
assert!(Scheme::decode(&mut &[0u8][..]).is_err());
assert!(Scheme::decode(&mut &[2u8][..]).is_err());
assert!(Scheme::decode(&mut &[4u8][..]).is_err());
assert!(
!Descriptor {
version: 2,
scheme: Scheme::Sr25519,
initial_commitment: [1; 32]
}
.is_supported()
);
}
#[test]
fn domain_separation_and_wire_layout_are_fixed() {
let signer = SignerAddress {
account: [8; 32],
version: VERSION,
scheme: Scheme::Ed25519,
generation: 9,
public_key: [10; 32],
next_commitment: [11; 32],
};
let mut expected = alloc::vec![8; 32];
expected.extend_from_slice(&[VERSION, 3]);
expected.extend_from_slice(&9u64.to_le_bytes());
expected.extend_from_slice(&[10; 32]);
expected.extend_from_slice(&[11; 32]);
assert_eq!(signer.encode(), expected);
assert_eq!(expected.len(), SignerAddress::ENCODED_LEN);
assert_eq!(SignerAddress::from_bytes(&expected).unwrap(), signer);
let descriptor = Descriptor {
version: VERSION,
scheme: Scheme::Sr25519,
initial_commitment: [7; 32],
};
let mut encoded = alloc::vec![1, 1];
encoded.extend_from_slice(&[7; 32]);
assert_eq!(descriptor.encode(), encoded);
assert_ne!(
account_id(&descriptor),
key_commitment(Scheme::Sr25519, &[7; 32])
);
let mut canonical = ACCOUNT_DOMAIN.to_vec();
canonical.extend_from_slice(&encoded);
assert_eq!(
account_id(&descriptor),
sp_core::hashing::blake2_256(&canonical)
);
}
}
#[cfg(test)]
mod registration_tests {
use super::*;
#[test]
fn registration_selector_preserves_inner_descriptor_encodings() {
let classical = Descriptor {
version: VERSION,
scheme: Scheme::Sr25519,
initial_commitment: [3; 32],
};
let mldsa = mldsa::Descriptor {
version: mldsa::VERSION,
algorithm: mldsa::ALGORITHM,
initial_commitment: [5; 64],
};
for (tag, inner, registration) in [
(
0u8,
classical.encode(),
RegistrationDescriptor::from(classical),
),
(1u8, mldsa.encode(), RegistrationDescriptor::from(mldsa)),
] {
let encoded = registration.encode();
assert_eq!(encoded.split_first(), Some((&tag, inner.as_slice())));
assert_eq!(
RegistrationDescriptor::decode(&mut encoded.as_slice()),
Ok(registration)
);
let mut truncated = encoded
.split_last()
.map(|(_, rest)| rest)
.unwrap_or_default();
assert!(RegistrationDescriptor::decode(&mut truncated).is_err());
}
assert!(RegistrationDescriptor::decode(&mut [2u8].as_slice()).is_err());
}
}