code/runtime/src/hashed_auth.rs

hashed_auth.rs

1,289 lines · 51,200 bytes · d967d13588RawGitHub
//! Standard v4 signatures authenticate the rotating key; this wire-transparent
//! adapter applies nonce, fee and call policy to its permanent account.
use crate::{AccountId, Runtime, RuntimeCall, RuntimeOrigin, TxExtension};
use alloc::{boxed::Box, vec::Vec};
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_support::{
    dispatch::{DispatchResult, PostDispatchInfo},
    ensure,
    weights::Weight,
};
use pallet_hashed_accounts::weights::WeightInfo;
use scale_info::TypeInfo;
use sp_runtime::{
    DispatchError,
    traits::{
        DispatchInfoOf, Implication, StaticLookup, TransactionExtension,
        TransactionExtensionMetadata, ValidateResult,
    },
    transaction_validity::{
        InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError,
    },
};

#[cfg(test)]
frame_support::parameter_types! { pub static TestVerificationWeight: Weight = Weight::MAX; }
#[cfg(test)]
pub struct TestWeights;
#[cfg(test)]
impl WeightInfo for TestWeights {
    fn register_falcon() -> Weight {
        TestVerificationWeight::get()
    }
    fn check_registered_falcon() -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_fixed_falcon512(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_fixed_falcon512_invalid(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_fixed_falcon1024(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_fixed_falcon1024_invalid(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_falcon512(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_falcon512_invalid(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_falcon1024(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_falcon1024_invalid(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn register_mldsa65() -> Weight {
        TestVerificationWeight::get()
    }
    fn check_registered_mldsa65() -> Weight {
        TestVerificationWeight::get()
    }
    fn register_derived() -> Weight {
        TestVerificationWeight::get()
    }
    fn register_derived_sponsored() -> Weight {
        TestVerificationWeight::get()
    }
    fn check_derived() -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_mldsa65(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_mldsa65_invalid(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize_ed25519(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
    fn check_registered() -> Weight {
        TestVerificationWeight::get()
    }
    fn register() -> Weight {
        TestVerificationWeight::get()
    }
    fn authorize(_: u32) -> Weight {
        TestVerificationWeight::get()
    }
}

/// Wire-independent context, reconstructed exclusively from the signer address.
/// Post-quantum headers are boxed to keep transaction-extension stack frames small.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum AccountAuthorization {
    V1(subtensor_hashed::SignerAddress),
    MlDsa(Box<subtensor_hashed::mldsa::Header>),
    Falcon(Box<subtensor_hashed::falcon::Header>),
}

impl AccountAuthorization {
    pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self, codec::Error> {
        match bytes.len() {
            subtensor_hashed::SignerAddress::ENCODED_LEN => {
                subtensor_hashed::SignerAddress::from_bytes(bytes).map(Self::V1)
            }
            subtensor_hashed::mldsa::SignerAddress::ENCODED_LEN => {
                let address = subtensor_hashed::mldsa::SignerAddress::from_bytes(bytes)?;
                Ok(Self::MlDsa(Box::new(address.header())))
            }
            subtensor_hashed::falcon::PROOF_LEN_512 | subtensor_hashed::falcon::PROOF_LEN_1024 => {
                let address = subtensor_hashed::falcon::SignerAddress::from_bytes(bytes)?;
                Ok(Self::Falcon(Box::new(address.header())))
            }
            _ => Err("Invalid hashed signer address length".into()),
        }
    }

    pub(crate) fn account(&self) -> AccountId {
        AccountId::new(match self {
            Self::V1(address) => address.account,
            Self::MlDsa(address) => address.account,
            Self::Falcon(address) => address.account,
        })
    }

    fn generation(&self) -> u64 {
        match self {
            Self::V1(address) => address.generation,
            Self::MlDsa(address) => address.generation,
            Self::Falcon(address) => address.generation,
        }
    }
}

/// Charge each Falcon parameter set independently, including invalid proofs.
pub(crate) fn falcon_authorization_weight(profile: u8, len: u32) -> Weight {
    type Weights = <Runtime as pallet_hashed_accounts::Config>::WeightInfo;
    match profile {
        subtensor_hashed::falcon::ALGORITHM_512 => {
            Weights::authorize_falcon512(len).max(Weights::authorize_falcon512_invalid(len))
        }
        subtensor_hashed::falcon::ALGORITHM_1024 => {
            Weights::authorize_falcon1024(len).max(Weights::authorize_falcon1024_invalid(len))
        }
        subtensor_hashed::falcon::FIXED_ALGORITHM_512 => Weights::authorize_fixed_falcon512(len)
            .max(Weights::authorize_fixed_falcon512_invalid(len)),
        subtensor_hashed::falcon::FIXED_ALGORITHM_1024 => Weights::authorize_fixed_falcon1024(len)
            .max(Weights::authorize_fixed_falcon1024_invalid(len)),
        _ => Weight::MAX,
    }
}

pub enum ValidatedAuthorization {
    V1(pallet_hashed_accounts::ValidatedProof),
    MlDsa(pallet_hashed_accounts::ValidatedMlDsaProof),
}

/// SCALE-transparent: the signed extension fields and metadata are unchanged.
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking)]
pub struct AccountTxExtension(
    pub TxExtension,
    #[codec(skip)] pub(crate) Option<AccountAuthorization>,
);
impl TypeInfo for AccountTxExtension {
    type Identity = Self;
    fn type_info() -> scale_info::Type {
        TxExtension::<false>::type_info()
    }
}
impl From<TxExtension> for AccountTxExtension {
    fn from(value: TxExtension) -> Self {
        Self(value, None)
    }
}
fn authorized_extension(extension: TxExtension) -> TxExtension<true> {
    let (system, (payment, sudo, shield, subtensor, drand), metadata) = extension;
    (
        system,
        (
            payment.with_account_authorization(),
            sudo,
            shield,
            subtensor,
            drand,
        ),
        metadata,
    )
}

impl TransactionExtension<RuntimeCall> for AccountTxExtension {
    const IDENTIFIER: &'static str = "AccountTxExtension";
    type Implicit = <TxExtension as TransactionExtension<RuntimeCall>>::Implicit;
    type Val = (
        <TxExtension as TransactionExtension<RuntimeCall>>::Val,
        Option<ValidatedAuthorization>,
    );
    type Pre = <TxExtension as TransactionExtension<RuntimeCall>>::Pre;

    fn implicit(&self) -> Result<Self::Implicit, TransactionValidityError> {
        self.0.implicit()
    }
    fn metadata() -> Vec<TransactionExtensionMetadata> {
        TxExtension::<false>::metadata()
    }
    fn weight(&self, call: &RuntimeCall) -> Weight {
        if let Some(address) = &self.1 {
            let len = u32::try_from(call.encoded_size()).unwrap_or(u32::MAX);
            let auth = match address {
                AccountAuthorization::V1(subtensor_hashed::SignerAddress {
                    scheme: subtensor_hashed::Scheme::Sr25519, ..
                }) => {
                    <Runtime as pallet_hashed_accounts::Config>::WeightInfo::authorize(len)
                }
                AccountAuthorization::V1(subtensor_hashed::SignerAddress {
                    scheme: subtensor_hashed::Scheme::Ed25519, ..
                }) => {
                    <Runtime as pallet_hashed_accounts::Config>::WeightInfo::authorize_ed25519(len)
                }
                AccountAuthorization::MlDsa(_) => {
                    <Runtime as pallet_hashed_accounts::Config>::WeightInfo::authorize_mldsa65(len)
                        .max(<Runtime as pallet_hashed_accounts::Config>::WeightInfo::authorize_mldsa65_invalid(len))
                }
                AccountAuthorization::Falcon(header) => falcon_authorization_weight(header.algorithm, len),
            };
            self.0.weight(call).saturating_add(auth)
        } else {
            // Preserve Frontier's ordinary signed-v4 fee policy.
            <Runtime as frame_system::Config>::DbWeight::get().reads(3)
        }
    }
    fn validate(
        &self,
        origin: RuntimeOrigin,
        call: &RuntimeCall,
        info: &DispatchInfoOf<RuntimeCall>,
        len: usize,
        implicit: Self::Implicit,
        implication: &impl Implication,
        source: TransactionSource,
    ) -> ValidateResult<Self::Val, RuntimeCall> {
        if let Some(address) = &self.1 {
            let account = address.account();
            ensure!(
                frame_system::ensure_signed(origin.clone()).ok().as_ref() == Some(&account),
                InvalidTransaction::BadSigner
            );
            let ticket = match address {
                AccountAuthorization::V1(address) => crate::HashedAccounts::check_authorization(
                    &account,
                    address.scheme,
                    &address.public_key,
                    address.generation,
                    address.next_commitment,
                )
                .map(ValidatedAuthorization::V1),
                AccountAuthorization::Falcon(address) => {
                    crate::HashedAccounts::check_mldsa_authorization(
                        &account,
                        address.algorithm,
                        &address.public_key,
                        address.generation,
                        address.next_commitment,
                    )
                    .map(ValidatedAuthorization::MlDsa)
                }
                AccountAuthorization::MlDsa(address) => {
                    crate::HashedAccounts::check_mldsa_authorization(
                        &account,
                        address.algorithm,
                        &address.public_key,
                        address.generation,
                        address.next_commitment,
                    )
                    .map(ValidatedAuthorization::MlDsa)
                }
            }
            .map_err(|_| InvalidTransaction::BadProof)?;
            let (mut valid, val, origin) = authorized_extension(self.0.clone()).validate(
                origin,
                call,
                info,
                len,
                implicit,
                implication,
                source,
            )?;
            ensure!(
                frame_system::ensure_signed(origin.clone()).ok().as_ref() == Some(&account),
                InvalidTransaction::BadSigner
            );
            // Fixed stateless keys use the ordinary account nonce, including
            // queued consecutive transactions. Their proof generation is always zero.
            let stateless = match address {
                AccountAuthorization::MlDsa(header) => {
                    header.algorithm == subtensor_hashed::mldsa::FIXED_ALGORITHM
                }
                AccountAuthorization::Falcon(header) => {
                    subtensor_hashed::falcon::is_fixed_profile(header.algorithm)
                }
                AccountAuthorization::V1(_) => false,
            };
            if !stateless {
                valid
                    .provides
                    .push((b"hashed-generation", &account, address.generation()).encode());
            }
            Ok((valid, (val, Some(ticket)), origin))
        } else {
            if let Ok(signer) = frame_system::ensure_signed(origin.clone()) {
                ensure!(
                    !crate::HashedAccounts::is_protected(&signer),
                    InvalidTransaction::BadSigner
                );
            }
            self.0
                .validate(origin, call, info, len, implicit, implication, source)
                .map(|(valid, val, origin)| (valid, (val, None), origin))
        }
    }
    fn prepare(
        self,
        (val, ticket): Self::Val,
        origin: &RuntimeOrigin,
        call: &RuntimeCall,
        info: &DispatchInfoOf<RuntimeCall>,
        len: usize,
    ) -> Result<Self::Pre, TransactionValidityError> {
        if let Some(ticket) = ticket {
            match ticket {
                ValidatedAuthorization::V1(ticket) => crate::HashedAccounts::advance(&ticket),
                ValidatedAuthorization::MlDsa(ticket) => {
                    crate::HashedAccounts::advance_mldsa(&ticket)
                }
            }
            .map_err(|_| InvalidTransaction::Stale)?;
            authorized_extension(self.0).prepare(val, origin, call, info, len)
        } else {
            self.0.prepare(val, origin, call, info, len)
        }
    }
    fn post_dispatch(
        pre: Self::Pre,
        info: &DispatchInfoOf<RuntimeCall>,
        post: &mut PostDispatchInfo,
        len: usize,
        result: &DispatchResult,
    ) -> Result<(), TransactionValidityError> {
        TxExtension::<false>::post_dispatch(pre, info, post, len, result)
    }
    fn bare_validate(
        call: &RuntimeCall,
        info: &DispatchInfoOf<RuntimeCall>,
        len: usize,
    ) -> TransactionValidity {
        TxExtension::<false>::bare_validate(call, info, len)
    }
    fn bare_validate_and_prepare(
        call: &RuntimeCall,
        info: &DispatchInfoOf<RuntimeCall>,
        len: usize,
    ) -> Result<(), TransactionValidityError> {
        TxExtension::<false>::bare_validate_and_prepare(call, info, len)
    }
    fn bare_post_dispatch(
        info: &DispatchInfoOf<RuntimeCall>,
        post: &mut PostDispatchInfo,
        len: usize,
        result: &DispatchResult,
    ) -> Result<(), TransactionValidityError> {
        TxExtension::<false>::bare_post_dispatch(info, post, len, result)
    }
}

pub struct OnHashedRegistered;

/// Reject, rather than silently erase, alternate control routes when installing
/// a new PQ identity. Every existing delegate must already have a permanent floor.
fn prepare_pq_authority(account: &AccountId) -> DispatchResult {
    use pallet_hashed_accounts::SecurityFloor;
    ensure!(
        pallet_subtensor::Owner::<Runtime>::try_get(account)
            .ok()
            .is_none_or(|owner| crate::HashedAccounts::authority_meets(
                &owner,
                SecurityFloor::PqRequired
            )),
        DispatchError::Other("MlDsaHotkeyRequiresPqOwner")
    );
    let (proxies, _) = crate::pallet_proxy::Proxies::<Runtime>::get(account);
    ensure!(
        proxies
            .iter()
            .all(|proxy| crate::HashedAccounts::authority_meets(
                &proxy.delegate,
                SecurityFloor::PqRequired
            )),
        DispatchError::Other("MlDsaRequiresPqDelegates")
    );
    ensure!(
        !pallet_subtensor::ColdkeySwapAnnouncements::<Runtime>::contains_key(account)
            && !pallet_subtensor::ColdkeySwapDisputes::<Runtime>::contains_key(account),
        DispatchError::Other("MlDsaPendingColdkeySwap")
    );
    ensure!(
        pallet_multisig::Multisigs::<Runtime>::iter_key_prefix(account)
            .next()
            .is_none(),
        DispatchError::Other("MlDsaPendingMultisig")
    );
    // A sponsor cannot erase another account's pending authorizations.
    for proxy in &proxies {
        ensure!(
            crate::Proxy::announcements(proxy.delegate.clone())
                .0
                .iter()
                .all(|announcement| announcement.real() != account),
            DispatchError::Other("MlDsaPendingProxyAnnouncement")
        );
    }
    // A relayer can execute an approved order after the approving proxy is
    // removed. Require live approvals to be cancelled or expire before the
    // account acquires its permanent PQ security floor.
    ensure!(
        pallet_limit_orders::ApprovedOrders::<Runtime>::get(account).is_none_or(|state| {
            let now = crate::Timestamp::get();
            state.orders.values().all(|approval| {
                now > approval.expiry
                    || matches!(
                        approval.status,
                        Some(
                            pallet_limit_orders::OrderStatus::Fulfilled
                                | pallet_limit_orders::OrderStatus::Cancelled
                        )
                    )
            })
        }),
        DispatchError::Other("MlDsaPendingOrderApproval")
    );
    crate::evm_origin::prepare_hashed_alias(account)
}

/// An application-scoped bound supplied by Executive through CheckedExtrinsic.
/// Do not infer this from a storage read: even a zero-output `storage::read`
/// clones the complete value in the host's overlay before returning its length.
#[derive(Clone, Copy)]
struct AppliedExtrinsicLength(usize);

struct AppliedExtrinsicLengthGuard(Option<AppliedExtrinsicLength>);
impl Drop for AppliedExtrinsicLengthGuard {
    fn drop(&mut self) {
        frame_support::dispatch_context::with_context::<AppliedExtrinsicLength, _>(|value| {
            if let Some(previous) = self.0 {
                value.set(previous);
            } else {
                value.clear();
            }
        });
    }
}

/// Nested application (including a Shield inner transaction) restores its
/// caller's bound. Ordinary nested pallet dispatch inherits the outer bound.
pub(crate) fn with_registration_extrinsic_length<R>(len: usize, apply: impl FnOnce() -> R) -> R {
    use frame_support::dispatch_context::{run_in_context, with_context};
    run_in_context(|| {
        let previous = with_context::<AppliedExtrinsicLength, _>(|value| {
            let previous = value.get().copied();
            value.set(AppliedExtrinsicLength(len));
            previous
        })
        .flatten();
        let _guard = AppliedExtrinsicLengthGuard(previous);
        apply()
    })
}

// Direct benchmark dispatch lacks CheckedExtrinsic::apply. Cache its synthetic
// storage length once per FRAME dispatch so even a malformed oversized fixture
// cannot force repeated host copies through a nested force_batch.
#[cfg(any(test, feature = "runtime-benchmarks"))]
#[derive(Clone, Copy)]
struct FixtureExtrinsicLength {
    index: u32,
    stored_len: Option<u32>,
}

/// Reject an oversized applied extrinsic before any host storage access. The
/// Executive passes the exact length of the bytes it stores in ExtrinsicData,
/// so an admissible registration needs only the original single value read.
fn bounded_registration_extrinsic(max_len: u32) -> Option<Vec<u8>> {
    let applied_len =
        frame_support::dispatch_context::with_context::<AppliedExtrinsicLength, _>(|value| {
            value.get().map(|value| value.0)
        })
        .flatten();
    if let Some(len) = applied_len {
        if len > max_len as usize {
            return None;
        }
        let encoded =
            crate::System::extrinsic_data(crate::System::extrinsic_index().unwrap_or_default());
        return (encoded.len() == len).then_some(encoded);
    }
    // Benchmarks and runtime unit fixtures call pallet functions directly after
    // installing a synthetic outer extrinsic. Real external extrinsics always
    // enter the applied context, including on nodes compiled with benchmarks.
    // Production fails closed for direct dispatch without that context.
    #[cfg(any(test, feature = "runtime-benchmarks"))]
    {
        bounded_fixture_registration_extrinsic(max_len)
    }
    #[cfg(not(any(test, feature = "runtime-benchmarks")))]
    {
        None
    }
}

#[cfg(any(test, feature = "runtime-benchmarks"))]
fn bounded_fixture_registration_extrinsic(max_len: u32) -> Option<Vec<u8>> {
    use frame_support::{StorageHasher, traits::PalletInfoAccess};
    let index = crate::System::extrinsic_index().unwrap_or_default();
    let mut key =
        frame_support::storage::storage_prefix(crate::System::name().as_bytes(), b"ExtrinsicData")
            .to_vec();
    key.extend_from_slice(&frame_support::Twox64Concat::hash(&index.encode()));
    // The SCALE Vec prefix is included in storage length, but not max_len.
    let max_stored_len = max_len.checked_add(codec::Compact(max_len).encoded_size() as u32)?;
    let read_length = || sp_io::storage::read(&key, &mut [], 0);
    let stored_len =
        frame_support::dispatch_context::with_context::<FixtureExtrinsicLength, _>(|value| {
            if let Some(cached) = value.get().filter(|cached| cached.index == index) {
                return cached.stored_len;
            }
            let stored_len = read_length();
            value.set(FixtureExtrinsicLength { index, stored_len });
            stored_len
        })
        .unwrap_or_else(read_length)?;
    if stored_len > max_stored_len {
        return None;
    }
    let encoded = crate::System::extrinsic_data(index);
    (encoded.len() <= max_len as usize).then_some(encoded)
}

#[cfg(test)]
#[test]
fn registration_outer_size_limits_include_scale_prefix() {
    sp_io::TestExternalities::default().execute_with(|| {
        assert!(bounded_registration_extrinsic(16 * 1024).is_none());
        for limit in [16 * 1024, 64 * 1024] {
            for len in [limit - 1, limit, limit + 1, 7 * 1024 * 1024] {
                crate::System::note_extrinsic(alloc::vec![0; len as usize]);
                let result = with_registration_extrinsic_length(len as usize, || {
                    bounded_registration_extrinsic(limit)
                });
                assert_eq!(result.is_some(), len <= limit);
                if let Some(bytes) = result {
                    assert_eq!(bytes.len(), len as usize);
                }
            }
        }
    });
}

#[cfg(test)]
#[test]
fn oversized_registration_bounds_need_no_externalities_or_extrinsic_copy() {
    // This test deliberately has no TestExternalities. Any storage access,
    // including a length-only read that clones the host value, would panic.
    with_registration_extrinsic_length(7 * 1024 * 1024, || {
        for _ in 0..2_000 {
            assert!(bounded_registration_extrinsic(16 * 1024).is_none());
            assert!(bounded_registration_extrinsic(64 * 1024).is_none());
        }
    });
}

#[cfg(test)]
#[test]
fn registration_extrinsic_length_context_restores_nested_and_finished_scopes() {
    let current = || {
        frame_support::dispatch_context::with_context::<AppliedExtrinsicLength, _>(|value| {
            value.get().map(|value| value.0)
        })
        .flatten()
    };
    assert_eq!(current(), None);
    with_registration_extrinsic_length(7 * 1024 * 1024, || {
        assert_eq!(current(), Some(7 * 1024 * 1024));
        with_registration_extrinsic_length(128, || assert_eq!(current(), Some(128)));
        assert_eq!(current(), Some(7 * 1024 * 1024));
        assert!(bounded_registration_extrinsic(16 * 1024).is_none());
    });
    assert_eq!(current(), None);
    frame_support::dispatch_context::run_in_context(|| {
        let result = std::panic::catch_unwind(|| {
            with_registration_extrinsic_length(999, || panic!("test unwind"));
        });
        assert!(result.is_err());
        assert_eq!(current(), None);
        with_registration_extrinsic_length(7 * 1024 * 1024, || {
            let result = std::panic::catch_unwind(|| {
                with_registration_extrinsic_length(123, || panic!("nested test unwind"));
            });
            assert!(result.is_err());
            assert_eq!(current(), Some(7 * 1024 * 1024));
        });
        assert_eq!(current(), None);
    });
}

impl pallet_hashed_accounts::OnRegister for OnHashedRegistered {
    fn on_sponsored_deposit(
        account: &AccountId,
        payer: &AccountId,
        descriptor: &pallet_hashed_accounts::DerivedDescriptor,
    ) -> DispatchResult {
        use sp_runtime::generic::Preamble;
        let invalid = DispatchError::Other("InvalidDerivedDepositConsent");
        let encoded = bounded_registration_extrinsic(64 * 1024).ok_or(invalid)?;
        let mut input = encoded.as_slice();
        let outer = crate::UncheckedExtrinsic::decode(&mut input).map_err(|_| invalid)?;
        ensure!(input.is_empty(), invalid);
        let crate::UncheckedExtrinsic::Legacy(outer) = outer;
        let Preamble::Signed(address, _, _) = outer.0.preamble else {
            return Err(invalid);
        };
        let outer_account = match address {
            crate::Address::Raw(bytes) => AccountAuthorization::from_bytes(&bytes)
                .map_err(|_| invalid)?
                .account(),
            address => {
                <Runtime as frame_system::Config>::Lookup::lookup(address).map_err(|_| invalid)?
            }
        };
        ensure!(
            &outer_account == payer
                && &descriptor.account_id() == account
                && commits_sponsored_registration(&outer.0.function, payer, descriptor, 0),
            invalid
        );
        Ok(())
    }

    #[cfg(feature = "runtime-benchmarks")]
    fn setup_benchmark_sponsored(
        _: &AccountId,
        payer: &AccountId,
        descriptor: &pallet_hashed_accounts::DerivedDescriptor,
    ) -> DispatchResult {
        let mut registration =
            RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register_derived_sponsored {
                descriptor: descriptor.clone(),
                deposit_payer: payer.clone(),
            });
        // Place the match at the final permitted depth and after the maximum
        // practical number of tiny calls. A single large remark would measure
        // decoding bytes but omit the recursive consent scan.
        for _ in 0..6 {
            registration = RuntimeCall::Utility(crate::pallet_utility::Call::batch {
                calls: alloc::vec![registration],
            });
        }
        let count = crate::Utility::batched_calls_limit()
            .saturating_sub(2)
            .min(18_000);
        let mut calls = alloc::vec![RuntimeCall::System(frame_system::Call::remark {
            remark: Vec::new(),
        }); count as usize];
        calls.push(RuntimeCall::System(frame_system::Call::remark {
            remark: Vec::new(),
        }));
        calls.push(registration);
        let extension = crate::TxExtension::<false>::decode(&mut &[0u8; 64][..])
            .map_err(|_| DispatchError::Other("DerivedBenchmarkExtension"))?;
        let make_outer = |calls| {
            crate::UncheckedExtrinsic::new_signed(
                RuntimeCall::Utility(crate::pallet_utility::Call::batch { calls }),
                payer.clone().into(),
                crate::Signature::Sr25519(sp_core::sr25519::Signature::from_raw([1; 64])),
                extension.clone(),
            )
        };
        let base = make_outer(calls.clone()).encoded_size();
        let padding = (64usize * 1024)
            .checked_sub(base.saturating_add(3))
            .ok_or(DispatchError::Other("DerivedBenchmarkEnvelopeTooLarge"))?;
        if let Some(RuntimeCall::System(frame_system::Call::remark { remark })) =
            calls.get_mut(count as usize)
        {
            *remark = alloc::vec![0; padding];
        }
        let outer = make_outer(calls);
        ensure!(
            outer.encoded_size() <= 64 * 1024 && outer.encoded_size() >= 64 * 1024 - 3,
            DispatchError::Other("DerivedBenchmarkEnvelopeSize")
        );
        frame_support::storage::unhashed::put(
            sp_core::storage::well_known_keys::EXTRINSIC_INDEX,
            &0u32,
        );
        crate::System::note_extrinsic(outer.encode());
        Ok(())
    }
    fn on_register_mldsa(
        account: &AccountId,
        sponsor: &AccountId,
        _: &subtensor_hashed::mldsa::Descriptor,
    ) -> DispatchResult {
        ensure!(
            account != sponsor,
            DispatchError::Other("InvalidMlDsaSponsor")
        );
        // FRAME authenticated the sponsor, including a normal multisig or proxy
        // origin. The descriptor determines the target; the sponsor gains no key.
        prepare_pq_authority(account)
    }

    fn on_register_derived(
        account: &AccountId,
        sponsor: &AccountId,
        descriptor: &pallet_hashed_accounts::DerivedDescriptor,
    ) -> DispatchResult {
        use pallet_hashed_accounts::DerivedDescriptor;
        // System account reaping cannot prove an identity is unused: stake,
        // contracts and application state may outlive its balance and nonce.
        let consent = match descriptor {
            DerivedDescriptor::Multisig { .. } => account == sponsor,
            DerivedDescriptor::Derivative { parent, .. } => account == sponsor || parent == sponsor,
            DerivedDescriptor::PureProxy { spawner, .. } => {
                account == sponsor || spawner == sponsor
            }
        };
        ensure!(
            consent,
            DispatchError::Other("DerivedProtectionRequiresOwner")
        );
        if let pallet_hashed_accounts::DerivedDescriptor::PureProxy {
            spawner,
            proxy_type,
            height,
            extrinsic_index,
            ..
        } = descriptor
        {
            let mut input = proxy_type.as_slice();
            let decoded = crate::ProxyType::decode(&mut input)
                .map_err(|_| DispatchError::Other("InvalidProtectedProxyType"))?;
            ensure!(
                input.is_empty() && decoded.encode().as_slice() == proxy_type.as_slice(),
                DispatchError::Other("InvalidProtectedProxyType")
            );
            // Sponsored pure identities must come from this extrinsic's actual
            // creation. An existing pure account can opt in through its own origin.
            if account != sponsor {
                ensure!(
                    spawner == sponsor
                        && *height == crate::System::block_number()
                        && *extrinsic_index == crate::System::extrinsic_index().unwrap_or_default()
                        && crate::Proxy::proxies(account.clone())
                            .0
                            .iter()
                            .any(|proxy| &proxy.delegate == spawner && proxy.proxy_type == decoded),
                    DispatchError::Other("InvalidProtectedProxyCreation")
                );
            }
        }
        prepare_pq_authority(account)
    }

    #[cfg(feature = "runtime-benchmarks")]
    fn setup_benchmark_mldsa(
        account: &AccountId,
        _: &AccountId,
        _: &subtensor_hashed::mldsa::Descriptor,
    ) -> DispatchResult {
        setup_pq_registration_benchmark(account)
    }

    #[cfg(feature = "runtime-benchmarks")]
    fn setup_benchmark_derived(
        account: &AccountId,
        _: &AccountId,
        _: &pallet_hashed_accounts::DerivedDescriptor,
    ) -> DispatchResult {
        setup_pq_registration_benchmark(account)
    }

    fn on_register(
        account: &AccountId,
        sponsor: &AccountId,
        descriptor: &subtensor_hashed::Descriptor,
    ) -> DispatchResult {
        use sp_runtime::generic::Preamble;
        let invalid = DispatchError::Other("InvalidHashedRegistrationContext");
        ensure!(account != sponsor, invalid);
        let encoded = bounded_registration_extrinsic(16 * 1024).ok_or(invalid)?;
        let mut bytes = encoded.as_slice();
        let outer = crate::UncheckedExtrinsic::decode(&mut bytes).map_err(|_| invalid)?;
        ensure!(bytes.is_empty(), invalid);
        let crate::UncheckedExtrinsic::Legacy(outer) = outer;
        let Preamble::Signed(address, _, _) = outer.0.preamble else {
            return Err(invalid);
        };
        let outer_account = match address {
            crate::Address::Raw(bytes) => AccountAuthorization::from_bytes(&bytes)
                .map_err(|_| invalid)?
                .account(),
            address => {
                <Runtime as frame_system::Config>::Lookup::lookup(address).map_err(|_| invalid)?
            }
        };
        let call = outer.0.function;
        ensure!(&outer_account == sponsor, invalid);
        let matches_registration = |call: &RuntimeCall| {
            matches!(call,
                RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register { descriptor: subtensor_hashed::RegistrationDescriptor::Classical(descriptor) })
                if subtensor_hashed::account_id(descriptor) == *AsRef::<[u8;32]>::as_ref(account)
            )
        };
        let supported_followup = |call: &RuntimeCall| {
            matches!(
                call,
                RuntimeCall::Balances(_) | RuntimeCall::SubtensorModule(_)
            )
        };
        let deposit_guard = |call: &RuntimeCall| {
            matches!(
                call,
                RuntimeCall::HashedAccounts(
                    pallet_hashed_accounts::Call::check_registration_deposit { .. }
                )
            )
        };
        let allowed = match &call {
            RuntimeCall::HashedAccounts(_) => matches_registration(&call),
            RuntimeCall::Utility(crate::pallet_utility::Call::batch_all { calls }) => {
                match calls.as_slice() {
                    [guard, registration] if deposit_guard(guard) => {
                        matches_registration(registration)
                    }
                    [guard, registration, followup] if deposit_guard(guard) => {
                        matches_registration(registration) && supported_followup(followup)
                    }
                    [registration, followup] => {
                        matches_registration(registration) && supported_followup(followup)
                    }
                    _ => false,
                }
            }
            _ => false,
        };
        ensure!(allowed, invalid);
        // The account record is not written yet. Check the incoming scheme so
        // registration cannot preserve a weaker preexisting hotkey owner.
        ensure!(
            pallet_subtensor::Owner::<Runtime>::try_get(account)
                .ok()
                .is_none_or(|owner| authority_satisfies_scheme(descriptor.scheme, &owner)),
            DispatchError::Other("HashedHotkeyRequiresHashedOwner")
        );
        crate::evm_origin::prepare_hashed_alias(account)?;
        crate::Proxy::remove_all_proxy_delegates(account);
        pallet_subtensor::ColdkeySwapAnnouncements::<Runtime>::remove(account);
        pallet_subtensor::ColdkeySwapDisputes::<Runtime>::remove(account);
        Ok(())
    }

    #[cfg(feature = "runtime-benchmarks")]
    fn setup_benchmark(
        account: &AccountId,
        sponsor: &AccountId,
        descriptor: &subtensor_hashed::Descriptor,
    ) -> DispatchResult {
        use frame_support::traits::Currency;
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        let _ = crate::Balances::make_free_balance_be(
            account,
            crate::TaoBalance::new(1_000_000_000_000),
        );
        for index in 0..crate::MaxProxies::get() {
            let delegate = AccountId::new(sp_io::hashing::blake2_256(
                &(b"hashed-benchmark-proxy", index).encode(),
            ));
            crate::Proxy::add_proxy_delegate(account, delegate, crate::ProxyType::Any, 0)?;
        }
        let registration = RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
            descriptor: (*descriptor).into(),
        });
        // Exercise decoding a near-limit first-use batch, not just the tiny
        // register call: the registration hook also inspects its outer call.
        let call = RuntimeCall::Utility(crate::pallet_utility::Call::batch_all {
            calls: alloc::vec![
                registration,
                RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights {
                    netuid: 1.into(),
                    dests: alloc::vec![0; 4000],
                    weights: alloc::vec![0; 4000],
                    version_key: 0,
                }),
            ],
        });
        let extension = crate::TxExtension::<false>::decode(&mut &[0u8; 64][..])
            .map_err(|_| DispatchError::Other("HashedBenchmarkExtension"))?;
        let xt = crate::UncheckedExtrinsic::new_signed(
            call,
            sponsor.clone().into(),
            sp_runtime::MultiSignature::Sr25519(sp_core::sr25519::Signature::from_raw([0; 64])),
            extension,
        );
        // System's setter is std/test-only; benchmark setup also runs in Wasm.
        frame_support::storage::unhashed::put(
            sp_core::storage::well_known_keys::EXTRINSIC_INDEX,
            &0u32,
        );
        crate::System::note_extrinsic(xt.encode());
        Ok(())
    }
}

fn commits_sponsored_registration(
    call: &RuntimeCall,
    payer: &AccountId,
    descriptor: &pallet_hashed_accounts::DerivedDescriptor,
    depth: u8,
) -> bool {
    if depth >= 8 {
        return false;
    }
    let nested =
        |call: &RuntimeCall| commits_sponsored_registration(call, payer, descriptor, depth + 1);
    match call {
        RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register_derived_sponsored {
            descriptor: committed,
            deposit_payer,
        }) => committed == descriptor && deposit_payer == payer,
        RuntimeCall::Utility(
            crate::pallet_utility::Call::batch { calls }
            | crate::pallet_utility::Call::batch_all { calls }
            | crate::pallet_utility::Call::force_batch { calls },
        ) => calls.iter().any(nested),
        RuntimeCall::Utility(
            crate::pallet_utility::Call::as_derivative { call, .. }
            | crate::pallet_utility::Call::as_safe_derivative { call, .. }
            | crate::pallet_utility::Call::as_protected_derivative { call, .. },
        ) => nested(call),
        RuntimeCall::Proxy(
            crate::pallet_proxy::Call::proxy { call, .. }
            | crate::pallet_proxy::Call::proxy_announced { call, .. },
        ) => nested(call),
        RuntimeCall::Multisig(
            pallet_multisig::Call::as_multi { call, .. }
            | pallet_multisig::Call::as_multi_threshold_1 { call, .. }
            | pallet_multisig::Call::as_multi_safe { call, .. }
            | pallet_multisig::Call::as_multi_threshold_1_safe { call, .. },
        ) => nested(call),
        _ => false,
    }
}

#[cfg(test)]
#[test]
fn sponsored_registration_consent_traverses_safe_wrappers_without_changing_its_commitment() {
    let payer = AccountId::new([1; 32]);
    let descriptor = pallet_hashed_accounts::DerivedDescriptor::Derivative {
        parent: payer.clone(),
        index: 7,
    };
    let inner =
        RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register_derived_sponsored {
            descriptor: descriptor.clone(),
            deposit_payer: payer.clone(),
        });
    let wrappers = [
        RuntimeCall::Utility(crate::pallet_utility::Call::as_safe_derivative {
            index: 3,
            call: Box::new(inner.clone()),
        }),
        RuntimeCall::Multisig(pallet_multisig::Call::as_multi_safe {
            threshold: 2,
            other_signatories: alloc::vec![AccountId::new([2; 32])],
            maybe_timepoint: None,
            call: Box::new(inner.clone()),
            max_weight: Weight::zero(),
        }),
        RuntimeCall::Multisig(pallet_multisig::Call::as_multi_threshold_1_safe {
            other_signatories: alloc::vec![AccountId::new([2; 32])],
            call: Box::new(inner),
        }),
    ];
    for call in wrappers {
        assert!(commits_sponsored_registration(
            &call,
            &payer,
            &descriptor,
            0
        ));
        assert!(!commits_sponsored_registration(
            &call,
            &AccountId::new([3; 32]),
            &descriptor,
            0
        ));
        assert!(!commits_sponsored_registration(
            &call,
            &payer,
            &descriptor,
            7
        ));
    }
    let deposit = RuntimeCall::EVM(pallet_evm::Call::deposit {
        address: sp_core::H160::repeat_byte(1),
        value: None,
    });
    assert!(!commits_sponsored_registration(
        &deposit,
        &payer,
        &descriptor,
        0
    ));
}

/// Alternative authorities must also use committed, rotating keys.
/// Ordinary accounts retain their existing ownership and proxy behavior.
pub(crate) fn compatible_authority(account: &AccountId, authority: &AccountId) -> bool {
    use pallet_hashed_accounts::{AuthenticationKind, SecurityFloor};
    let floor = match crate::HashedAccounts::authentication_kind(account) {
        AuthenticationKind::Ordinary => SecurityFloor::Unrestricted,
        AuthenticationKind::HashedV1 => SecurityFloor::Committed,
        AuthenticationKind::MlDsa | AuthenticationKind::DerivedPq => SecurityFloor::PqRequired,
    };
    crate::HashedAccounts::authority_meets(authority, floor)
}

fn authority_satisfies_scheme(_: subtensor_hashed::Scheme, authority: &AccountId) -> bool {
    crate::HashedAccounts::authority_meets(
        authority,
        pallet_hashed_accounts::SecurityFloor::Committed,
    )
}

/// Prevent weaker delegates from bypassing a protected account's policy.
pub struct HashedProxyPolicy;
impl frame_support::traits::Contains<(AccountId, AccountId)> for HashedProxyPolicy {
    fn contains((real, delegate): &(AccountId, AccountId)) -> bool {
        compatible_authority(real, delegate)
    }
}
impl crate::pallet_proxy::ProxyAccountPolicy<AccountId> for HashedProxyPolicy {
    fn requires_protection(account: &AccountId) -> bool {
        crate::HashedAccounts::authority_meets(
            account,
            pallet_hashed_accounts::SecurityFloor::PqRequired,
        )
    }
    fn on_create_protected_pure(
        pure: &AccountId,
        spawner: &AccountId,
        proxy_type_encoded: &[u8],
        index: u16,
        height_encoded: &[u8],
        extrinsic_index: u32,
    ) -> DispatchResult {
        let mut height_input = height_encoded;
        let height = u32::decode(&mut height_input)
            .map_err(|_| DispatchError::Other("InvalidProtectedProxyHeight"))?;
        ensure!(
            height_input.is_empty(),
            DispatchError::Other("InvalidProtectedProxyHeight")
        );
        let descriptor = pallet_hashed_accounts::DerivedDescriptor::PureProxy {
            spawner: spawner.clone(),
            proxy_type: proxy_type_encoded
                .to_vec()
                .try_into()
                .map_err(|_| DispatchError::Other("InvalidProtectedProxyType"))?,
            index,
            height,
            extrinsic_index,
        };
        ensure!(
            &descriptor.account_id() == pure,
            DispatchError::Other("InvalidProtectedProxyAccount")
        );
        crate::HashedAccounts::register_derived_with_sponsor(spawner, descriptor)
    }
    #[cfg(feature = "runtime-benchmarks")]
    fn setup_protected_benchmark(account: &AccountId) -> Result<AccountId, DispatchError> {
        setup_pq_benchmark_graph(account)
    }
    fn weight() -> Weight {
        pallet_subtensor::staking::fee_preserving_additional_work(
            <Runtime as frame_system::Config>::DbWeight::get().reads(6),
        )
    }
    fn grant_weight() -> Weight {
        use crate::pallet_proxy::WeightInfo;
        pallet_subtensor::staking::fee_preserving_additional_work(
            <Runtime as frame_system::Config>::DbWeight::get()
                .reads(7)
                .saturating_add(
                    <Runtime as crate::pallet_proxy::Config>::WeightInfo::reject_announcement(
                        crate::MaxPending::get(),
                        crate::MaxProxies::get(),
                    ),
                ),
        )
    }
    fn on_grant(real: &AccountId, delegate: &AccountId) {
        if crate::HashedAccounts::is_protected(real) {
            crate::Proxy::invalidate_announcements(real, delegate);
        }
    }
}

/// Utility descendants keep the parent's immutable PQ security floor.
pub struct HashedDerivedPolicy;
impl crate::pallet_utility::DerivedAccountPolicy<AccountId> for HashedDerivedPolicy {
    fn requires_protection(account: &AccountId) -> bool {
        crate::HashedAccounts::authority_meets(
            account,
            pallet_hashed_accounts::SecurityFloor::PqRequired,
        )
    }
    fn weight() -> Weight {
        pallet_subtensor::staking::fee_preserving_additional_work(
            crate::HashedAccounts::protected_query_weight(),
        )
    }
    fn on_protected_derivative(
        parent: &AccountId,
        child: &AccountId,
        index: u16,
    ) -> DispatchResult {
        let descriptor = pallet_hashed_accounts::DerivedDescriptor::Derivative {
            parent: parent.clone(),
            index,
        };
        ensure!(
            &descriptor.account_id() == child,
            DispatchError::Other("InvalidProtectedDerivative")
        );
        crate::HashedAccounts::register_derived_with_sponsor(parent, descriptor)
    }
    #[cfg(feature = "runtime-benchmarks")]
    fn setup_protected_benchmark(parent: &AccountId) -> Result<AccountId, DispatchError> {
        setup_pq_benchmark_graph(parent)
    }
}

/// A valid depth-15, 1023-node parent makes a new child reach both protocol
/// bounds. Every cached record is derived from an already established PQ leaf.
#[cfg(feature = "runtime-benchmarks")]
fn setup_pq_benchmark_graph(marker: &AccountId) -> Result<AccountId, DispatchError> {
    use frame_support::traits::Currency;
    use pallet_hashed_accounts::{DerivedAuthorities, DerivedAuthorityRecord, DerivedDescriptor};
    let seed = sp_io::hashing::blake2_256(&(b"pq/benchmark/leaf", marker).encode());
    let public =
        subtensor_hashed::mldsa::public_key_from_seed(&seed).map_err(DispatchError::Other)?;
    let descriptor = subtensor_hashed::mldsa::Descriptor {
        version: 2,
        algorithm: 4,
        initial_commitment: subtensor_hashed::mldsa::commitment(&public),
    };
    let leaf = AccountId::new(subtensor_hashed::mldsa::account_id(&descriptor));
    pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);

    pallet_hashed_accounts::MlDsaAccounts::<Runtime>::insert(
        &leaf,
        pallet_hashed_accounts::MlDsaAccountRecord {
            descriptor,
            generation: 0,
            commitment: descriptor.initial_commitment,
        },
    );
    let mut members = Vec::new();
    for member in 0u16..100 {
        let mut parent = leaf.clone();
        let length = if member == 99 {
            14u8
        } else if member < 17 {
            10
        } else {
            9
        };
        for depth in 1u8..=length {
            let child = DerivedDescriptor::Derivative {
                parent: parent.clone(),
                index: member.saturating_mul(16).saturating_add(u16::from(depth)),
            };
            let account = child.account_id();
            let dependencies = alloc::vec![parent]
                .try_into()
                .map_err(|_| DispatchError::Other("BenchmarkProofBound"))?;
            DerivedAuthorities::<Runtime>::insert(
                &account,
                DerivedAuthorityRecord {
                    descriptor: child,
                    pq_dependencies: dependencies,
                    depth,
                    nodes: u32::from(depth).saturating_add(1),
                },
            );
            parent = account;
        }
        members.push(parent);
    }
    members.sort();
    let members: pallet_hashed_accounts::DerivedSignatories = members
        .try_into()
        .map_err(|_| DispatchError::Other("BenchmarkMembersBound"))?;
    let descriptor = DerivedDescriptor::Multisig {
        threshold: 100,
        signatories: members.clone(),
    };
    let parent = descriptor.account_id();
    DerivedAuthorities::<Runtime>::insert(
        &parent,
        DerivedAuthorityRecord {
            descriptor,
            pq_dependencies: members,
            depth: 15,
            nodes: 1023,
        },
    );
    let _ = crate::Balances::make_free_balance_be(
        &parent,
        crate::TaoBalance::new(1_000_000_000_000_000),
    );
    Ok(parent)
}

/// Maximum valid alternate-authority scans and the nonempty alias sweep. The
/// announcements intentionally target another real account; target-specific
/// pending approvals would correctly make registration fail instead.
#[cfg(feature = "runtime-benchmarks")]
fn setup_pq_registration_benchmark(account: &AccountId) -> DispatchResult {
    use frame_support::traits::Currency;
    use pallet_evm::AddressMapping;
    use pallet_hashed_accounts::{DerivedAuthorities, DerivedDescriptor};
    let owner = setup_pq_benchmark_graph(account)?;
    pallet_subtensor::Owner::<Runtime>::insert(account, &owner);
    let _ = crate::Balances::make_free_balance_be(
        account,
        crate::TaoBalance::new(1_000_000_000_000_000),
    );
    let template = DerivedAuthorities::<Runtime>::get(&owner)
        .ok_or(DispatchError::Other("BenchmarkGraphMissing"))?;
    for index in 0..crate::MaxProxies::get() {
        let mut record = template.clone();
        if let DerivedDescriptor::Multisig { threshold, .. } = &mut record.descriptor {
            *threshold = u16::try_from(index.saturating_add(1))
                .map_err(|_| DispatchError::Other("BenchmarkProxyBound"))?;
        }
        let delegate = record.descriptor.account_id();
        DerivedAuthorities::<Runtime>::insert(&delegate, record);
        let _ = crate::Balances::make_free_balance_be(
            &delegate,
            crate::TaoBalance::new(1_000_000_000_000_000),
        );
        crate::Proxy::add_proxy_delegate(account, delegate.clone(), crate::ProxyType::Any, 1)?;
        let unrelated = AccountId::new(sp_io::hashing::blake2_256(
            &(b"pq/benchmark/announcement", account, index).encode(),
        ));
        let _ = crate::Balances::make_free_balance_be(
            &unrelated,
            crate::TaoBalance::new(1_000_000_000_000_000),
        );
        crate::Proxy::add_proxy_delegate(&unrelated, delegate.clone(), crate::ProxyType::Any, 1)?;
        for pending in 0..crate::MaxPending::get() {
            let hash = sp_core::H256::from(sp_io::hashing::blake2_256(&(index, pending).encode()));
            crate::Proxy::announce(
                RuntimeOrigin::signed(delegate.clone()),
                unrelated.clone().into(),
                hash,
            )?;
        }
    }
    let alias = sp_core::H160::from(crate::HashedAccounts::evm_alias(account));
    let backing =
        pallet_evm::HashedAddressMapping::<sp_runtime::traits::BlakeTwo256>::into_account_id(alias);
    let _ = crate::Balances::make_free_balance_be(&backing, crate::TaoBalance::new(1_000_000_000));
    Ok(())
}