code/runtime/src/hashed_tests.rs
#![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(_)))
);
});
}
}