//! Exercise native EVM calls through the full signed extension pipeline. use super::*; use frame_support::{ assert_ok, dispatch::Pays, storage::{TransactionOutcome, with_transaction_unchecked}, }; use pallet_evm::{AddressMapping, FeeCalculator, runner::Runner}; use sp_core::{H160, U256, ed25519}; #[derive(Clone, Copy, Debug)] enum Mode { Sr, Ed, HashedSr, HashedEd, MlDsa, FixedMlDsa, Falcon(u8), } const MODES: [Mode; 5] = [ Mode::Sr, Mode::Ed, Mode::HashedSr, Mode::HashedEd, Mode::MlDsa, ]; struct Wallet { mode: Mode, account: AccountId, } impl Wallet { fn new(mode: Mode) -> Self { let account = match mode { Mode::MlDsa => super::mldsa::register(62), Mode::FixedMlDsa => super::mldsa::stateless::Wallet::new(62).account, Mode::Falcon(profile) => { let wallet = super::falcon::Wallet::new(62, profile); wallet.prepare(); wallet.account } Mode::Sr => AccountId::from(pair(10).public()), Mode::Ed => AccountId::new(ed25519::Pair::from_seed(&[10; 32]).public().0), _ => composable_accounts::register(Descriptor { version: 1, scheme: Self::scheme(mode), initial_commitment: Self::commitment(mode, 0), }), }; System::set_block_number(1); hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0)); let _ = Balances::make_free_balance_be(&account, TaoBalance::new(1_000_000_000_000)); let source = H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&account)[..20]); let backing = evm_origin::HashedAddressMapping::into_account_id(source); let _ = Balances::make_free_balance_be(&backing, TaoBalance::new(1_000_000_000_000)); pallet_evm::DisableWhitelistCheck::::put(true); Self { mode, account } } fn scheme(mode: Mode) -> Scheme { match mode { Mode::Ed | Mode::HashedEd => Scheme::Ed25519, _ => Scheme::Sr25519, } } fn public(mode: Mode, generation: u8) -> Vec { let seed = [10 + generation; 32]; match Self::scheme(mode) { Scheme::Sr25519 => sr25519::Pair::from_seed(&seed).public().0.to_vec(), Scheme::Ed25519 => ed25519::Pair::from_seed(&seed).public().0.to_vec(), } } fn commitment(mode: Mode, generation: u8) -> [u8; 32] { let public = Self::public(mode, generation); subtensor_hashed::key_commitment( Self::scheme(mode), <&[u8; 32]>::try_from(public.as_slice()).unwrap(), ) } fn source(&self) -> H160 { H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&self.account)[..20]) } fn sign(&self, call: RuntimeCall) -> UncheckedExtrinsic { if matches!(self.mode, Mode::MlDsa) { return super::mldsa::sign(62, call); } if matches!(self.mode, Mode::FixedMlDsa) { return super::mldsa::stateless::Wallet::new(62).sign(call); } if let Mode::Falcon(profile) = self.mode { return super::falcon::Wallet::new(62, profile).sign(call); } let extra = extra(System::account_nonce(&self.account)); if matches!(self.mode, Mode::Sr | Mode::Ed) { let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap(); let signature = if matches!(self.mode, Mode::Sr) { Signature::Sr25519(payload.using_encoded(|m| pair(10).sign(m))) } else { Signature::Ed25519( payload.using_encoded(|m| ed25519::Pair::from_seed(&[10; 32]).sign(m)), ) }; return UncheckedExtrinsic::new_signed( call, self.account.clone().into(), signature, extra, ); } let generation = HashedAccounts::accounts(&self.account).unwrap().generation; let key_generation = u8::try_from(generation).unwrap(); let next_commitment = Self::commitment(self.mode, key_generation + 1); let scheme = Self::scheme(self.mode); let address = subtensor_hashed::SignerAddress { version: 1, account: *self.account.as_ref(), scheme, generation, next_commitment, public_key: Self::public(self.mode, key_generation).try_into().unwrap(), }; let payload = hashed_extrinsic::signing_hash(&address, &call, &extra).unwrap(); let seed = [10 + key_generation; 32]; let signature = match scheme { Scheme::Sr25519 => Signature::Sr25519(sr25519::Pair::from_seed(&seed).sign(&payload)), Scheme::Ed25519 => Signature::Ed25519(ed25519::Pair::from_seed(&seed).sign(&payload)), }; UncheckedExtrinsic::new_signed(call, Address::Raw(address.encode()), signature, extra) } } fn gas_price() -> U256 { ::FeeCalculator::min_gas_price().0 } fn evm_call(wallet: &Wallet, nonce: Option) -> RuntimeCall { RuntimeCall::EVM(pallet_evm::Call::call { source: wallet.source(), target: H160::repeat_byte(0x42), input: vec![], value: U256::zero(), gas_limit: 21_000, max_fee_per_gas: gas_price(), max_priority_fee_per_gas: None, nonce, access_list: vec![], authorization_list: vec![], }) } fn simulate_call(wallet: &Wallet, nonce: Option) { let info = with_transaction_unchecked(|| { TransactionOutcome::Rollback(hashed_evm_runner::HashedEvmRunner::call( wallet.source(), H160::repeat_byte(0x42), vec![], U256::zero(), 21_000, Some(gas_price()), None, nonce, vec![], vec![], false, true, None, None, ::config(), )) }) .unwrap(); assert!(info.exit_reason.is_succeed()); assert_eq!(info.used_gas.standard, U256::from(21_000)); assert_eq!(info.used_gas.effective, U256::from(21_000)); } fn association_call( account: &AccountId, evm_pair: &sp_core::ecdsa::Pair, netuid: subtensor_runtime_common::NetUid, block_number: u64, ) -> (H160, RuntimeCall) { let message = [ account.encode(), sp_core::keccak_256(&block_number.encode()).to_vec(), ] .concat(); let digest = sp_core::keccak_256( &[ b"\x19Ethereum Signed Message:\n64".as_slice(), message.as_slice(), ] .concat(), ); let mut signature = evm_pair.sign_prehashed(&digest); let public = sp_io::crypto::secp256k1_ecdsa_recover(&signature.0, &digest) .unwrap_or_else(|_| panic!("valid ECDSA association proof must recover")); let evm_key = H160::from_slice(&sp_core::keccak_256(&public)[12..]); signature.0[64] += 27; ( evm_key, RuntimeCall::SubtensorModule(pallet_subtensor::Call::associate_evm_key { netuid, evm_key, block_number, signature, }), ) } #[test] #[cfg(not(feature = "fast-runtime"))] fn neuron_evm_association_honors_production_cooldown_for_every_wallet_mode() { use pallet_subtensor::{AssociatedEvmAddress, AssociatedUidsByEvmAddress}; use subtensor_runtime_common::{CustomTransactionError, NetUid}; assert_eq!(EvmKeyAssociateRateLimit::get(), 7_200); for mode in MODES.into_iter().chain([ Mode::FixedMlDsa, Mode::Falcon(6), Mode::Falcon(7), Mode::Falcon(8), Mode::Falcon(9), ]) { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let netuid = NetUid::from(1); let uid = 0u16; pallet_subtensor::NetworksAdded::::insert(netuid, true); // Self ownership preserves each protected wallet's authority floor. assert_ok!(SubtensorModule::create_account_if_non_existent( &wallet.account, &wallet.account, )); pallet_subtensor::Uids::::insert(netuid, &wallet.account, uid); pallet_subtensor::Keys::::insert(netuid, uid, &wallet.account); let evm_pair = sp_core::ecdsa::Pair::from_seed(&[73; 32]); let make_call = |block_number: u64| { association_call(&wallet.account, &evm_pair, netuid, block_number) }; // Pool validation prepares the next block, so a parent at 7,198 // must reject inclusion at 7,199. Roll its temporary header back. System::set_block_number(7_198); let (evm_key, too_early) = make_call(7_199); let expected_early_error = sp_runtime::transaction_validity::TransactionValidityError::from( CustomTransactionError::EvmKeyAssociateRateLimitExceeded, ); assert_eq!( with_transaction_unchecked(|| TransactionOutcome::Rollback( Executive::validate_transaction( sp_runtime::transaction_validity::TransactionSource::External, wallet.sign(too_early.clone()), System::parent_hash(), ), )), Err(expected_early_error) ); System::set_block_number(7_199); let before = System::account_nonce(&wallet.account); let result = with_transaction_unchecked(|| { TransactionOutcome::Rollback(Executive::apply_extrinsic(wallet.sign(too_early))) }); assert_eq!( result, Err(expected_early_error), "{mode:?}: premature association accepted" ); assert_eq!(System::account_nonce(&wallet.account), before); assert!(AssociatedEvmAddress::::get(netuid, uid).is_none()); assert!(AssociatedUidsByEvmAddress::::get(netuid, evm_key).is_empty()); // A parent at 7,199 admits the first valid inclusion at 7,200. let (evm_key, call) = make_call(7_200); assert_ok!(with_transaction_unchecked(|| TransactionOutcome::Rollback( Executive::validate_transaction( sp_runtime::transaction_validity::TransactionSource::External, wallet.sign(call.clone()), System::parent_hash(), ), ))); System::set_block_number(7_200); assert_ok!(Executive::apply_extrinsic(wallet.sign(call)).unwrap()); assert_eq!( AssociatedEvmAddress::::get(netuid, uid), Some((evm_key, 7_200)) ); assert_eq!( AssociatedUidsByEvmAddress::::get(netuid, evm_key).as_slice(), &[(uid, 7_200)] ); assert_eq!(System::account_nonce(&wallet.account), before + 1); }); } } #[test] fn native_evm_deposit_resolves_the_mapping_at_execution_for_every_wallet() { for mode in MODES.into_iter().chain([ Mode::FixedMlDsa, Mode::Falcon(6), Mode::Falcon(7), Mode::Falcon(8), Mode::Falcon(9), ]) { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let target_descriptor = super::mldsa::descriptor(119); let target = AccountId::new(subtensor_hashed::mldsa::account_id(&target_descriptor)); let address = H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&target)[..20]); let obsolete = evm_origin::HashedAddressMapping::into_account_id(address); assert_ne!(obsolete, target); let deposit = wallet.sign(RuntimeCall::EVM(pallet_evm::Call::deposit { address, value: Some(TaoBalance::new(1_000_000_000)), })); // The sender has already signed when a new alias becomes active. assert_eq!(super::mldsa::register(119), target); let before = Balances::free_balance(&target); assert_ok!(Executive::apply_extrinsic(deposit).unwrap()); assert_eq!( Balances::free_balance(&target), before + TaoBalance::new(1_000_000_000) ); assert_eq!(Balances::free_balance(&obsolete), TaoBalance::new(0)); }); } } #[test] fn native_evm_deposit_all_preserves_frozen_balance_and_batch_rollback() { use frame_support::traits::{LockableCurrency, WithdrawReasons}; super::mldsa::with_profiles(|| { let wallet = Wallet::new(Mode::FixedMlDsa); let address = H160::from_low_u64_be(119); let destination = evm_origin::HashedAddressMapping::into_account_id(address); let source_before = Balances::free_balance(&wallet.account); let frozen = TaoBalance::new(2_000_000_000); Balances::set_lock( *b"evm-test", &wallet.account, frozen, WithdrawReasons::all(), ); assert_ok!(EVM::deposit( RuntimeOrigin::signed(wallet.account.clone()), address, None )); assert_eq!(Balances::free_balance(&wallet.account), frozen); assert_eq!(Balances::free_balance(&destination), source_before - frozen); Balances::remove_lock(*b"evm-test", &wallet.account); let before = Balances::free_balance(&destination); let failing = RuntimeCall::Utility(pallet_utility::Call::batch_all { calls: vec![ RuntimeCall::EVM(pallet_evm::Call::deposit { address, value: Some(TaoBalance::new(1_000_000)), }), RuntimeCall::Balances(BalancesCall::transfer_keep_alive { dest: destination.clone().into(), value: TaoBalance::new(u64::MAX), }), ], }); assert!( Executive::apply_extrinsic(wallet.sign(failing)) .unwrap() .is_err() ); assert_eq!(Balances::free_balance(&destination), before); assert_eq!( Balances::reserved_balance(&wallet.account), TaoBalance::new(0) ); assert!(!pallet_hashed_accounts::MlDsaAccounts::::contains_key(&wallet.account)); }); } #[test] fn fixed_ms_association_keeps_evm_funds_and_signing_authority_separate() { use fp_self_contained::SelfContainedCall; use pallet_hashed_accounts::{EvmAliases, MlDsaAccounts}; use pallet_subtensor::{AssociatedEvmAddress, AssociatedUidsByEvmAddress}; use subtensor_runtime_common::NetUid; super::mldsa::with_profiles(|| { let wallet = Wallet::new(Mode::FixedMlDsa); let netuid = NetUid::from(1); pallet_subtensor::NetworksAdded::::insert(netuid, true); assert_ok!(SubtensorModule::create_account_if_non_existent( &wallet.account, &wallet.account, )); pallet_subtensor::Uids::::insert(netuid, &wallet.account, 0u16); pallet_subtensor::Keys::::insert(netuid, 0u16, &wallet.account); System::set_block_number(7_200); let evm_pair = sp_core::ecdsa::Pair::from_seed(&[74; 32]); let (evm_key, association) = association_call(&wallet.account, &evm_pair, netuid, 7_200); assert_ne!(evm_key, wallet.source()); assert_ok!(Executive::apply_extrinsic(wallet.sign(association)).unwrap()); assert_eq!( AssociatedEvmAddress::::get(netuid, 0u16), Some((evm_key, 7_200)) ); assert_eq!( AssociatedUidsByEvmAddress::::get(netuid, evm_key).as_slice(), &[(0u16, 7_200)] ); let backing = evm_origin::HashedAddressMapping::into_account_id(evm_key); assert_ne!(backing, wallet.account); assert!(EvmAliases::::get(evm_key.0).is_none()); assert!(MlDsaAccounts::::get(&wallet.account).is_none()); // Attribution must not turn the associated ECDSA key into an alternative // signer for the full native account, even with a valid ECDSA signature. let native_before = System::account(&wallet.account); let call = super::call(); let extra = extra(native_before.nonce); let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap(); let signature = Signature::Ecdsa(payload.using_encoded(|m| evm_pair.sign(m))); let forged = UncheckedExtrinsic::new_signed(call, wallet.account.clone().into(), signature, extra); assert_eq!( Executive::apply_extrinsic(forged), Err(InvalidTransaction::BadProof.into()) ); assert_eq!(System::account(&wallet.account), native_before); // Native transfers fund the conventional EVM backing account. The // association neither shares balances nor creates a protected alias. let funding = RuntimeCall::EVM(pallet_evm::Call::deposit { address: evm_key, value: Some(TaoBalance::new(20_000_000_000)), }); assert_ok!(Executive::apply_extrinsic(wallet.sign(funding)).unwrap()); assert_eq!( Balances::free_balance(&backing), TaoBalance::new(20_000_000_000) ); let native_before_return = System::account(&wallet.account); let evm_nonce = EVM::account_basic(&evm_key).0.nonce; let mut input = sp_core::keccak_256(b"transfer(bytes32)")[..4].to_vec(); input.extend_from_slice(AsRef::<[u8; 32]>::as_ref(&wallet.account)); let destination = H160::from_low_u64_be(2048); let message = ethereum::LegacyTransactionMessage { nonce: evm_nonce, gas_price: gas_price(), gas_limit: U256::from(500_000), action: ethereum::TransactionAction::Call(destination), value: U256::from(1_000_000_000_000_000_000u64), input, chain_id: None, }; let signature = evm_pair.sign_prehashed(message.hash().as_fixed_bytes()); let transaction = ethereum::LegacyTransaction { nonce: message.nonce, gas_price: message.gas_price, gas_limit: message.gas_limit, action: message.action, value: message.value, input: message.input, signature: ethereum::legacy::TransactionSignature::new( >::from(signature.0[64]) + 27, H256::from_slice(&signature.0[..32]), H256::from_slice(&signature.0[32..64]), ) .unwrap(), }; let call = RuntimeCall::Ethereum(pallet_ethereum::Call::transact { transaction: ethereum::TransactionV3::Legacy(transaction), }); assert_eq!(call.check_self_contained().unwrap().unwrap(), evm_key); assert_ok!(Executive::apply_extrinsic(UncheckedExtrinsic::new_bare(call)).unwrap()); assert!(System::events().iter().any(|record| matches!( record.event, RuntimeEvent::Ethereum(pallet_ethereum::Event::Executed { from, to, ref exit_reason, .. }) if from == evm_key && to == destination && exit_reason.is_succeed() ))); assert_eq!( Balances::free_balance(&wallet.account), native_before_return.data.free + TaoBalance::new(1_000_000_000) ); assert_eq!( System::account_nonce(&wallet.account), native_before_return.nonce ); assert_eq!( EVM::account_basic(&evm_key).0.nonce, evm_nonce + U256::one() ); assert!(Balances::free_balance(&backing) < TaoBalance::new(19_000_000_000)); assert_eq!( Balances::reserved_balance(&wallet.account), TaoBalance::ZERO ); assert!(MlDsaAccounts::::get(&wallet.account).is_none()); assert!(EvmAliases::::get(evm_key.0).is_none()); assert!(EvmAliases::::get(wallet.source().0).is_none()); }); } // PROTECTED_REGRESSION: queried nonce executes and advances independently for every wallet mode; changes require explicit human approval. #[test] fn queried_nonce_executes_and_advances_independently_for_every_wallet_mode() { for mode in MODES.into_iter().chain([ Mode::FixedMlDsa, Mode::Falcon(6), Mode::Falcon(7), Mode::Falcon(8), Mode::Falcon(9), ]) { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let native = System::account_nonce(&wallet.account); let nonce = EVM::account_basic(&wallet.source()).0.nonce; simulate_call(&wallet, Some(nonce)); assert_ok!( Executive::apply_extrinsic(wallet.sign(evm_call(&wallet, Some(nonce)))).unwrap() ); assert_eq!( System::account_nonce(&wallet.account), native + 1, "{mode:?}" ); assert_eq!( EVM::account_basic(&wallet.source()).0.nonce, nonce + U256::one(), "{mode:?}" ); }); } } #[test] fn create_prediction_matches_dispatch_with_and_without_explicit_nonce() { for mode in MODES { for explicit in [false, true] { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let nonce = explicit.then(|| EVM::account_basic(&wallet.source()).0.nonce); let init = hex::decode("6001600c60003960016000f300").unwrap(); let simulated = with_transaction_unchecked(|| { TransactionOutcome::Rollback(hashed_evm_runner::HashedEvmRunner::create( wallet.source(), init.clone(), U256::zero(), 200_000, Some(gas_price()), None, nonce, vec![], vec![], true, vec![], false, true, None, None, ::config(), )) }) .unwrap(); assert!(simulated.exit_reason.is_succeed()); let call = RuntimeCall::EVM(pallet_evm::Call::create { source: wallet.source(), init, value: U256::zero(), gas_limit: 200_000, max_fee_per_gas: gas_price(), max_priority_fee_per_gas: None, nonce, access_list: vec![], authorization_list: vec![], }); assert_ok!(Executive::apply_extrinsic(wallet.sign(call)).unwrap()); let created = System::events() .iter() .rev() .find_map(|event| match event.event { RuntimeEvent::EVM(pallet_evm::Event::Created { address }) => Some(address), _ => None, }) .unwrap(); assert_eq!( created, simulated.value, "{mode:?}, explicit nonce={explicit}" ); }); } } } #[test] fn authorization_fee_is_not_refunded_when_gas_pays_for_execution() { super::mldsa::with_profiles(|| { for mode in MODES { let mut fees = Vec::new(); let mut charges = Vec::new(); for factor in [1, 100] { ext().execute_with(|| { let wallet = Wallet::new(mode); hashed_auth::TestVerificationWeight::set(Weight::from_parts( 10_000_000 * factor, 0, )); let xt = wallet.sign(evm_call(&wallet, None)); let mut envelope = xt.get_dispatch_info(); envelope.call_weight = Weight::zero(); let expected = TransactionPayment::compute_fee( u32::try_from(xt.encoded_size()).unwrap(), &envelope, TaoBalance::ZERO, ); // Exercise the wire decoder as well as the local constructor. let decoded = UncheckedExtrinsic::decode(&mut xt.encode().as_slice()).unwrap(); assert_eq!(decoded.encode(), xt.encode()); let before = Balances::free_balance(&wallet.account); assert_ok!(Executive::apply_extrinsic(decoded).unwrap()); charges.push(before - Balances::free_balance(&wallet.account)); let fee = System::events() .iter() .rev() .find_map(|event| match event.event { RuntimeEvent::TransactionPayment( pallet_transaction_payment::Event::TransactionFeePaid { actual_fee, .. }, ) => Some(actual_fee), _ => None, }) .unwrap(); if !matches!(mode, Mode::Sr | Mode::Ed) { assert_eq!(fee, expected, "{mode:?}"); } fees.push(fee); }); } if matches!(mode, Mode::Sr | Mode::Ed) { assert_eq!(fees, vec![TaoBalance::ZERO; 2]); } else { assert!( fees[1] > fees[0] && fees[0] > TaoBalance::ZERO, "{mode:?}: authorization fees {fees:?}" ); // Gas is unchanged: increasing authorization work must increase // the real balance debit by precisely the added native fee. assert!(charges[0] > fees[0]); assert_eq!(charges[1] - charges[0], fees[1] - fees[0], "{mode:?}"); } } }); } // PROTECTED_REGRESSION: reverted and invalid evm calls keep authorization fees and reject replays; changes require explicit human approval. #[test] fn reverted_and_invalid_evm_calls_keep_authorization_fees_and_reject_replays() { for mode in MODES { for invalid in [false, true] { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let nonce = EVM::account_basic(&wallet.source()).0.nonce; // REVERT(0, 0): a completed EVM execution still pays gas. pallet_evm::AccountCodes::::insert( H160::repeat_byte(0x42), hex::decode("60006000fd").unwrap(), ); let mut call = evm_call( &wallet, Some(nonce + U256::from(>::from(invalid))), ); // This case executes REVERT bytecode, so it needs gas beyond // the plain-transfer intrinsic limit used by the helper. if let RuntimeCall::EVM(pallet_evm::Call::call { gas_limit, .. }) = &mut call { *gas_limit = 100_000; } let xt = wallet.sign(call); let replay = xt.clone(); let native = System::account_nonce(&wallet.account); let generation = HashedAccounts::accounts(&wallet.account).map(|a| a.generation); let result = Executive::apply_extrinsic(xt).unwrap(); if invalid { assert_eq!( result, Err(pallet_evm::Error::::InvalidNonce.into()) ); } else { assert_ok!(result); assert!(System::events().iter().any(|e| matches!( e.event, RuntimeEvent::EVM(pallet_evm::Event::ExecutedFailed { .. }) ))); } assert_eq!(System::account_nonce(&wallet.account), native + 1); assert_eq!( EVM::account_basic(&wallet.source()).0.nonce, nonce + U256::from(>::from(!invalid)) ); if let Some(generation) = generation { assert_eq!( HashedAccounts::accounts(&wallet.account) .unwrap() .generation, generation + 1 ); } let paid = System::events() .iter() .rev() .find_map(|e| match e.event { RuntimeEvent::TransactionPayment( pallet_transaction_payment::Event::TransactionFeePaid { actual_fee, .. }, ) => Some(actual_fee), _ => None, }) .unwrap(); assert_eq!( paid > TaoBalance::ZERO, invalid || !matches!(mode, Mode::Sr | Mode::Ed) ); assert!(Executive::apply_extrinsic(replay).is_err()); }); } } } // PROTECTED_REGRESSION: alias nonce survives balance reaping and native transactions when disabled; changes require explicit human approval. #[test] fn alias_nonce_survives_balance_reaping_and_native_transactions_when_disabled() { for mode in [Mode::HashedSr, Mode::HashedEd, Mode::MlDsa] { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); assert_eq!(EVM::account_basic(&wallet.source()).0.nonce, U256::one()); assert_ok!( Executive::apply_extrinsic(wallet.sign(evm_call(&wallet, Some(U256::one())))) .unwrap() ); let backing = evm_origin::HashedAddressMapping::nonce_account_id(wallet.source()).unwrap(); assert_ne!(backing, wallet.account); let _ = Balances::make_free_balance_be(&backing, TaoBalance::new(1_000_000_000)); let _ = Balances::make_free_balance_be(&backing, TaoBalance::ZERO); assert_eq!(System::account_nonce(&backing), 2); assert_eq!(System::account(&backing).sufficients, 1); pallet_admin_utils::HashedAccountsEnabled::::put(false); assert_eq!(EVM::account_basic(&wallet.source()).0.nonce, U256::from(2)); simulate_call(&wallet, Some(U256::from(2))); assert!(Executive::apply_extrinsic(wallet.sign(call())).is_err()); pallet_admin_utils::HashedAccountsEnabled::::put(true); assert_ok!(Executive::apply_extrinsic(wallet.sign(call())).unwrap()); assert_eq!(EVM::account_basic(&wallet.source()).0.nonce, U256::from(2)); simulate_call(&wallet, Some(U256::from(2))); assert_ok!( Executive::apply_extrinsic(wallet.sign(evm_call(&wallet, Some(U256::from(2))))) .unwrap() ); assert_eq!(EVM::account_basic(&wallet.source()).0.nonce, U256::from(3)); }); } } // PROTECTED_REGRESSION: protected alias rejects create2 collision before any native evm transaction; changes require explicit human approval. #[test] fn protected_alias_rejects_create2_collision_before_any_native_evm_transaction() { ext().execute_with(|| { System::set_block_number(1); pallet_evm::DisableWhitelistCheck::::put(true); let source = H160::repeat_byte(0x42); let backing = evm_origin::HashedAddressMapping::into_account_id(source); let _ = Balances::make_free_balance_be(&backing, TaoBalance::new(1_000_000_000_000)); let salt = H256::repeat_byte(0x17); let init = hex::decode("6001600c60003960016000f300").unwrap(); let mut preimage = vec![0xff]; preimage.extend_from_slice(source.as_bytes()); preimage.extend_from_slice(salt.as_bytes()); preimage.extend_from_slice(&sp_io::hashing::keccak_256(&init)); let expected = H160::from_slice(&sp_io::hashing::keccak_256(&preimage)[12..]); let create = || { hashed_evm_runner::HashedEvmRunner::create2( source, init.clone(), salt, U256::zero(), 200_000, Some(gas_price()), None, None, vec![], vec![], true, vec![], false, true, None, None, ::config(), ) }; let before = with_transaction_unchecked(|| TransactionOutcome::Rollback(create())).unwrap(); assert!(before.exit_reason.is_succeed()); assert_eq!(before.value, expected); // Construct the collision directly: discovering one cryptographically // is outside this regression. The alias must remain creation-ineligible // regardless of the destination derivation or the owner's balance. let mut owner_bytes = [0x73; 32]; owner_bytes[..20].copy_from_slice(expected.as_bytes()); let owner = AccountId::new(owner_bytes); assert_ok!(evm_origin::prepare_hashed_alias(&owner)); pallet_hashed_accounts::EvmAliases::::insert(expected.0, &owner); assert_eq!(EVM::account_basic(&expected).0.nonce, U256::one()); assert_eq!(System::account_nonce(&owner), 0); let after = create().unwrap(); assert_eq!( after.exit_reason, pallet_evm::ExitReason::Error(pallet_evm::ExitError::CreateCollision) ); assert!(!pallet_evm::AccountCodes::::contains_key(expected)); assert_eq!(EVM::account_basic(&expected).0.nonce, U256::one()); }); } #[test] fn create2_prediction_and_envelope_fee_match_dispatch() { for mode in MODES { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let nonce = Some(EVM::account_basic(&wallet.source()).0.nonce); let init = hex::decode("6001600c60003960016000f300").unwrap(); let salt = H256::repeat_byte(0x17); let simulated = with_transaction_unchecked(|| { TransactionOutcome::Rollback(hashed_evm_runner::HashedEvmRunner::create2( wallet.source(), init.clone(), salt, U256::zero(), 200_000, Some(gas_price()), None, nonce, vec![], vec![], true, vec![], false, true, None, None, ::config(), )) }) .unwrap(); assert!(simulated.exit_reason.is_succeed()); let call = RuntimeCall::EVM(pallet_evm::Call::create2 { source: wallet.source(), init, salt, value: U256::zero(), gas_limit: 200_000, max_fee_per_gas: gas_price(), max_priority_fee_per_gas: None, nonce, access_list: vec![], authorization_list: vec![], }); let xt = wallet.sign(call); let mut envelope = xt.get_dispatch_info(); envelope.call_weight = Weight::zero(); envelope.pays_fee = Pays::Yes; let expected = TransactionPayment::compute_fee( u32::try_from(xt.encoded_size()).unwrap(), &envelope, TaoBalance::ZERO, ); assert_ok!(Executive::apply_extrinsic(xt).unwrap()); let events = System::events(); let created = events .iter() .rev() .find_map(|event| match event.event { RuntimeEvent::EVM(pallet_evm::Event::Created { address }) => Some(address), _ => None, }) .unwrap(); assert_eq!(created, simulated.value, "{mode:?}"); let fee = events .iter() .rev() .find_map(|event| match event.event { RuntimeEvent::TransactionPayment( pallet_transaction_payment::Event::TransactionFeePaid { actual_fee, .. }, ) => Some(actual_fee), _ => None, }) .unwrap(); assert_eq!( fee, if matches!(mode, Mode::Sr | Mode::Ed) { TaoBalance::ZERO } else { expected } ); }); } } #[test] fn every_wallet_mode_executes_mixed_multisig_and_proxy_transfers() { for mode in MODES { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let other = pair(24); let other_id = AccountId::from(other.public()); let _ = Balances::make_free_balance_be(&other_id, TaoBalance::new(100_000_000_000)); let mut members = vec![wallet.account.clone(), other_id.clone()]; members.sort(); let multisig = Multisig::multi_account_id(&members, 2); let _ = Balances::make_free_balance_be(&multisig, TaoBalance::new(10_000_000_000)); let dest = AccountId::new([97; 32]); let transfer = RuntimeCall::Balances(BalancesCall::transfer_keep_alive { dest: dest.clone().into(), value: TaoBalance::new(1_000_000_000), }); let hash = sp_io::hashing::blake2_256(&transfer.encode()); let weight = transfer.get_dispatch_info().total_weight(); let approval = RuntimeCall::Multisig(pallet_multisig::Call::approve_as_multi { threshold: 2, other_signatories: vec![other_id], maybe_timepoint: None, call_hash: hash, max_weight: weight, }); assert_ok!(Executive::apply_extrinsic(wallet.sign(approval)).unwrap()); let pending = pallet_multisig::Multisigs::::get(&multisig, hash).unwrap(); assert_eq!(pending.depositor, wallet.account); let execute = RuntimeCall::Multisig(pallet_multisig::Call::as_multi { threshold: 2, other_signatories: vec![wallet.account.clone()], maybe_timepoint: Some(pending.when), call: Box::new(transfer.clone()), max_weight: weight, }); assert_ok!(Executive::apply_extrinsic(legacy(&other, execute)).unwrap()); assert_eq!( Balances::free_balance(&dest), TaoBalance::new(1_000_000_000) ); assert!(pallet_multisig::Multisigs::::get(&multisig, hash).is_none()); let delegate = Wallet::new(if matches!(mode, Mode::MlDsa) { Mode::MlDsa } else if matches!(mode, Mode::HashedEd) { Mode::HashedSr } else { Mode::HashedEd }); let delegate_account = if matches!(mode, Mode::MlDsa) { super::mldsa::register(63) } else { delegate.account.clone() }; let grant = RuntimeCall::Proxy(pallet_proxy::Call::add_proxy { delegate: delegate_account.into(), proxy_type: ProxyType::Any, delay: 0, }); assert_ok!(Executive::apply_extrinsic(wallet.sign(grant)).unwrap()); let proxy = RuntimeCall::Proxy(pallet_proxy::Call::proxy { real: wallet.account.clone().into(), force_proxy_type: Some(ProxyType::Any), call: Box::new(transfer), }); assert_ok!( Executive::apply_extrinsic(if matches!(mode, Mode::MlDsa) { super::mldsa::sign(63, proxy) } else { delegate.sign(proxy) }) .unwrap() ); assert_eq!( Balances::free_balance(&dest), TaoBalance::new(2_000_000_000) ); }); } } #[test] fn shield_recognizes_every_wallet_mode_without_rewriting_or_rotating_it() { for mode in MODES { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let mut wire = vec![0xAB; 16]; wire.extend_from_slice(&1088u16.to_le_bytes()); wire.extend_from_slice(&[0xCC; 1088]); wire.extend_from_slice(&[0xDD; 24]); wire.extend_from_slice(&[0xEE; 64]); let call = RuntimeCall::MevShield(pallet_shield::Call::submit_encrypted { ciphertext: wire.try_into().unwrap(), }); let xt = wallet.sign(call); let original = xt.encode(); let state = HashedAccounts::accounts(&wallet.account); let parsed = hashed_extrinsic::shielded_transaction(xt.clone()).unwrap(); assert_eq!(parsed.key_hash, [0xAB; 16]); assert_eq!(parsed.kem_ct, vec![0xCC; 1088]); assert_eq!(parsed.nonce, [0xDD; 24]); assert_eq!(parsed.aead_ct, vec![0xEE; 64]); assert_eq!(HashedAccounts::accounts(&wallet.account), state); assert_eq!(xt.encode(), original); assert!(hashed_extrinsic::shielded_transaction(wallet.sign(super::call())).is_none()); let mut invalid = xt; let UncheckedExtrinsic::Legacy(ref mut tx) = invalid; let sp_runtime::generic::Preamble::Signed(_, signature, _) = &mut tx.0.preamble else { unreachable!() }; match signature { Signature::Sr25519(sig) => sig.0[0] ^= 1, Signature::Ed25519(sig) => sig.0[0] ^= 1, _ => unreachable!(), } assert!(hashed_extrinsic::shielded_transaction(invalid.clone()).is_some()); assert!(invalid.check(&ChainContext::default()).is_err()); }); } } #[test] fn staking_fee_quotes_and_charges_preserve_account_wrapper_costs() { use crate::transaction_payment_wrapper::{ ChargeTransactionPaymentWrapper, FeeWeightDiscount, fee_dispatch_info, }; use sp_runtime::traits::DispatchTransaction; for mode in [Mode::HashedSr, Mode::HashedEd, Mode::MlDsa] { super::mldsa::with_profiles(|| { let wallet = Wallet::new(mode); let inner = RuntimeCall::SubtensorModule(pallet_subtensor::Call::remove_stake { hotkey: AccountId::new([98; 32]), netuid: 1.into(), amount_unstaked: subtensor_runtime_common::AlphaBalance::new(1_000_000), }); let tx = wallet.sign(inner.clone()); let info = tx.get_dispatch_info(); let len = u32::try_from(tx.encoded_size()).unwrap(); let discount = Runtime::fee_weight_discount(&inner, &inner.get_dispatch_info()); assert!(discount.ref_time() > 0); let expected = TransactionPayment::compute_fee( len, &fee_dispatch_info(&info, discount), TaoBalance::ZERO, ); let quote = staking_fee::query_info( sp_runtime::traits::ExtrinsicCall::call(&tx), &info, len, false, ); assert_eq!(quote.partial_fee, expected); let before = Balances::free_balance(&wallet.account); let result = ChargeTransactionPaymentWrapper::::new(TaoBalance::ZERO) .with_account_authorization() .test_run( RuntimeOrigin::signed(wallet.account.clone()), &inner, &info, len as usize, 0, |_| Err(sp_runtime::DispatchError::Other("failed inner call").into()), ); assert!(result.unwrap().is_err()); assert_eq!(before - Balances::free_balance(&wallet.account), expected); }); } }