#![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::::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::::put(true); hashed_auth::TestVerificationWeight::set(Weight::zero()); pallet_hashed_accounts::Accounts::::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::::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); 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::::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 { 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 { 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 - >::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::(&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::::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::::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::::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::::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::::insert(NetUid::ROOT, true); pallet_subtensor::SubtokenEnabled::::insert(NetUid::ROOT, true); pallet_subtensor::Owner::::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::::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::::CallFiltered.into()) ); pallet_hashed_accounts::Accounts::::remove(account()); assert_eq!( dispatch(batch(vec![guard, stake])), Err(pallet_hashed_accounts::Error::::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::::AccountPolicyViolation ); pallet_hashed_accounts::Accounts::::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::::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::::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::::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::( 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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::Disabled, ), ( true, bad_version, pallet_hashed_accounts::Error::::UnsupportedDescriptor, ), ( true, zero_commitment, pallet_hashed_accounts::Error::::UnsupportedDescriptor, ), ] { ext().execute_with(|| { pallet_admin_utils::HashedAccountsEnabled::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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(_))) ); }); } }