//! Registrationless fixed-key MS uses ordinary origins and durable native nonces. use super::*; use pallet_hashed_accounts::{AuthenticationKind, DerivedAuthorities, EvmAliases}; pub(crate) struct Wallet { pub(crate) seed: [u8; 32], pub(crate) descriptor: mldsa::Descriptor, pub(crate) account: AccountId, } impl Wallet { pub(crate) fn new(identity: u8) -> Self { for attempt in 0u32..256 { let mut seed = [identity; 32]; seed[..4].copy_from_slice(&attempt.to_le_bytes()); let public_key = mldsa::public_key_from_seed(&seed).unwrap(); let descriptor = mldsa::Descriptor { version: mldsa::VERSION, algorithm: mldsa::FIXED_ALGORITHM, initial_commitment: mldsa::commitment(&public_key), }; let account = mldsa::account_id(&descriptor); if mldsa::rejects_legacy_public_keys(&account) { return Self { seed, descriptor, account: AccountId::new(account), }; } } panic!("stateless fixture could not find an admissible native account") } pub(crate) fn sign(&self, call: RuntimeCall) -> UncheckedExtrinsic { self.sign_nonce(call, System::account_nonce(&self.account)) } fn sign_nonce(&self, call: RuntimeCall, nonce: u32) -> UncheckedExtrinsic { let extra = extra(nonce); let header = mldsa::Header { account: *self.account.as_ref(), version: mldsa::VERSION, algorithm: mldsa::FIXED_ALGORITHM, generation: 0, public_key: mldsa::public_key_from_seed(&self.seed).unwrap(), next_commitment: self.descriptor.initial_commitment, }; let payload = (&call, &extra, extra.implicit().unwrap()).encode(); let address = header.sign(&self.seed, &payload, &[0; 32]).unwrap(); UncheckedExtrinsic::new_signed( call, Address::Raw(address.encode()), Signature::Ed25519(sp_core::ed25519::Signature::from_raw( mldsa::CARRIER_SIGNATURE, )), extra, ) } } fn setup() { System::set_block_number(1); pallet_admin_utils::HashedAccountsEnabled::::put(true); hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0)); } fn fund(account: &AccountId) { let _ = Balances::make_free_balance_be(account, TaoBalance::new(1_000_000_000_000)); } fn transfer(destination: &AccountId, amount: u64) -> RuntimeCall { RuntimeCall::Balances(BalancesCall::transfer_keep_alive { dest: destination.clone().into(), value: TaoBalance::new(amount), }) } fn assert_registrationless(account: &AccountId) { assert!(!MlDsaAccounts::::contains_key(account)); assert!(HashedAccounts::accounts(account).is_none()); assert!(!DerivedAuthorities::::contains_key(account)); assert!(!EvmAliases::::contains_key( HashedAccounts::evm_alias(account) )); assert_eq!( HashedAccounts::authentication_kind(account), AuthenticationKind::Ordinary ); assert_eq!(Balances::reserved_balance(account), TaoBalance::new(0)); } #[test] fn prefunded_account_spends_without_registration_or_a_sponsor_deposit() { ext().execute_with(|| { setup(); let wallet = Wallet::new(80); let funder = pair(81); let funder_account = AccountId::from(funder.public()); fund(&funder_account); assert_ok!( Executive::apply_extrinsic(legacy(&funder, transfer(&wallet.account, 10_000_000_000),)) .unwrap() ); assert_eq!(System::account_nonce(&wallet.account), 0); assert_registrationless(&wallet.account); assert_eq!( Balances::reserved_balance(&funder_account), TaoBalance::new(0) ); let destination = AccountId::new([82; 32]); let tx = wallet.sign(transfer(&destination, 1_000_000_000)); let encoded = tx.encode(); assert_eq!(UncheckedExtrinsic::decode(&mut &encoded[..]).unwrap(), tx); let balance = Balances::free_balance(&wallet.account); assert!(tx.clone().check(&ChainContext::default()).is_ok()); assert_eq!(System::account_nonce(&wallet.account), 0); assert_eq!(Balances::free_balance(&wallet.account), balance); assert_registrationless(&wallet.account); assert_ok!(Executive::apply_extrinsic(tx.clone()).unwrap()); assert_eq!( Balances::free_balance(destination), TaoBalance::new(1_000_000_000) ); assert_eq!(System::account_nonce(&wallet.account), 1); assert_registrationless(&wallet.account); assert!(Executive::apply_extrinsic(tx).is_err()); assert_ok!(Executive::apply_extrinsic(wallet.sign(call())).unwrap()); assert_eq!(System::account_nonce(&wallet.account), 2); assert_eq!( frame_system::Account::::get(&wallet.account).sufficients, 1 ); assert_registrationless(&wallet.account); }); } #[test] fn dispatch_failure_consumes_nonce_but_invalid_proof_does_not() { ext().execute_with(|| { setup(); let wallet = Wallet::new(83); fund(&wallet.account); let tx = wallet.sign(transfer(&AccountId::new([84; 32]), u64::MAX)); let mut invalid = tx.clone(); let UncheckedExtrinsic::Legacy(ref mut legacy) = invalid; legacy.0.function = call(); let balance = Balances::free_balance(&wallet.account); assert!(Executive::apply_extrinsic(invalid).is_err()); assert_eq!(System::account_nonce(&wallet.account), 0); assert_eq!(Balances::free_balance(&wallet.account), balance); assert!(Executive::apply_extrinsic(tx.clone()).unwrap().is_err()); assert_eq!(System::account_nonce(&wallet.account), 1); assert!(Balances::free_balance(&wallet.account) < balance); assert!(Executive::apply_extrinsic(tx).is_err()); assert_ok!(Executive::apply_extrinsic(wallet.sign(call())).unwrap()); assert_eq!(System::account_nonce(&wallet.account), 2); assert_registrationless(&wallet.account); }); } #[test] fn admin_pause_blocks_stateless_auth_without_consuming_the_nonce() { ext().execute_with(|| { setup(); let wallet = Wallet::new(100); fund(&wallet.account); let tx = wallet.sign(call()); let balance = Balances::free_balance(&wallet.account); let account = frame_system::Account::::get(&wallet.account); assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled( RuntimeOrigin::root(), false )); assert!(Executive::apply_extrinsic(tx.clone()).is_err()); assert_eq!( frame_system::Account::::get(&wallet.account), account ); assert_eq!(Balances::free_balance(&wallet.account), balance); assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled( RuntimeOrigin::root(), true )); assert_ok!(Executive::apply_extrinsic(tx).unwrap()); assert_eq!(System::account_nonce(&wallet.account), 1); assert_registrationless(&wallet.account); }); } #[test] fn draining_and_refunding_cannot_reset_consumed_native_nonces() { ext().execute_with(|| { setup(); let wallet = Wallet::new(85); fund(&wallet.account); let first = wallet.sign(call()); assert_ok!(Executive::apply_extrinsic(first.clone()).unwrap()); let drain = wallet.sign(RuntimeCall::Balances(BalancesCall::transfer_all { dest: AccountId::new([86; 32]).into(), keep_alive: false, })); assert_ok!(Executive::apply_extrinsic(drain.clone()).unwrap()); // Remove any fee refund too, exercising balances' complete reap path. assert_ok!(Balances::force_set_balance( RuntimeOrigin::root(), wallet.account.clone().into(), TaoBalance::new(0), )); assert_eq!(Balances::free_balance(&wallet.account), TaoBalance::new(0)); assert_eq!(System::account_nonce(&wallet.account), 2); assert!(frame_system::Account::::contains_key( &wallet.account )); fund(&wallet.account); let refunded = Balances::free_balance(&wallet.account); for consumed in [first, drain] { assert!(Executive::apply_extrinsic(consumed).is_err()); assert_eq!(System::account_nonce(&wallet.account), 2); assert_eq!(Balances::free_balance(&wallet.account), refunded); } assert_ok!(Executive::apply_extrinsic(wallet.sign(call())).unwrap()); assert_eq!(System::account_nonce(&wallet.account), 3); assert_registrationless(&wallet.account); }); } #[test] fn exhausted_native_nonce_is_rejected_without_wrapping_or_charging() { ext().execute_with(|| { setup(); let wallet = Wallet::new(103); fund(&wallet.account); frame_system::Account::::mutate(&wallet.account, |account| { account.nonce = u32::MAX }); let before = frame_system::Account::::get(&wallet.account); let tx = wallet.sign_nonce(call(), u32::MAX); assert!( Executive::validate_transaction( sp_runtime::transaction_validity::TransactionSource::External, tx.clone(), System::parent_hash(), ) .is_err() ); assert_eq!( frame_system::Account::::get(&wallet.account), before ); assert!(Executive::apply_extrinsic(tx).is_err()); assert_eq!( frame_system::Account::::get(&wallet.account), before ); assert_registrationless(&wallet.account); }); } #[test] fn consecutive_fixed_transactions_have_independent_pool_nonce_tags() { ext().execute_with(|| { setup(); let wallet = Wallet::new(104); fund(&wallet.account); let first = wallet.sign_nonce(call(), 0); let second = wallet.sign_nonce(call(), 1); let validate = |tx| { Executive::validate_transaction( sp_runtime::transaction_validity::TransactionSource::External, tx, System::parent_hash(), ) .unwrap() }; let first_validity = validate(first.clone()); let second_validity = validate(second.clone()); let first_tag = (&wallet.account, 0u32).encode(); let second_tag = (&wallet.account, 1u32).encode(); let obsolete_generation_tag = (b"hashed-generation", &wallet.account, 0u64).encode(); assert!(first_validity.provides.contains(&first_tag)); assert!(second_validity.provides.contains(&second_tag)); assert!(second_validity.requires.contains(&first_tag)); assert!(!first_validity.provides.contains(&obsolete_generation_tag)); assert!(!second_validity.provides.contains(&obsolete_generation_tag)); assert_eq!(System::account_nonce(&wallet.account), 0); let before = frame_system::Account::::get(&wallet.account); assert!(Executive::apply_extrinsic(second.clone()).is_err()); // Authorization prepares a sufficient reference before CheckNonce rejects // this future transaction. The failed application must roll it all back. assert_eq!( frame_system::Account::::get(&wallet.account), before ); assert_registrationless(&wallet.account); assert_ok!(Executive::apply_extrinsic(first).unwrap()); assert_ok!(Executive::apply_extrinsic(second).unwrap()); assert_eq!(System::account_nonce(&wallet.account), 2); assert_registrationless(&wallet.account); }); } #[test] fn hostile_hotkey_association_does_not_block_direct_ms_spending() { ext().execute_with(|| { setup(); let wallet = Wallet::new(87); let attacker = pair(88); let attacker_account = AccountId::from(attacker.public()); fund(&attacker_account); assert_ok!( Executive::apply_extrinsic(legacy( &attacker, RuntimeCall::SubtensorModule(pallet_subtensor::Call::try_associate_hotkey { hotkey: wallet.account.clone(), }), )) .unwrap() ); assert_eq!( pallet_subtensor::Owner::::get(&wallet.account), attacker_account ); fund(&wallet.account); let destination = AccountId::new([89; 32]); assert_ok!( Executive::apply_extrinsic(wallet.sign(transfer(&destination, 1_000_000_000))).unwrap() ); assert_eq!( Balances::free_balance(destination), TaoBalance::new(1_000_000_000) ); assert_eq!(System::account_nonce(&wallet.account), 1); assert_registrationless(&wallet.account); }); } #[test] fn ordinary_classical_proxy_can_be_granted_and_execute() { ext().execute_with(|| { setup(); let wallet = Wallet::new(90); fund(&wallet.account); let delegate = pair(91); let delegate_account = AccountId::from(delegate.public()); fund(&delegate_account); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Proxy( pallet_proxy::Call::add_proxy { delegate: delegate_account.clone().into(), proxy_type: ProxyType::Any, delay: 0, }, ))) .unwrap() ); let destination = AccountId::new([92; 32]); assert_ok!( Executive::apply_extrinsic(legacy( &delegate, RuntimeCall::Proxy(pallet_proxy::Call::proxy { real: wallet.account.clone().into(), force_proxy_type: Some(ProxyType::Any), call: Box::new(transfer(&destination, 1_000_000_000)), }), )) .unwrap() ); assert_eq!( Balances::free_balance(destination), TaoBalance::new(1_000_000_000) ); assert_eq!(System::account_nonce(&wallet.account), 1); assert!(!MlDsaAccounts::::contains_key(&wallet.account)); assert!(!HashedAccounts::is_protected(&wallet.account)); }); } #[test] fn ordinary_multisig_accepts_ms_approval_without_protecting_the_composite() { ext().execute_with(|| { setup(); let wallet = Wallet::new(93); fund(&wallet.account); let other = pair(94); let other_account = AccountId::from(other.public()); fund(&other_account); let mut members = vec![wallet.account.clone(), other_account.clone()]; members.sort(); let composite = Multisig::multi_account_id(&members, 2); fund(&composite); let destination = AccountId::new([95; 32]); let inner = transfer(&destination, 1_000_000_000); let hash = sp_io::hashing::blake2_256(&inner.encode()); let max_weight = inner.get_dispatch_info().total_weight(); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Multisig( pallet_multisig::Call::approve_as_multi { threshold: 2, other_signatories: vec![other_account.clone()], maybe_timepoint: None, call_hash: hash, max_weight, }, ))) .unwrap() ); let pending = pallet_multisig::Multisigs::::get(&composite, hash).unwrap(); assert_ok!( Executive::apply_extrinsic(legacy( &other, RuntimeCall::Multisig(pallet_multisig::Call::as_multi { threshold: 2, other_signatories: vec![wallet.account.clone()], maybe_timepoint: Some(pending.when), call: Box::new(inner), max_weight, }), )) .unwrap() ); assert_eq!( Balances::free_balance(destination), TaoBalance::new(1_000_000_000) ); assert!(!pallet_multisig::Multisigs::::contains_key( &composite, hash )); assert_registrationless(&wallet.account); assert_registrationless(&composite); }); } #[test] fn ordinary_derivative_and_pure_proxy_creation_need_no_protected_registration() { ext().execute_with(|| { setup(); let wallet = Wallet::new(96); fund(&wallet.account); let child = Utility::derivative_account_id(wallet.account.clone(), 7).unwrap(); fund(&child); let destination = AccountId::new([97; 32]); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Utility( pallet_utility::Call::as_derivative { index: 7, call: Box::new(transfer(&destination, 1_000_000_000)), }, ))) .unwrap() ); assert_eq!( Balances::free_balance(destination), TaoBalance::new(1_000_000_000) ); assert_registrationless(&child); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Proxy( pallet_proxy::Call::create_pure { proxy_type: ProxyType::Any, delay: 0, index: 8, }, ))) .unwrap() ); let pure = System::events() .iter() .find_map(|record| match &record.event { RuntimeEvent::Proxy(pallet_proxy::Event::PureCreated { pure, who, .. }) if who == &wallet.account => { Some(pure.clone()) } _ => None, }) .unwrap(); assert!(!DerivedAuthorities::::contains_key(&pure)); assert!(!EvmAliases::::contains_key( HashedAccounts::evm_alias(&pure) )); assert_eq!(Proxy::proxies(pure).0[0].delegate, wallet.account); assert!(!MlDsaAccounts::::contains_key(&wallet.account)); }); } #[test] fn safe_ms_multisig_requires_the_quorum_without_registration() { ext().execute_with(|| { setup(); let first = Wallet::new(121); let second = Wallet::new(122); fund(&first.account); fund(&second.account); let mut members = vec![first.account.clone(), second.account.clone()]; members.sort(); let composite = Multisig::safe_multi_account_id(&members, 2).unwrap(); assert!(mldsa::rejects_legacy_public_keys(composite.as_ref())); assert_ne!(composite, Multisig::multi_account_id(&members, 2)); fund(&composite); let destination = AccountId::new([123; 32]); let inner = transfer(&destination, 1_000_000_000); let hash = sp_io::hashing::blake2_256(&inner.encode()); let max_weight = inner.get_dispatch_info().total_weight(); assert_ok!( Executive::apply_extrinsic(first.sign(RuntimeCall::Multisig( pallet_multisig::Call::approve_as_multi_safe { threshold: 2, other_signatories: vec![second.account.clone()], maybe_timepoint: None, call_hash: hash, max_weight, }, ))) .unwrap() ); let pending = pallet_multisig::Multisigs::::get(&composite, hash).unwrap(); assert_eq!( pending.approvals.as_slice(), core::slice::from_ref(&first.account) ); assert_eq!(Balances::free_balance(&destination), TaoBalance::new(0)); assert_eq!(Balances::reserved_balance(&first.account), pending.deposit); assert_ok!( Executive::apply_extrinsic(second.sign(RuntimeCall::Multisig( pallet_multisig::Call::as_multi_safe { threshold: 2, other_signatories: vec![first.account.clone()], maybe_timepoint: Some(pending.when), call: Box::new(inner), max_weight, }, ))) .unwrap() ); assert_eq!( Balances::free_balance(&destination), TaoBalance::new(1_000_000_000) ); assert!(!pallet_multisig::Multisigs::::contains_key( &composite, hash )); for account in [&first.account, &second.account, &composite] { assert_registrationless(account); } }); } #[test] fn safe_ms_derivative_and_pure_proxy_use_full_width_authority() { ext().execute_with(|| { setup(); let wallet = Wallet::new(124); fund(&wallet.account); let child = Utility::safe_derivative_account_id(wallet.account.clone(), 7).unwrap(); assert!(mldsa::rejects_legacy_public_keys(child.as_ref())); fund(&child); let destination = AccountId::new([125; 32]); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Utility( pallet_utility::Call::as_safe_derivative { index: 7, call: Box::new(transfer(&destination, 1_000_000_000)), }, ))) .unwrap() ); assert_eq!( Balances::free_balance(&destination), TaoBalance::new(1_000_000_000) ); assert_registrationless(&child); let height = System::block_number(); let ext_index = System::extrinsic_index().unwrap_or_default(); let pure = Proxy::safe_pure_account( &wallet.account, &ProxyType::Any, 8, Some((height, ext_index)), ) .unwrap(); assert!(mldsa::rejects_legacy_public_keys(pure.as_ref())); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Proxy( pallet_proxy::Call::create_safe_pure { proxy_type: ProxyType::Any, delay: 0, index: 8, }, ))) .unwrap() ); let (delegates, deposit) = Proxy::proxies(pure.clone()); assert_eq!(delegates[0].delegate, wallet.account); assert_eq!(Balances::reserved_balance(&wallet.account), deposit); assert!(!DerivedAuthorities::::contains_key(&pure)); assert!(!EvmAliases::::contains_key( HashedAccounts::evm_alias(&pure) )); fund(&pure); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Proxy( pallet_proxy::Call::proxy { real: pure.clone().into(), force_proxy_type: Some(ProxyType::Any), call: Box::new(transfer(&destination, 1_000_000_000)), }, ))) .unwrap() ); assert_eq!( Balances::free_balance(&destination), TaoBalance::new(2_000_000_000) ); // Move funds before killing the pure, just as for the legacy lifecycle. assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Proxy( pallet_proxy::Call::proxy { real: pure.clone().into(), force_proxy_type: Some(ProxyType::Any), call: Box::new(RuntimeCall::Balances(BalancesCall::transfer_all { dest: wallet.account.clone().into(), keep_alive: false, })), }, ))) .unwrap() ); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::Proxy( pallet_proxy::Call::proxy { real: pure.clone().into(), force_proxy_type: Some(ProxyType::Any), call: Box::new(RuntimeCall::Proxy(pallet_proxy::Call::kill_safe_pure { spawner: wallet.account.clone().into(), proxy_type: ProxyType::Any, index: 8, height, ext_index, })), }, ))) .unwrap() ); assert!(Proxy::proxies(pure.clone()).0.is_empty()); assert_registrationless(&wallet.account); assert_registrationless(&pure); }); } #[test] fn native_evm_keeps_ordinary_separate_backing_balance_and_nonce() { use pallet_evm::{AddressMapping, FeeCalculator}; use sp_core::{H160, U256}; ext().execute_with(|| { setup(); let wallet = Wallet::new(98); fund(&wallet.account); let source = H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&wallet.account)[..20]); let backing = evm_origin::HashedAddressMapping::into_account_id(source); assert_eq!( backing, pallet_evm::HashedAddressMapping::::into_account_id( source ) ); assert_ne!(backing, wallet.account); // Ordinary native funding of the historical EVM backing address. assert_ok!( Executive::apply_extrinsic(wallet.sign(transfer(&backing, 10_000_000_000))).unwrap() ); assert_eq!(System::account_nonce(&wallet.account), 1); assert_eq!(EVM::account_basic(&source).0.nonce, U256::zero()); let target = H160::repeat_byte(0x42); let destination = evm_origin::HashedAddressMapping::into_account_id(target); let before = Balances::free_balance(&backing); pallet_evm::DisableWhitelistCheck::::put(true); assert_ok!( Executive::apply_extrinsic(wallet.sign(RuntimeCall::EVM(pallet_evm::Call::call { source, target, input: vec![], value: U256::from(1_000_000_000_u64) * U256::from(EVM_TO_SUBSTRATE_DECIMALS), gas_limit: 100_000, max_fee_per_gas: ::FeeCalculator::min_gas_price().0, max_priority_fee_per_gas: None, nonce: Some(U256::zero()), access_list: vec![], authorization_list: vec![], },))) .unwrap() ); assert_eq!( Balances::free_balance(destination), TaoBalance::new(1_000_000_000) ); assert!(Balances::free_balance(&backing) < before - TaoBalance::new(1_000_000_000)); assert_eq!(System::account_nonce(&wallet.account), 2); assert_eq!(EVM::account_basic(&source).0.nonce, U256::one()); assert!(evm_origin::HashedAddressMapping::is_ethereum_authority_allowed(source)); assert_registrationless(&wallet.account); }); } #[test] fn rotating_profile_still_requires_registration_and_keeps_generation_state() { with_profiles(|| { setup(); let descriptor = descriptor(99); let account = AccountId::new(mldsa::account_id(&descriptor)); fund(&account); let before = Balances::free_balance(&account); let unregistered = sign_with_extra(99, 0, call(), extra(0)); assert!(Executive::apply_extrinsic(unregistered).is_err()); assert_eq!(Balances::free_balance(&account), before); assert_eq!(System::account_nonce(&account), 0); let account = register(99); let alias = HashedAccounts::evm_alias(&account); assert_eq!(EvmAliases::::get(alias), Some(account.clone())); assert_ok!(Executive::apply_extrinsic(sign(99, call())).unwrap()); assert_eq!( MlDsaAccounts::::get(&account).unwrap().generation, 1 ); assert_eq!(System::account_nonce(&account), 2); assert_eq!(EvmAliases::::get(alias), Some(account.clone())); assert!(HashedAccounts::is_protected(&account)); }); }