code/runtime/src/hashed_native_evm_tests.rs

hashed_native_evm_tests.rs

1,082 lines · 45,796 bytes · d967d13588RawGitHub
//! 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::<Runtime>::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<u8> {
        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 {
    <Runtime as pallet_evm::Config>::FeeCalculator::min_gas_price().0
}
fn evm_call(wallet: &Wallet, nonce: Option<U256>) -> 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<U256>) {
    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,
            <Runtime as pallet_evm::Config>::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::<Runtime>::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::<Runtime>::insert(netuid, &wallet.account, uid);
            pallet_subtensor::Keys::<Runtime>::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::<Runtime>::get(netuid, uid).is_none());
            assert!(AssociatedUidsByEvmAddress::<Runtime>::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::<Runtime>::get(netuid, uid),
                Some((evm_key, 7_200))
            );
            assert_eq!(
                AssociatedUidsByEvmAddress::<Runtime>::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::<Runtime>::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::<Runtime>::insert(netuid, true);
        assert_ok!(SubtensorModule::create_account_if_non_existent(
            &wallet.account,
            &wallet.account,
        ));
        pallet_subtensor::Uids::<Runtime>::insert(netuid, &wallet.account, 0u16);
        pallet_subtensor::Keys::<Runtime>::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::<Runtime>::get(netuid, 0u16),
            Some((evm_key, 7_200))
        );
        assert_eq!(
            AssociatedUidsByEvmAddress::<Runtime>::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::<Runtime>::get(evm_key.0).is_none());
        assert!(MlDsaAccounts::<Runtime>::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(
                <u64 as From<u8>>::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::<Runtime>::get(&wallet.account).is_none());
        assert!(EvmAliases::<Runtime>::get(evm_key.0).is_none());
        assert!(EvmAliases::<Runtime>::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,
                        <Runtime as pallet_evm::Config>::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::<Runtime>::insert(
                    H160::repeat_byte(0x42),
                    hex::decode("60006000fd").unwrap(),
                );
                let mut call = evm_call(
                    &wallet,
                    Some(nonce + U256::from(<u8 as From<bool>>::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::<Runtime>::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(<u8 as From<bool>>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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,
                <Runtime as pallet_evm::Config>::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::<Runtime>::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::<Runtime>::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,
                    <Runtime as pallet_evm::Config>::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::<Runtime>::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::<Runtime>::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::<Runtime>::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);
        });
    }
}