code/runtime/src/hashed_tests.rs

hashed_tests.rs

1,740 lines · 70,998 bytes · d967d13588RawGitHub
#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use super::*;
use crate::hashed_extrinsic::LegacyUnchecked;
use frame_support::{dispatch::GetDispatchInfo, traits::Currency};
use sp_core::{Pair, sr25519};
use sp_runtime::{
    generic::Era,
    traits::{Applyable, Checkable},
    transaction_validity::{InvalidTransaction, TransactionSource},
};
use subtensor_hashed::{Descriptor, Scheme};
#[path = "hashed_envelope_tests.rs"]
mod envelope;
#[path = "falcon_tests.rs"]
mod falcon;
#[path = "mldsa_tests.rs"]
mod mldsa;
#[path = "mldsa_benchmark_tests.rs"]
mod mldsa_benchmarks;
#[path = "mldsa_dispatch_matrix_tests.rs"]
mod mldsa_dispatch_matrix;
#[path = "mldsa_order_tests.rs"]
mod mldsa_orders;
#[path = "hashed_native_evm_tests.rs"]
mod native_evm;
fn ext() -> sp_io::TestExternalities {
    let mut ext = sp_io::TestExternalities::default();
    ext.execute_with(|| frame_system::BlockHash::<Runtime>::insert(0, H256::zero()));
    ext
}

fn extra(nonce: u32) -> TxExtension {
    (
        (
            check_nonzero_sender::CheckNonZeroSender::new(),
            frame_system::CheckSpecVersion::new(),
            frame_system::CheckTxVersion::new(),
            frame_system::CheckGenesis::new(),
            check_mortality::CheckMortality::from(Era::Immortal),
            check_nonce::CheckNonce::from(nonce),
            frame_system::CheckWeight::new(),
        ),
        (
            transaction_payment_wrapper::ChargeTransactionPaymentWrapper::new(TaoBalance::new(0)),
            sudo_wrapper::SudoTransactionExtension::new(),
            pallet_shield::CheckShieldedTxValidity::new(),
            pallet_subtensor::SubtensorTransactionExtension::new(),
            pallet_drand::drand_priority::DrandPriority::new(),
        ),
        frame_metadata_hash_extension::CheckMetadataHash::new(false),
    )
}

fn call() -> RuntimeCall {
    RuntimeCall::System(SystemCall::remark {
        remark: b"hashed wire vector".to_vec(),
    })
}

fn pair(generation: u8) -> sr25519::Pair {
    sr25519::Pair::from_seed(&[generation; 32])
}
fn descriptor() -> Descriptor {
    Descriptor {
        version: 1,
        scheme: Scheme::Sr25519,
        initial_commitment: subtensor_hashed::key_commitment(Scheme::Sr25519, &pair(10).public().0),
    }
}
fn account() -> AccountId {
    AccountId::new(subtensor_hashed::account_id(&descriptor()))
}
fn setup() {
    pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
    hashed_auth::TestVerificationWeight::set(Weight::zero());
    pallet_hashed_accounts::Accounts::<Runtime>::insert(
        account(),
        pallet_hashed_accounts::AccountRecord {
            descriptor: descriptor().into(),
            generation: 0,
            commitment: descriptor().initial_commitment,
        },
    );
    System::inc_providers(&account());
    let _ = Balances::make_free_balance_be(&account(), TaoBalance::new(1_000_000_000_000));
}
fn signed(call: RuntimeCall, generation: u64, key: &sr25519::Pair) -> UncheckedExtrinsic {
    signed_nonce(call, generation, key, System::account_nonce(account()))
}
fn signed_nonce(
    call: RuntimeCall,
    generation: u64,
    key: &sr25519::Pair,
    nonce: u32,
) -> UncheckedExtrinsic {
    signed_with_extra(call, generation, key, extra(nonce))
}

fn signed_with_extra(
    call: RuntimeCall,
    generation: u64,
    key: &sr25519::Pair,
    extra: TxExtension,
) -> UncheckedExtrinsic {
    let next_commitment = subtensor_hashed::key_commitment(
        Scheme::Sr25519,
        &pair(u8::try_from(generation).unwrap().checked_add(11).unwrap())
            .public()
            .0,
    );
    let address = subtensor_hashed::SignerAddress {
        version: 1,
        account: *account().as_ref(),
        scheme: Scheme::Sr25519,
        generation,
        next_commitment,
        public_key: key.public().0,
    };
    let signature = key.sign(&hashed_extrinsic::signing_hash(&address, &call, &extra).unwrap());
    UncheckedExtrinsic::new_signed(
        call,
        Address::Raw(address.encode()),
        Signature::Sr25519(signature),
        extra,
    )
}

fn signing_state() -> (pallet_hashed_accounts::AccountRecord, u32, TaoBalance) {
    (
        HashedAccounts::accounts(account()).unwrap(),
        System::account_nonce(account()),
        Balances::free_balance(account()),
    )
}

fn legacy(key: &sr25519::Pair, call: RuntimeCall) -> UncheckedExtrinsic {
    let who = AccountId::from(key.public());
    let extra = extra(System::account_nonce(&who));
    let signature = SignedPayload::new(call.clone(), extra.clone())
        .unwrap()
        .using_encoded(|m| key.sign(m));
    UncheckedExtrinsic::new_signed(call, who.into(), Signature::Sr25519(signature), extra)
}
#[test]
fn wire_roundtrip_preserves_legacy_and_rejects_unknown_pipeline() {
    ext().execute_with(|| {
        setup();
        let xt = signed(call(), 0, &pair(10));
        let bytes = xt.encode();
        assert_eq!(UncheckedExtrinsic::decode(&mut &bytes[..]).unwrap(), xt);
        assert_eq!(
            xt.get_dispatch_info().extension_weight,
            xt.clone()
                .check(&ChainContext::default())
                .unwrap()
                .get_dispatch_info()
                .extension_weight
        );
        let old = LegacyUnchecked::new_signed(
            call(),
            AccountId::from(pair(1).public()).into(),
            Signature::Sr25519(pair(1).sign(b"x")),
            extra(0).into(),
        );
        assert_eq!(
            UncheckedExtrinsic::Legacy(old.clone()).encode(),
            old.encode()
        );
        let general: LegacyUnchecked = sp_runtime::generic::UncheckedExtrinsic {
            preamble: sp_runtime::generic::Preamble::General(0, extra(0).into()),
            function: call(),
        }
        .into();
        assert_eq!(
            UncheckedExtrinsic::decode(&mut &general.encode()[..]).unwrap(),
            UncheckedExtrinsic::Legacy(general)
        );
        let mut input = &bytes[..];
        let _ = codec::Compact::<u32>::decode(&mut input).unwrap();
        let offset = bytes.len() - input.len();
        let mut changed = bytes;
        changed[offset + 4 + 32] = 9; // Raw version follows its AccountId32 prefix.
        assert!(UncheckedExtrinsic::decode(&mut &changed[..]).is_err());
    });
}

fn wire_prefixes() -> [(&'static str, Vec<u8>); 5] {
    use sp_runtime::generic::Preamble;

    let bare = |version| {
        UncheckedExtrinsic::Legacy(
            sp_runtime::generic::UncheckedExtrinsic {
                preamble: Preamble::Bare(version),
                function: call(),
            }
            .into(),
        )
    };
    let general: LegacyUnchecked = sp_runtime::generic::UncheckedExtrinsic {
        preamble: Preamble::General(0, extra(0).into()),
        function: call(),
    }
    .into();
    [
        ("signed v4", legacy(&pair(1), call())),
        ("bare v4", bare(4)),
        ("bare v5", bare(5)),
        ("general v0", UncheckedExtrinsic::Legacy(general)),
        ("account envelope", signed(call(), 0, &pair(10))),
    ]
    .map(|(name, extrinsic)| {
        let encoded = extrinsic.encode();
        let mut body = encoded.as_slice();
        codec::Compact::<u32>::decode(&mut body).unwrap();
        let prefix = body.strip_suffix(call().encode().as_slice()).unwrap();
        (name, prefix.to_vec())
    })
}

fn wire_with_call(prefix: &[u8], encoded_call: &[u8]) -> Vec<u8> {
    let len = u32::try_from(prefix.len() + encoded_call.len()).unwrap();
    let mut encoded = codec::Compact(len).encode();
    encoded.extend_from_slice(prefix);
    encoded.extend_from_slice(encoded_call);
    encoded
}

fn nested_call_bytes(depth: u32, boxed: bool) -> Vec<u8> {
    let leaf = call().encode();
    let wrapper = if boxed {
        RuntimeCall::Sudo(pallet_sudo::Call::sudo {
            call: alloc::boxed::Box::new(call()),
        })
    } else {
        RuntimeCall::Utility(pallet_utility::Call::batch_all {
            calls: alloc::vec![call()],
        })
    }
    .encode();
    let prefix = wrapper.strip_suffix(leaf.as_slice()).unwrap();
    // Assemble the hostile input iteratively so producing it does not itself
    // recurse through Encode (or require a deeply nested Rust value).
    let mut encoded = Vec::new();
    for _ in 0..depth {
        encoded.extend_from_slice(prefix);
    }
    encoded.extend_from_slice(&leaf);
    encoded
}

#[test]
fn direct_call_decoding_bounds_depth_for_every_wire_format() {
    ext().execute_with(|| {
        // No feature activation or signature verification is needed to enforce
        // this bound: these are direct Decode and serde entry points.
        for (name, prefix) in wire_prefixes() {
            for boxed in [true, false] {
                for depth in [
                    1,
                    frame_support::MAX_EXTRINSIC_DEPTH
                        - <u32 as From<bool>>::from(name == "account envelope"),
                ] {
                    let encoded = wire_with_call(&prefix, &nested_call_bytes(depth, boxed));
                    let mut input = encoded.as_slice();
                    let decoded = UncheckedExtrinsic::decode(&mut input).unwrap_or_else(|error| {
                        panic!("{name}, boxed={boxed}, depth={depth}: {error}")
                    });
                    assert!(input.is_empty());
                    assert_eq!(decoded.encode(), encoded);
                }

                let encoded = wire_with_call(
                    &prefix,
                    &nested_call_bytes(frame_support::MAX_EXTRINSIC_DEPTH + 1, boxed),
                );
                let error = UncheckedExtrinsic::decode(&mut encoded.as_slice()).unwrap_err();
                assert!(
                    error
                        .to_string()
                        .contains("Maximum recursion depth reached"),
                    "{name}, boxed={boxed}: {error}"
                );
                let json =
                    alloc::format!("\"0x{}\"", sp_core::hexdisplay::HexDisplay::from(&encoded));
                let error = serde_json::from_str::<UncheckedExtrinsic>(&json).unwrap_err();
                assert!(
                    error
                        .to_string()
                        .contains("Maximum recursion depth reached"),
                    "serde {name}, boxed={boxed}: {error}"
                );
            }
        }
    });
}

#[test]
fn direct_call_decoding_preserves_enclosing_resource_limits() {
    use codec::{DecodeLimit, DecodeWithMemLimit};

    ext().execute_with(|| {
        for (name, prefix) in wire_prefixes() {
            let at_limit = wire_with_call(&prefix, &nested_call_bytes(2, true));
            assert!(
                UncheckedExtrinsic::decode_with_depth_limit(2, &mut at_limit.as_slice()).is_ok()
            );
            let too_deep = wire_with_call(&prefix, &nested_call_bytes(3, true));
            let error = UncheckedExtrinsic::decode_with_depth_limit(2, &mut too_deep.as_slice())
                .unwrap_err();
            assert!(
                error
                    .to_string()
                    .contains("Maximum recursion depth reached"),
                "enclosing depth limit {name}: {error}"
            );
            let error =
                UncheckedExtrinsic::decode_with_mem_limit(&mut at_limit.as_slice(), 1).unwrap_err();
            assert!(
                error.to_string().contains("Heap memory limit exceeded"),
                "enclosing memory limit {name}: {error}"
            );
        }
    });
}

#[test]
fn direct_call_decoding_keeps_memory_limit_for_every_wire_format() {
    ext().execute_with(|| {
        let encoded_call = RuntimeCall::System(SystemCall::remark {
            remark: alloc::vec![0; 16 * 1024 * 1024 + 1],
        })
        .encode();
        for (name, prefix) in wire_prefixes() {
            let encoded = wire_with_call(&prefix, &encoded_call);
            let error = UncheckedExtrinsic::decode(&mut encoded.as_slice()).unwrap_err();
            assert!(
                error.to_string().contains("Heap memory limit exceeded"),
                "{name}: {error}"
            );
        }
    });
}

#[test]
fn admin_switch_defaults_off_requires_root_and_preserves_authority() {
    ext().execute_with(|| {
        assert!(!HashedEnabled::get());
        frame_support::assert_noop!(
            AdminUtils::sudo_set_hashed_accounts_enabled(RuntimeOrigin::signed(account()), true),
            sp_runtime::DispatchError::BadOrigin
        );
        frame_support::assert_noop!(
            AdminUtils::sudo_set_hashed_accounts_enabled(RuntimeOrigin::none(), true),
            sp_runtime::DispatchError::BadOrigin
        );
        setup();
        let before = HashedAccounts::accounts(account()).unwrap();
        let alias = HashedAccounts::evm_alias(&account());
        pallet_hashed_accounts::EvmAliases::<Runtime>::insert(alias, account());
        frame_support::assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            false
        ));
        assert!(!HashedEnabled::get());
        assert!(Executive::apply_extrinsic(signed(call(), 0, &pair(10))).is_err());
        assert_eq!(HashedAccounts::accounts(account()).unwrap(), before);
        assert_eq!(
            pallet_hashed_accounts::EvmAliases::<Runtime>::get(alias),
            Some(account())
        );
        frame_support::assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            true
        ));
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 0, &pair(10))).unwrap()
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
    });
    ext().execute_with(|| assert!(!HashedEnabled::get()));
}

#[test]
fn included_failure_advances_generation_but_invalid_proof_does_not() {
    ext().execute_with(|| {
        setup();
        System::reset_events();
        let failing = RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
            dest: AccountId::new([25; 32]).into(),
            value: TaoBalance::new(u64::MAX),
        });
        let xt = signed(failing.clone(), 0, &pair(10));
        let invalid = signed(failing, 0, &pair(9));
        assert!(Executive::apply_extrinsic(invalid).is_err());
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 0);
        assert_eq!(System::account_nonce(account()), 0);
        assert_eq!(
            Executive::apply_extrinsic(xt.clone()).unwrap(),
            Err(sp_runtime::DispatchError::Arithmetic(
                sp_runtime::ArithmeticError::Underflow
            ))
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
        assert_eq!(System::account_nonce(account()), 1);
        assert!(Executive::apply_extrinsic(xt).is_err());
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 1, &pair(11))).unwrap()
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 2);
        assert_eq!(System::account_nonce(account()), 2);
        assert!(
            !System::events()
                .iter()
                .any(|record| matches!(record.event, RuntimeEvent::HashedAccounts(_)))
        );
    });
}
#[test]
fn rejected_weight_does_not_rotate() {
    ext().execute_with(|| {
        setup();
        hashed_auth::TestVerificationWeight::set(Weight::MAX);
        assert!(Executive::apply_extrinsic(signed(call(), 0, &pair(10))).is_err());
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 0);
        assert_eq!(System::account_nonce(account()), 0);
    });
}
#[test]
fn sponsor_can_register_without_revealing_signer_and_fund_atomically() {
    ext().execute_with(|| {
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let sponsor = pair(22);
        let sponsor_id = AccountId::from(sponsor.public());
        let _ = Balances::make_free_balance_be(&sponsor_id, TaoBalance::new(1_000_000_000_000));
        let call = RuntimeCall::Utility(pallet_utility::Call::batch_all {
            calls: alloc::vec![
                RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                    descriptor: descriptor().into()
                }),
                RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
                    dest: account().into(),
                    value: TaoBalance::new(1_000_000_000)
                }),
            ],
        });
        frame_support::assert_ok!(Executive::apply_extrinsic(legacy(&sponsor, call)).unwrap());
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 0);
        assert_eq!(System::account_nonce(account()), 1);
        assert_eq!(
            Balances::free_balance(account()),
            TaoBalance::new(1_000_000_000)
        );
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(super::hashed_tests::call(), 0, &pair(10))).unwrap()
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
    });
}
#[test]
fn registered_identity_cannot_use_legacy_signature_path() {
    ext().execute_with(|| {
        setup();
        let key = pair(20);
        let who = AccountId::from(key.public());
        pallet_hashed_accounts::Accounts::<Runtime>::insert(
            &who,
            pallet_hashed_accounts::AccountRecord {
                descriptor: descriptor().into(),
                generation: 0,
                commitment: descriptor().initial_commitment,
            },
        );
        assert!(Executive::apply_extrinsic(legacy(&key, call())).is_err());
    });
}

#[test]
fn recipient_checks_preserve_restricted_proxy_permissions_and_atomicity() {
    use alloc::boxed::Box;
    use frame_support::assert_ok;
    for proxy_type in [ProxyType::NonTransfer, ProxyType::Staking] {
        ext().execute_with(|| {
            setup();
            System::set_block_number(1);
            let real = AccountId::from(pair(22).public());
            let delegate = AccountId::from(pair(23).public());
            let _ = Balances::make_free_balance_be(&real, TaoBalance::new(1_000_000_000_000));
            assert_ok!(Proxy::add_proxy_delegate(
                &real,
                delegate.clone(),
                proxy_type,
                0
            ));
            pallet_subtensor::NetworksAdded::<Runtime>::insert(NetUid::ROOT, true);
            pallet_subtensor::SubtokenEnabled::<Runtime>::insert(NetUid::ROOT, true);
            pallet_subtensor::Owner::<Runtime>::insert(account(), account());
            let guard =
                RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::check_registered {
                    descriptor: descriptor().into(),
                });
            let stake = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake {
                hotkey: account(),
                netuid: NetUid::ROOT,
                amount_staked: TaoBalance::new(1_000_000_000),
            });
            let batch = |calls| RuntimeCall::Utility(pallet_utility::Call::batch_all { calls });
            let dispatch = |call| {
                assert_ok!(Proxy::proxy(
                    RuntimeOrigin::signed(delegate.clone()),
                    real.clone().into(),
                    Some(proxy_type),
                    Box::new(call)
                ));
                match System::events().last().unwrap().event.clone() {
                    RuntimeEvent::Proxy(pallet_proxy::Event::ProxyExecuted { result }) => result,
                    other => panic!("expected proxy result, got {other:?}"),
                }
            };
            assert_ok!(dispatch(batch(vec![guard.clone(), stake.clone()])));
            assert!(
                SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(
                    &account(),
                    &real,
                    NetUid::ROOT
                ) > 0.into()
            );
            let balance = Balances::free_balance(&real);
            let reserved = Balances::reserved_balance(&real);
            // Permission to check does not grant permission to reserve a deposit.
            assert_eq!(
                dispatch(batch(vec![
                    RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                        descriptor: descriptor().into()
                    }),
                    stake.clone()
                ])),
                Err(frame_system::Error::<Runtime>::CallFiltered.into())
            );
            // Nor can wrapping a transfer acquire permissions excluded by this proxy.
            assert_eq!(
                dispatch(batch(vec![
                    stake.clone(),
                    guard.clone(),
                    RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
                        dest: account().into(),
                        value: TaoBalance::new(1_000_000_000)
                    })
                ])),
                Err(frame_system::Error::<Runtime>::CallFiltered.into())
            );
            pallet_hashed_accounts::Accounts::<Runtime>::remove(account());
            assert_eq!(
                dispatch(batch(vec![guard, stake])),
                Err(pallet_hashed_accounts::Error::<Runtime>::NotRegistered.into())
            );
            assert_eq!(Balances::free_balance(&real), balance);
            assert_eq!(Balances::reserved_balance(&real), reserved);
            assert!(!HashedAccounts::is_registered(&account()));
        });
    }
}

#[test]
fn hashed_accounts_only_accept_hashed_proxy_delegates() {
    ext().execute_with(|| {
        setup();
        let delegate = AccountId::from(pair(30).public());
        frame_support::assert_noop!(
            Proxy::add_proxy_delegate(&account(), delegate.clone(), ProxyType::Any, 0),
            pallet_proxy::Error::<Runtime>::AccountPolicyViolation
        );
        pallet_hashed_accounts::Accounts::<Runtime>::insert(
            &delegate,
            pallet_hashed_accounts::AccountRecord {
                descriptor: descriptor().into(),
                generation: 0,
                commitment: descriptor().initial_commitment,
            },
        );
        frame_support::assert_ok!(Proxy::add_proxy_delegate(
            &account(),
            delegate.clone(),
            ProxyType::Any,
            0,
        ));
        assert!(Proxy::find_proxy(&account(), &delegate, None).is_ok());
        // Even a stale grant cannot authorize an unprotected delegate.
        pallet_hashed_accounts::Accounts::<Runtime>::remove(&delegate);
        assert!(Proxy::find_proxy(&account(), &delegate, None).is_err());
    });
}

#[cfg(feature = "runtime-benchmarks")]
#[test]
fn registration_benchmark_context_covers_outer_decode_and_proxy_cleanup() {
    use pallet_hashed_accounts::OnRegister;
    ext().execute_with(|| {
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        let sponsor = AccountId::from(pair(31).public());
        let _ = Balances::make_free_balance_be(&sponsor, TaoBalance::new(1_000_000_000_000));
        frame_support::assert_ok!(hashed_auth::OnHashedRegistered::setup_benchmark(
            &account(),
            &sponsor,
            &descriptor(),
        ));
        assert_eq!(
            Proxy::proxies(account()).0.len(),
            MaxProxies::get() as usize
        );
        assert!(System::extrinsic_data(0).len() > 16_000);
        frame_support::assert_ok!(HashedAccounts::register(
            RuntimeOrigin::signed(sponsor),
            descriptor().into()
        ));
        assert!(Proxy::proxies(account()).0.is_empty());
    });
}

#[test]
fn successive_generations_keep_the_account_and_reject_retired_keys() {
    ext().execute_with(|| {
        setup();
        let stable = account();
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 0, &pair(10))).unwrap()
        );
        let current = HashedAccounts::accounts(&stable).unwrap();
        assert_eq!(current.generation, 1);
        assert_eq!(
            current.commitment,
            subtensor_hashed::key_commitment(Scheme::Sr25519, &pair(11).public().0)
        );
        // A fresh nonce/generation cannot make a retired signing secret valid.
        let before = signing_state();
        assert_eq!(
            Executive::apply_extrinsic(signed(call(), 1, &pair(10))),
            Err(InvalidTransaction::BadProof.into())
        );
        assert_eq!(signing_state(), before);
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 1, &pair(11))).unwrap()
        );
        assert_eq!(HashedAccounts::accounts(stable).unwrap().generation, 2);
    });
}
#[test]
fn future_nonce_preparation_failure_rolls_back_rotation() {
    ext().execute_with(|| {
        setup();
        let before = HashedAccounts::accounts(account()).unwrap();
        let balance = Balances::free_balance(account());
        assert!(Executive::apply_extrinsic(signed_nonce(call(), 0, &pair(10), 5)).is_err());
        assert_eq!(HashedAccounts::accounts(account()).unwrap(), before);
        assert_eq!(System::account_nonce(account()), 0);
        assert_eq!(Balances::free_balance(account()), balance);
    });
}
#[test]
fn setup_through_derivative_wrapper_cannot_change_authorization() {
    ext().execute_with(|| {
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let sponsor = pair(22);
        let _ = Balances::make_free_balance_be(
            &AccountId::from(sponsor.public()),
            TaoBalance::new(1_000_000_000_000),
        );
        let registration = RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
            descriptor: descriptor().into(),
        });
        let call = RuntimeCall::Utility(pallet_utility::Call::as_derivative {
            index: 0,
            call: alloc::boxed::Box::new(registration),
        });
        assert!(
            Executive::apply_extrinsic(legacy(&sponsor, call))
                .unwrap()
                .is_err()
        );
        assert!(!HashedAccounts::is_registered(&account()));
    });
}

#[test]
fn insufficient_fee_preserves_authority_and_the_same_proof_can_be_retried() {
    ext().execute_with(|| {
        setup();
        // The signer has a permanent provider and enough ordinary balance;
        // only this self-paid tip makes the transaction unaffordable.
        let mut extensions = extra(0);
        extensions.1.0 = transaction_payment_wrapper::ChargeTransactionPaymentWrapper::new(
            TaoBalance::new(2_000_000_000_000),
        );
        let transaction = signed_with_extra(call(), 0, &pair(10), extensions);
        let before = signing_state();
        assert_eq!(
            Executive::apply_extrinsic(transaction.clone()),
            Err(InvalidTransaction::Payment.into())
        );
        assert_eq!(signing_state(), before);

        let _ = Balances::make_free_balance_be(&account(), TaoBalance::new(4_000_000_000_000));
        frame_support::assert_ok!(Executive::apply_extrinsic(transaction).unwrap());
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
        assert_eq!(System::account_nonce(account()), 1);
    });
}

#[test]
fn competing_devices_share_a_generation_tag_even_with_different_nonces() {
    ext().execute_with(|| {
        setup();
        let first = signed_nonce(call(), 0, &pair(10), 0);
        let competitor = signed_nonce(call(), 0, &pair(10), 1);
        let before = signing_state();
        let validate = |transaction: &UncheckedExtrinsic| {
            transaction
                .clone()
                .check(&ChainContext::default())
                .unwrap()
                .validate::<Runtime>(
                    TransactionSource::External,
                    &transaction.get_dispatch_info(),
                    transaction.encoded_size(),
                )
                .unwrap()
        };
        let first_validity = validate(&first);
        let second_validity = validate(&competitor);
        let generation_tag = (b"hashed-generation", account(), 0u64).encode();
        assert!(first_validity.provides.contains(&generation_tag));
        assert!(second_validity.provides.contains(&generation_tag));
        // Ordinary nonce tags differ; the hashed tag prevents both authorities
        // being queued as if they were sequential signing generations.
        assert_ne!(first_validity.provides, second_validity.provides);
        assert_eq!(signing_state(), before);

        frame_support::assert_ok!(Executive::apply_extrinsic(first).unwrap());
        let included = signing_state();
        assert_eq!(
            Executive::apply_extrinsic(competitor),
            Err(InvalidTransaction::BadProof.into())
        );
        assert_eq!(signing_state(), included);
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 1, &pair(11))).unwrap()
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 2);
    });
}

#[test]
fn failed_first_funding_rolls_back_registration_and_can_be_retried() {
    ext().execute_with(|| {
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let sponsor = pair(22);
        let sponsor_id = AccountId::from(sponsor.public());
        let initial_balance = TaoBalance::new(1_000_000_000_000);
        let _ = Balances::make_free_balance_be(&sponsor_id, initial_balance);
        let batch = |value| {
            RuntimeCall::Utility(pallet_utility::Call::batch_all {
                calls: alloc::vec![
                    RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                        descriptor: descriptor().into(),
                    }),
                    RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
                        dest: account().into(),
                        value,
                    }),
                ],
            })
        };
        // Balances rejects a transfer above total issuance with Underflow;
        // checking that error rules out an early registration rejection.
        assert_eq!(
            Executive::apply_extrinsic(legacy(&sponsor, batch(TaoBalance::new(u64::MAX)))).unwrap(),
            Err(sp_runtime::DispatchError::Arithmetic(
                sp_runtime::ArithmeticError::Underflow
            ))
        );
        assert!(!HashedAccounts::is_registered(&account()));
        assert!(
            !pallet_hashed_accounts::EvmAliases::<Runtime>::contains_key(
                HashedAccounts::evm_alias(&account())
            )
        );
        assert_eq!(System::providers(&account()), 0);
        assert_eq!(System::account_nonce(account()), 0);
        assert_eq!(Balances::reserved_balance(&sponsor_id), TaoBalance::new(0));
        assert_eq!(System::account_nonce(&sponsor_id), 1);
        assert!(Balances::free_balance(&sponsor_id) < initial_balance);

        frame_support::assert_ok!(
            Executive::apply_extrinsic(legacy(&sponsor, batch(TaoBalance::new(1_000_000_000))))
                .unwrap()
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 0);
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 0, &pair(10))).unwrap()
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
    });
}

#[test]
fn changing_the_call_nonce_tip_or_next_key_cannot_reuse_a_signature() {
    ext().execute_with(|| {
        setup();
        let original = signed(call(), 0, &pair(10));
        let before = signing_state();
        for field in [
            "call",
            "nonce",
            "tip",
            "account",
            "generation",
            "public_key",
            "scheme",
            "next_key",
            "signature",
        ] {
            let mut changed = original.clone();
            let UncheckedExtrinsic::Legacy(transaction) = &mut changed;
            let sp_runtime::generic::Preamble::Signed(signer, signature, extensions) =
                &mut transaction.0.preamble
            else {
                panic!("signed v4")
            };
            let Address::Raw(raw) = signer else {
                panic!("raw signer")
            };
            let mut address = subtensor_hashed::SignerAddress::from_bytes(raw).unwrap();
            match field {
                "call" => {
                    transaction.0.function = RuntimeCall::System(SystemCall::remark {
                        remark: b"altered call".to_vec(),
                    })
                }
                "nonce" => extensions.0.0.5 = check_nonce::CheckNonce::from(1),
                "tip" => {
                    extensions.0.1.0 =
                        transaction_payment_wrapper::ChargeTransactionPaymentWrapper::new(
                            TaoBalance::new(1),
                        )
                }
                "account" => address.account = [99; 32],
                "generation" => address.generation += 1,
                "public_key" => address.public_key = pair(99).public().0,
                "scheme" => address.scheme = Scheme::Ed25519,
                "next_key" => address.next_commitment = [99; 32],
                "signature" => {
                    let Signature::Sr25519(sig) = signature else {
                        panic!("sr")
                    };
                    sig.0[0] ^= 1;
                }
                _ => unreachable!(),
            }
            *raw = address.encode();
            assert_eq!(
                Executive::apply_extrinsic(changed),
                Err(InvalidTransaction::BadProof.into()),
                "mutated {field} must invalidate the signature"
            );
            assert_eq!(signing_state(), before);
        }
        frame_support::assert_ok!(Executive::apply_extrinsic(original).unwrap());
    });
}

#[test]
fn a_proof_signed_for_another_genesis_cannot_advance_the_account() {
    ext().execute_with(|| {
        setup();
        let transaction = signed(call(), 0, &pair(10));
        let before = signing_state();
        frame_system::BlockHash::<Runtime>::insert(0, H256::repeat_byte(7));
        assert_eq!(
            Executive::apply_extrinsic(transaction.clone()),
            Err(InvalidTransaction::BadProof.into())
        );
        assert_eq!(signing_state(), before);
        frame_system::BlockHash::<Runtime>::insert(0, H256::zero());
        frame_support::assert_ok!(Executive::apply_extrinsic(transaction).unwrap());
    });
}

#[test]
fn reverting_an_inclusion_restores_authority_and_rejects_the_orphaned_next_generation() {
    ext().execute_with(|| {
        setup();
        let before = signing_state();
        let original = signed(call(), 0, &pair(10));
        // Simulate the storage effect of discarding an included block. This
        // verifies state recovery, not consensus finality or key concealment.
        sp_io::storage::start_transaction();
        frame_support::assert_ok!(Executive::apply_extrinsic(original.clone()).unwrap());
        let orphaned_next = signed(call(), 1, &pair(11));
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
        sp_io::storage::rollback_transaction();
        assert_eq!(signing_state(), before);
        assert_eq!(
            Executive::apply_extrinsic(orphaned_next),
            Err(InvalidTransaction::BadProof.into())
        );
        assert_eq!(signing_state(), before);
        frame_support::assert_ok!(Executive::apply_extrinsic(original).unwrap());
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
    });
}

fn first_funding(descriptor: Descriptor, value: TaoBalance) -> RuntimeCall {
    let recipient = AccountId::new(subtensor_hashed::account_id(&descriptor));
    RuntimeCall::Utility(pallet_utility::Call::batch_all {
        calls: alloc::vec![
            RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                descriptor: descriptor.into()
            }),
            RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
                dest: recipient.into(),
                value,
            }),
        ],
    })
}

fn assert_no_received_account(recipient: &AccountId) {
    assert!(!HashedAccounts::is_registered(recipient));
    assert!(
        !pallet_hashed_accounts::EvmAliases::<Runtime>::contains_key(HashedAccounts::evm_alias(
            recipient
        ))
    );
    assert_eq!(System::providers(recipient), 0);
    assert_eq!(System::account_nonce(recipient), 0);
    assert_eq!(Balances::free_balance(recipient), TaoBalance::new(0));
}

#[test]
fn registration_deposit_guard_obeys_live_changes_without_requiring_registration_enabled() {
    ext().execute_with(|| {
        assert!(!pallet_admin_utils::HashedAccountsEnabled::<Runtime>::get());
        let quote = TaoBalance::new(200_000_000);
        frame_support::assert_ok!(HashedAccounts::check_registration_deposit(
            RuntimeOrigin::signed(account()),
            quote
        ));
        for value in [
            TaoBalance::new(350_000_000),
            TaoBalance::new(100_000_000),
            TaoBalance::new(0),
        ] {
            frame_support::assert_ok!(AdminUtils::sudo_set_hashed_account_registration_deposit(
                RuntimeOrigin::root(),
                value
            ));
            let result =
                HashedAccounts::check_registration_deposit(RuntimeOrigin::signed(account()), quote);
            if value > quote {
                assert_eq!(
                    result,
                    Err(
                        pallet_hashed_accounts::Error::<Runtime>::RegistrationDepositTooHigh.into()
                    )
                );
            } else {
                frame_support::assert_ok!(result);
            }
        }
        frame_support::assert_ok!(HashedAccounts::check_registration_deposit(
            RuntimeOrigin::signed(account()),
            TaoBalance::new(0)
        ));
    });
}

#[test]
fn registration_deposit_guard_rejects_a_stale_atomic_registration_and_funding_quote() {
    for scheme in [Scheme::Sr25519, Scheme::Ed25519] {
        ext().execute_with(|| {
            System::set_block_number(1);
            pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
            hashed_auth::TestVerificationWeight::set(Weight::zero());
            let key = pair(72);
            let sponsor = AccountId::from(key.public());
            let _ = Balances::make_free_balance_be(&sponsor, TaoBalance::new(100_000_000_000));
            let descriptor = Descriptor {
                scheme,
                ..descriptor()
            };
            let recipient = AccountId::new(subtensor_hashed::account_id(&descriptor));
            let quoted = TaoBalance::new(200_000_000);
            let batch = RuntimeCall::Utility(pallet_utility::Call::batch_all {
                calls: vec![
                    RuntimeCall::HashedAccounts(
                        pallet_hashed_accounts::Call::check_registration_deposit {
                            max_deposit: quoted,
                        },
                    ),
                    RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                        descriptor: descriptor.into(),
                    }),
                    RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
                        dest: recipient.clone().into(),
                        value: TaoBalance::new(1_000_000_000),
                    }),
                ],
            });
            frame_support::assert_ok!(AdminUtils::sudo_set_hashed_account_registration_deposit(
                RuntimeOrigin::root(),
                TaoBalance::new(350_000_000)
            ));
            assert_eq!(
                Executive::apply_extrinsic(legacy(&key, batch.clone())).unwrap(),
                Err(pallet_hashed_accounts::Error::<Runtime>::RegistrationDepositTooHigh.into())
            );
            assert_no_received_account(&recipient);
            assert_eq!(Balances::reserved_balance(&sponsor), TaoBalance::new(0));
            // The rejected extrinsic still pays its ordinary transaction fee.
            assert!(Balances::free_balance(&sponsor) < TaoBalance::new(100_000_000_000));
            frame_support::assert_ok!(AdminUtils::sudo_set_hashed_account_registration_deposit(
                RuntimeOrigin::root(),
                TaoBalance::new(100_000_000)
            ));
            frame_support::assert_ok!(Executive::apply_extrinsic(legacy(&key, batch)).unwrap());
            assert_eq!(
                Balances::reserved_balance(&sponsor),
                TaoBalance::new(100_000_000)
            );
            assert_eq!(
                Balances::free_balance(&recipient),
                TaoBalance::new(1_000_000_000)
            );
        });
    }
}

#[test]
fn registration_deposit_storage_is_owned_by_hashed_accounts() {
    let encoded = hashed_extrinsic::metadata_at_version(15).unwrap();
    let data = frame_metadata::RuntimeMetadataPrefixed::decode(&mut &encoded[..]).unwrap();
    let frame_metadata::RuntimeMetadata::V15(data) = data.1 else {
        panic!("metadata15 unavailable");
    };
    for (name, expected) in [("HashedAccounts", true), ("AdminUtils", false)] {
        let pallet = data.pallets.iter().find(|p| p.name == name).unwrap();
        let storage = pallet.storage.as_ref().unwrap();
        let entry = storage
            .entries
            .iter()
            .find(|entry| entry.name == "HashedAccountRegistrationDeposit");
        assert_eq!(entry.is_some(), expected);
        if let Some(entry) = entry {
            assert_eq!(entry.default, TaoBalance::new(200_000_000).encode());
        }
    }
}

#[test]
fn registration_deposit_updates_preserve_metadata_and_default_constants() {
    // Metadata generation must also work outside Externalities for SDK export.
    let versions = [14, 15, 16];
    let metadata = versions.map(|version| {
        hashed_extrinsic::metadata_at_version(version)
            .unwrap()
            .encode()
    });
    ext().execute_with(|| {
        let default = TaoBalance::new(200_000_000);
        assert!(!pallet_hashed_accounts::HashedAccountRegistrationDeposit::<
            Runtime,
        >::exists());
        assert_eq!(
            pallet_hashed_accounts::HashedAccountRegistrationDeposit::<Runtime>::get(),
            default
        );
        for value in [TaoBalance::new(350_000_000), TaoBalance::new(0)] {
            frame_support::assert_ok!(AdminUtils::sudo_set_hashed_account_registration_deposit(
                RuntimeOrigin::root(),
                value
            ));
            assert_eq!(
                pallet_hashed_accounts::HashedAccountRegistrationDeposit::<Runtime>::get(),
                value
            );
            assert_eq!(HashedRegistrationDeposit::get(), default);
            assert_eq!(MlDsaRegistrationDeposit::get(), default);
            assert_eq!(DerivedRegistrationDeposit::get(), default);
            for (version, expected) in versions.into_iter().zip(&metadata) {
                assert_eq!(
                    &hashed_extrinsic::metadata_at_version(version)
                        .unwrap()
                        .encode(),
                    expected
                );
            }
        }
    });
}

#[test]
fn all_registration_profiles_use_the_live_deposit_without_repricing_existing_reserves() {
    use subtensor_hashed::{RegistrationDescriptor, falcon, mldsa};

    let profiles = [
        Descriptor {
            scheme: Scheme::Sr25519,
            ..descriptor()
        }
        .into(),
        Descriptor {
            scheme: Scheme::Ed25519,
            ..descriptor()
        }
        .into(),
        mldsa::Descriptor {
            version: mldsa::VERSION,
            algorithm: mldsa::ALGORITHM,
            initial_commitment: [41; mldsa::COMMITMENT_LEN],
        }
        .into(),
        mldsa::Descriptor {
            version: mldsa::VERSION,
            algorithm: falcon::ALGORITHM_512,
            initial_commitment: [41; mldsa::COMMITMENT_LEN],
        }
        .into(),
        mldsa::Descriptor {
            version: mldsa::VERSION,
            algorithm: falcon::ALGORITHM_1024,
            initial_commitment: [41; mldsa::COMMITMENT_LEN],
        }
        .into(),
    ];
    for profile in profiles {
        ext().execute_with(|| {
            System::set_block_number(1);
            pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
            hashed_auth::TestVerificationWeight::set(Weight::zero());
            let key = pair(72);
            let sponsor = AccountId::from(key.public());
            let _ = Balances::make_free_balance_be(&sponsor, TaoBalance::new(100_000_000_000));
            let mut reserved = TaoBalance::new(0);
            let mut previous_registration = None;
            for (marker, value) in [
                (41, TaoBalance::new(200_000_000)),
                (42, TaoBalance::new(350_000_000)),
                (43, TaoBalance::new(0)),
            ] {
                frame_support::assert_ok!(
                    AdminUtils::sudo_set_hashed_account_registration_deposit(
                        RuntimeOrigin::root(),
                        value
                    )
                );
                assert_eq!(Balances::reserved_balance(&sponsor), reserved);
                if let Some(previous) = previous_registration.take() {
                    frame_support::assert_ok!(
                        Executive::apply_extrinsic(legacy(&key, previous)).unwrap()
                    );
                    assert_eq!(Balances::reserved_balance(&sponsor), reserved);
                }
                let descriptor = match profile {
                    RegistrationDescriptor::Classical(mut descriptor) => {
                        descriptor.initial_commitment = [marker; 32];
                        RegistrationDescriptor::Classical(descriptor)
                    }
                    RegistrationDescriptor::MlDsa65(mut descriptor) => {
                        descriptor.initial_commitment = [marker; mldsa::COMMITMENT_LEN];
                        RegistrationDescriptor::MlDsa65(descriptor)
                    }
                    RegistrationDescriptor::Falcon(mut descriptor) => {
                        descriptor.initial_commitment = [marker; mldsa::COMMITMENT_LEN];
                        RegistrationDescriptor::Falcon(descriptor)
                    }
                };
                let registration =
                    RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                        descriptor,
                    });
                frame_support::assert_ok!(
                    Executive::apply_extrinsic(legacy(
                        &key,
                        RuntimeCall::Utility(pallet_utility::Call::batch_all {
                            calls: vec![
                                RuntimeCall::HashedAccounts(
                                    pallet_hashed_accounts::Call::check_registration_deposit {
                                        max_deposit: value
                                    }
                                ),
                                registration.clone(),
                            ]
                        })
                    ))
                    .unwrap()
                );
                reserved += value;
                assert_eq!(Balances::reserved_balance(&sponsor), reserved);
                previous_registration = Some(registration);
            }
        });
    }
}

#[test]
fn all_registration_profiles_reserve_the_same_flat_deposit_once() {
    use frame_support::traits::ReservableCurrency;
    use subtensor_hashed::{RegistrationDescriptor, mldsa};

    let commitment = mldsa::commitment(&mldsa::public_key_from_seed(&[71; 32]).unwrap());
    let profiles = [
        Descriptor {
            scheme: Scheme::Sr25519,
            ..descriptor()
        }
        .into(),
        Descriptor {
            scheme: Scheme::Ed25519,
            ..descriptor()
        }
        .into(),
        mldsa::Descriptor {
            version: mldsa::VERSION,
            algorithm: mldsa::ALGORITHM,
            initial_commitment: commitment,
        }
        .into(),
    ];
    for descriptor in profiles {
        ext().execute_with(|| {
            System::set_block_number(1);
            pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
            hashed_auth::TestVerificationWeight::set(Weight::zero());
            let recipient = AccountId::new(match descriptor {
                RegistrationDescriptor::Classical(value) => subtensor_hashed::account_id(&value),
                RegistrationDescriptor::MlDsa65(value) | RegistrationDescriptor::Falcon(value) => {
                    mldsa::account_id(&value)
                }
            });
            let sponsor = AccountId::from(pair(72).public());
            let registration =
                RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register { descriptor });
            let deposit = TaoBalance::new(200_000_000);
            let _ = Balances::make_free_balance_be(&sponsor, deposit - TaoBalance::new(1));
            assert!(
                Executive::apply_extrinsic(legacy(&pair(72), registration.clone()))
                    .unwrap()
                    .is_err()
            );
            assert_no_received_account(&recipient);
            assert_eq!(Balances::reserved_balance(&sponsor), TaoBalance::new(0));

            let initial = TaoBalance::new(10_000_000_000);
            let existing_reserve = TaoBalance::new(1_144_000_000);
            let _ = Balances::make_free_balance_be(&sponsor, initial);
            frame_support::assert_ok!(Balances::reserve(&sponsor, existing_reserve));
            frame_support::assert_ok!(
                Executive::apply_extrinsic(legacy(&pair(72), registration.clone())).unwrap()
            );
            assert!(HashedAccounts::is_protected(&recipient));
            assert_eq!(
                Balances::reserved_balance(&sponsor),
                existing_reserve + deposit
            );
            assert!(Balances::free_balance(&sponsor) < initial - existing_reserve - deposit);
            // Repeating registration, even through another sponsor, neither
            // charges again nor releases any previously reserved balance.
            let other = AccountId::from(pair(73).public());
            let _ = Balances::make_free_balance_be(&other, TaoBalance::new(1_000_000_000));
            frame_support::assert_ok!(
                Executive::apply_extrinsic(legacy(&pair(73), registration)).unwrap()
            );
            assert_eq!(
                Balances::reserved_balance(&sponsor),
                existing_reserve + deposit
            );
            assert_eq!(Balances::reserved_balance(&other), TaoBalance::new(0));
        });
    }
}

#[test]
fn fixed_ms_registration_cannot_create_policy_or_reserve_a_deposit() {
    ext().execute_with(|| {
        System::set_block_number(1);
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let wallet = mldsa::stateless::Wallet::new(71);
        let sponsor = pair(72);
        let sponsor_account = AccountId::from(sponsor.public());
        let _ = Balances::make_free_balance_be(&sponsor_account, TaoBalance::new(10_000_000_000));
        assert_eq!(
            Executive::apply_extrinsic(legacy(
                &sponsor,
                RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                    descriptor: wallet.descriptor.into(),
                }),
            ))
            .unwrap(),
            Err(pallet_hashed_accounts::Error::<Runtime>::UnsupportedDescriptor.into())
        );
        assert_no_received_account(&wallet.account);
        assert_eq!(
            Balances::reserved_balance(&sponsor_account),
            TaoBalance::new(0)
        );
    });
}

#[test]
fn competing_first_funding_sponsors_preserve_the_recipients_rotated_authority() {
    ext().execute_with(|| {
        System::set_block_number(1);
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let first = pair(22);
        let second = pair(23);
        let first_id = AccountId::from(first.public());
        let second_id = AccountId::from(second.public());
        for sponsor in [&first_id, &second_id] {
            let _ = Balances::make_free_balance_be(sponsor, TaoBalance::new(1_000_000_000_000));
        }
        // Both senders build against the same unregistered receiving address.
        let first_transaction = legacy(
            &first,
            first_funding(descriptor(), TaoBalance::new(1_000_000_000)),
        );
        let second_transaction = legacy(
            &second,
            first_funding(descriptor(), TaoBalance::new(2_000_000_000)),
        );
        assert_no_received_account(&account());
        frame_support::assert_ok!(Executive::apply_extrinsic(first_transaction).unwrap());
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 0, &pair(10))).unwrap()
        );
        let (rotated, nonce, balance) = signing_state();
        let providers = System::providers(&account());
        frame_support::assert_ok!(Executive::apply_extrinsic(second_transaction).unwrap());
        assert_eq!(HashedAccounts::accounts(account()).unwrap(), rotated);
        assert_eq!(System::account_nonce(account()), nonce);
        assert_eq!(System::providers(&account()), providers);
        assert_eq!(
            Balances::free_balance(account()),
            balance + TaoBalance::new(2_000_000_000)
        );
        assert_eq!(
            Balances::reserved_balance(first_id),
            HashedRegistrationDeposit::get()
        );
        assert_eq!(Balances::reserved_balance(second_id), TaoBalance::new(0));
        assert_eq!(
            System::events()
                .iter()
                .filter(|event| matches!(
                    &event.event,
                    RuntimeEvent::HashedAccounts(pallet_hashed_accounts::Event::Registered { .. })
                ))
                .count(),
            1
        );
        frame_support::assert_ok!(
            Executive::apply_extrinsic(signed(call(), 1, &pair(11))).unwrap()
        );
    });
}

#[test]
fn first_funding_send_all_reserves_registration_before_computing_transferable_funds() {
    use transaction_payment_wrapper::FeeWeightDiscount;
    ext().execute_with(|| {
        System::set_block_number(1);
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let sponsor = pair(22);
        let sponsor_id = AccountId::from(sponsor.public());
        let initial = TaoBalance::new(1_000_000_000_000);
        let _ = Balances::make_free_balance_be(&sponsor_id, initial);
        let batch = RuntimeCall::Utility(pallet_utility::Call::batch_all {
            calls: alloc::vec![
                RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                    descriptor: descriptor().into()
                }),
                RuntimeCall::Balances(BalancesCall::transfer_all {
                    dest: account().into(),
                    keep_alive: false,
                }),
            ],
        });
        let transaction = legacy(&sponsor, batch.clone());
        let info = transaction.get_dispatch_info();
        let fee_info = transaction_payment_wrapper::fee_dispatch_info(
            &info,
            Runtime::fee_weight_discount(&batch, &info),
        );
        let prepaid = TransactionPayment::compute_fee(
            u32::try_from(transaction.encoded_size()).unwrap(),
            &fee_info,
            TaoBalance::new(0),
        );
        frame_support::assert_ok!(Executive::apply_extrinsic(transaction).unwrap());
        let actual_fee = System::events()
            .iter()
            .find_map(|event| match &event.event {
                RuntimeEvent::TransactionPayment(
                    pallet_transaction_payment::Event::TransactionFeePaid {
                        who, actual_fee, ..
                    },
                ) if who == &sponsor_id => Some(*actual_fee),
                _ => None,
            })
            .unwrap();
        let deposit = HashedRegistrationDeposit::get();
        assert_eq!(Balances::reserved_balance(&sponsor_id), deposit);
        // The permanent reserve also needs the sponsor's ordinary balances
        // provider, so transfer_all must retain its existential deposit.
        let retained = ExistentialDeposit::get();
        assert_eq!(
            Balances::free_balance(account()),
            initial - deposit - prepaid - retained
        );
        assert_eq!(
            Balances::free_balance(&sponsor_id),
            retained + prepaid - actual_fee
        );
        assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 0);
        assert!(System::providers(&account()) > 0);
    });
}

#[test]
fn disabled_or_unsupported_first_funding_leaves_no_recipient_or_deposit() {
    let mut bad_version = descriptor();
    bad_version.version = 2;
    let mut zero_commitment = descriptor();
    zero_commitment.initial_commitment = [0; 32];
    for (enabled, receiving_descriptor, error) in [
        (
            false,
            descriptor(),
            pallet_hashed_accounts::Error::<Runtime>::Disabled,
        ),
        (
            true,
            bad_version,
            pallet_hashed_accounts::Error::<Runtime>::UnsupportedDescriptor,
        ),
        (
            true,
            zero_commitment,
            pallet_hashed_accounts::Error::<Runtime>::UnsupportedDescriptor,
        ),
    ] {
        ext().execute_with(|| {
            pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(enabled);
            hashed_auth::TestVerificationWeight::set(Weight::zero());
            let sponsor = pair(22);
            let sponsor_id = AccountId::from(sponsor.public());
            let initial = TaoBalance::new(1_000_000_000_000);
            let _ = Balances::make_free_balance_be(&sponsor_id, initial);
            let recipient = AccountId::new(subtensor_hashed::account_id(&receiving_descriptor));
            assert_eq!(
                Executive::apply_extrinsic(legacy(
                    &sponsor,
                    first_funding(receiving_descriptor, TaoBalance::new(1_000_000_000))
                ))
                .unwrap(),
                Err(error.into()),
            );
            assert_no_received_account(&recipient);
            assert_eq!(Balances::reserved_balance(&sponsor_id), TaoBalance::new(0));
            assert_eq!(System::account_nonce(&sponsor_id), 1);
            assert!(Balances::free_balance(&sponsor_id) < initial);
        });
    }
}

#[test]
fn nested_first_funding_batch_is_filtered_without_registration_or_transfer() {
    ext().execute_with(|| {
        System::set_block_number(1);
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let sponsor = pair(22);
        let sponsor_id = AccountId::from(sponsor.public());
        let _ = Balances::make_free_balance_be(&sponsor_id, TaoBalance::new(1_000_000_000_000));
        let nested = RuntimeCall::Utility(pallet_utility::Call::batch {
            calls: alloc::vec![first_funding(descriptor(), TaoBalance::new(1_000_000_000))],
        });
        // The runtime's nesting filter rejects this before registration dispatch.
        assert_eq!(
            Executive::apply_extrinsic(legacy(&sponsor, nested)).unwrap(),
            Err(frame_system::Error::<Runtime>::CallFiltered.into())
        );
        assert_no_received_account(&account());
        assert_eq!(Balances::reserved_balance(sponsor_id), TaoBalance::new(0));
    });
}

#[test]
fn oversized_force_batch_registration_attempts_leave_no_account_or_deposit() {
    use sp_runtime::traits::Dispatchable;
    ext().execute_with(|| {
        System::set_block_number(1);
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        let sponsor = pair(22);
        let sponsor_id = AccountId::from(sponsor.public());
        let initial = TaoBalance::new(1_000_000_000_000);
        let _ = Balances::make_free_balance_be(&sponsor_id, initial);
        let registration = RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
            descriptor: descriptor().into(),
        });
        let mut calls = alloc::vec![registration; 2_000];
        calls.push(RuntimeCall::System(SystemCall::remark {
            remark: alloc::vec![0; 7 * 1024 * 1024],
        }));
        let batch = RuntimeCall::Utility(pallet_utility::Call::force_batch { calls });
        let encoded = legacy(&sponsor, batch.clone()).encode();
        let len = encoded.len();
        System::note_extrinsic(encoded);
        frame_support::assert_ok!(hashed_auth::with_registration_extrinsic_length(len, || {
            batch.dispatch(RuntimeOrigin::signed(sponsor_id.clone()))
        }));
        assert_eq!(
            System::events()
                .iter()
                .filter(|event| matches!(
                    event.event,
                    RuntimeEvent::Utility(pallet_utility::Event::ItemFailed { .. })
                ))
                .count(),
            2_000
        );
        assert_no_received_account(&account());
        assert_eq!(Balances::reserved_balance(&sponsor_id), TaoBalance::new(0));
        assert_eq!(Balances::free_balance(&sponsor_id), initial);
    });
}

#[test]
fn proxied_first_funding_cannot_leave_a_partial_registration_or_transfer() {
    ext().execute_with(|| {
        System::set_block_number(1);
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let delegate = pair(22);
        let delegate_id = AccountId::from(delegate.public());
        let real = AccountId::from(pair(23).public());
        for who in [&delegate_id, &real] {
            let _ = Balances::make_free_balance_be(who, TaoBalance::new(1_000_000_000_000));
        }
        frame_support::assert_ok!(Proxy::add_proxy_delegate(
            &real,
            delegate_id.clone(),
            ProxyType::Any,
            0
        ));
        let real_free = Balances::free_balance(&real);
        let real_reserved = Balances::reserved_balance(&real);
        let proxied = RuntimeCall::Proxy(pallet_proxy::Call::proxy {
            real: real.clone().into(),
            force_proxy_type: None,
            call: alloc::boxed::Box::new(first_funding(
                descriptor(),
                TaoBalance::new(1_000_000_000),
            )),
        });
        // Proxy also reports inner failure in an event while returning success.
        frame_support::assert_ok!(Executive::apply_extrinsic(legacy(&delegate, proxied)).unwrap());
        // DispatchError::Other omits its text when encoded into event storage.
        assert!(System::events().iter().any(|event| matches!(
            &event.event,
            RuntimeEvent::Proxy(pallet_proxy::Event::ProxyExecuted {
                result: Err(sp_runtime::DispatchError::Other(_)),
            })
        )));
        assert_no_received_account(&account());
        assert_eq!(Balances::free_balance(&real), real_free);
        assert_eq!(Balances::reserved_balance(&real), real_reserved);
        assert_eq!(Balances::reserved_balance(delegate_id), TaoBalance::new(0));
    });
}

#[test]
fn multisig_first_funding_cannot_leave_a_partial_registration_or_transfer() {
    ext().execute_with(|| {
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let signer = pair(22);
        let signer_id = AccountId::from(signer.public());
        let other = AccountId::from(pair(23).public());
        let mut signatories = alloc::vec![signer_id.clone(), other.clone()];
        signatories.sort();
        let multisig = Multisig::multi_account_id(&signatories, 1);
        let initial = TaoBalance::new(1_000_000_000_000);
        for who in [&signer_id, &multisig] {
            let _ = Balances::make_free_balance_be(who, initial);
        }
        let wrapped = RuntimeCall::Multisig(pallet_multisig::Call::as_multi_threshold_1 {
            other_signatories: alloc::vec![other],
            call: alloc::boxed::Box::new(first_funding(
                descriptor(),
                TaoBalance::new(1_000_000_000),
            )),
        });
        assert_eq!(
            Executive::apply_extrinsic(legacy(&signer, wrapped)).unwrap(),
            Err(sp_runtime::DispatchError::Other(
                "InvalidHashedRegistrationContext"
            ))
        );
        assert_no_received_account(&account());
        assert_eq!(Balances::free_balance(&multisig), initial);
        assert_eq!(Balances::reserved_balance(multisig), TaoBalance::new(0));
        assert_eq!(Balances::reserved_balance(signer_id), TaoBalance::new(0));
    });
}

#[test]
fn preassociated_classical_owner_blocks_activation_without_funding_the_account() {
    ext().execute_with(|| {
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let sponsor = pair(22);
        let sponsor_id = AccountId::from(sponsor.public());
        let classical_owner = AccountId::from(pair(23).public());
        let _ = Balances::make_free_balance_be(&sponsor_id, TaoBalance::new(1_000_000_000_000));
        pallet_subtensor::Owner::<Runtime>::insert(account(), &classical_owner);
        assert_eq!(
            Executive::apply_extrinsic(legacy(
                &sponsor,
                first_funding(descriptor(), TaoBalance::new(1_000_000_000))
            ))
            .unwrap(),
            Err(sp_runtime::DispatchError::Other(
                "HashedHotkeyRequiresHashedOwner"
            )),
        );
        assert_no_received_account(&account());
        assert_eq!(Balances::reserved_balance(sponsor_id), TaoBalance::new(0));
        assert_eq!(
            pallet_subtensor::Owner::<Runtime>::get(account()),
            classical_owner
        );
    });
}

mod composable_accounts {
    use super::*;
    use sp_core::ed25519;

    pub(super) fn register(descriptor: Descriptor) -> AccountId {
        System::set_block_number(1);
        pallet_admin_utils::HashedAccountsEnabled::<Runtime>::put(true);
        hashed_auth::TestVerificationWeight::set(Weight::zero());
        let sponsor = pair(22);
        let _ = Balances::make_free_balance_be(
            &AccountId::from(sponsor.public()),
            TaoBalance::new(1_000_000_000_000),
        );
        let account = AccountId::new(subtensor_hashed::account_id(&descriptor));
        let call = RuntimeCall::Utility(pallet_utility::Call::batch_all {
            calls: alloc::vec![
                RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                    descriptor: descriptor.into()
                }),
                RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
                    dest: account.clone().into(),
                    value: TaoBalance::new(10_000_000_000)
                }),
            ],
        });
        frame_support::assert_ok!(Executive::apply_extrinsic(legacy(&sponsor, call)).unwrap());
        account
    }

    #[test]
    fn hashed_ed25519_rotates_through_executive_and_rejects_replay() {
        ext().execute_with(|| {
            let key = ed25519::Pair::from_seed(&[51; 32]);
            let next = ed25519::Pair::from_seed(&[52; 32]);
            let descriptor = Descriptor {
                version: 1,
                scheme: Scheme::Ed25519,
                initial_commitment: subtensor_hashed::key_commitment(
                    Scheme::Ed25519,
                    &key.public().0,
                ),
            };
            let account = register(descriptor);
            System::reset_events();
            let call = call();
            let extra = extra(1);
            let next_commitment =
                subtensor_hashed::key_commitment(Scheme::Ed25519, &next.public().0);
            let address = subtensor_hashed::SignerAddress {
                version: 1,
                account: *account.as_ref(),
                scheme: Scheme::Ed25519,
                generation: 0,
                next_commitment,
                public_key: key.public().0,
            };
            let signature =
                key.sign(&hashed_extrinsic::signing_hash(&address, &call, &extra).unwrap());
            let xt = UncheckedExtrinsic::new_signed(
                call,
                Address::Raw(address.encode()),
                Signature::Ed25519(signature),
                extra,
            );
            let before = HashedAccounts::accounts(&account);
            frame_support::assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
                RuntimeOrigin::root(),
                false
            ));
            assert!(Executive::apply_extrinsic(xt.clone()).is_err());
            assert_eq!(HashedAccounts::accounts(&account), before);
            frame_support::assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
                RuntimeOrigin::root(),
                true
            ));
            frame_support::assert_ok!(Executive::apply_extrinsic(xt.clone()).unwrap());
            let record = HashedAccounts::accounts(&account).unwrap();
            assert_eq!(record.generation, 1);
            assert_eq!(record.commitment, next_commitment);
            assert_eq!(System::account_nonce(&account), 2);
            assert!(Executive::apply_extrinsic(xt).is_err());
            assert_eq!(HashedAccounts::accounts(&account).unwrap(), record);
            assert!(
                !System::events()
                    .iter()
                    .any(|record| matches!(record.event, RuntimeEvent::HashedAccounts(_)))
            );
        });
    }
}