code/runtime/src/mldsa_tests.rs

mldsa_tests.rs

1,104 lines · 44,864 bytes · d967d13588RawGitHub
//! ML-DSA authority enters the same Executive and pallet origin pipeline.
use super::*;
use frame_support::{assert_noop, assert_ok};
use pallet_hashed_accounts::{DerivedDescriptor, MlDsaAccounts};
use sp_runtime::{generic::Preamble, traits::TransactionExtension};
use subtensor_hashed::mldsa;

#[path = "mldsa_stateless_tests.rs"]
pub(super) mod stateless;

// Registered rotation and protected authority graphs use profile 4. Fixed
// profile 5 follows ordinary authority policy and is exercised by `stateless`.
pub(super) fn with_profiles(test: impl Fn()) {
    ext().execute_with(|| {
        assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            true
        ));
        test();
    });
}

pub(super) fn seed(identity: u8, generation: u64) -> [u8; 32] {
    let mut seed = [identity; 32];
    seed[..8].copy_from_slice(&generation.to_le_bytes());
    seed
}

pub(super) fn descriptor(identity: u8) -> mldsa::Descriptor {
    mldsa::Descriptor {
        version: mldsa::VERSION,
        algorithm: mldsa::ALGORITHM,
        initial_commitment: mldsa::commitment(
            &mldsa::public_key_from_seed(&seed(identity, 0)).unwrap(),
        ),
    }
}

pub(super) fn register(identity: u8) -> AccountId {
    System::set_block_number(1);
    assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
        RuntimeOrigin::root(),
        true
    ));
    hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0));
    let sponsor = AccountId::from(pair(70).public());
    let _ = Balances::make_free_balance_be(&sponsor, TaoBalance::new(1_000_000_000_000_000));
    let descriptor = descriptor(identity);
    let account = AccountId::new(mldsa::account_id(&descriptor));
    assert_ok!(HashedAccounts::register(
        RuntimeOrigin::signed(sponsor),
        descriptor.into()
    ));
    let _ = Balances::make_free_balance_be(&account, TaoBalance::new(1_000_000_000_000));
    account
}

pub(super) fn sign(identity: u8, call: RuntimeCall) -> UncheckedExtrinsic {
    let account = AccountId::new(mldsa::account_id(&descriptor(identity)));
    let generation = MlDsaAccounts::<Runtime>::get(&account).unwrap().generation;
    sign_with_extra(
        identity,
        generation,
        call,
        extra(System::account_nonce(account)),
    )
}

fn sign_with_extra(
    identity: u8,
    generation: u64,
    call: RuntimeCall,
    extra: TxExtension,
) -> UncheckedExtrinsic {
    let descriptor = descriptor(identity);
    let header = mldsa::Header {
        account: mldsa::account_id(&descriptor),
        version: 2,
        algorithm: descriptor.algorithm,
        generation,
        public_key: mldsa::public_key_from_seed(&seed(identity, generation)).unwrap(),
        next_commitment: mldsa::commitment(
            &mldsa::public_key_from_seed(&seed(identity, generation + 1)).unwrap(),
        ),
    };
    let payload = (&call, &extra, extra.implicit().unwrap()).encode();
    let address = header
        .sign(&seed(identity, generation), &payload, &[0; 32])
        .unwrap();
    UncheckedExtrinsic::new_signed(
        call,
        Address::Raw(address.encode()),
        Signature::Ed25519(sp_core::ed25519::Signature::from_raw(
            mldsa::CARRIER_SIGNATURE,
        )),
        extra,
    )
}

#[test]
fn protected_derivative_registration_uses_the_live_deposit_once() {
    with_profiles(|| {
        let parent = register(42);
        let mut reserved = Balances::reserved_balance(&parent);
        let mut previous_descriptor = None;
        for (index, value) in [
            (0, TaoBalance::new(200_000_000)),
            (1, TaoBalance::new(350_000_000)),
            (2, TaoBalance::new(0)),
        ] {
            assert_ok!(AdminUtils::sudo_set_hashed_account_registration_deposit(
                RuntimeOrigin::root(),
                value
            ));
            assert_eq!(Balances::reserved_balance(&parent), reserved);
            if let Some(descriptor) = previous_descriptor.take() {
                assert_ok!(HashedAccounts::register_derived(
                    RuntimeOrigin::signed(parent.clone()),
                    descriptor
                ));
                assert_eq!(Balances::reserved_balance(&parent), reserved);
            }
            let descriptor = DerivedDescriptor::Derivative {
                parent: parent.clone(),
                index,
            };
            assert_ok!(HashedAccounts::register_derived(
                RuntimeOrigin::signed(parent.clone()),
                descriptor.clone()
            ));
            reserved += value;
            assert_eq!(Balances::reserved_balance(&parent), reserved);
            assert!(HashedAccounts::is_protected(&descriptor.account_id()));
            previous_descriptor = Some(descriptor);
        }
    });
}

#[test]
fn full_payload_v4_roundtrip_executes_and_consumes_only_once() {
    with_profiles(|| {
        let account = register(42);
        let call = RuntimeCall::System(SystemCall::remark {
            remark: vec![91; 4096],
        });
        let tx = sign(42, call.clone());
        let wire = tx.encode();
        let mut body = wire.as_slice();
        codec::Compact::<u32>::decode(&mut body).unwrap();
        assert_eq!(&body[..4], &[0x84, 2, 0xdd, 0x53]);
        assert_eq!(UncheckedExtrinsic::decode(&mut &wire[..]).unwrap(), tx);
        assert_eq!(
            LegacyUnchecked::decode(&mut &wire[..]).unwrap().encode(),
            wire
        );
        let before = MlDsaAccounts::<Runtime>::get(&account).unwrap();
        let checked = tx.clone().check(&ChainContext::default()).unwrap();
        assert_eq!(checked.get_dispatch_info(), tx.get_dispatch_info());
        assert_eq!(MlDsaAccounts::<Runtime>::get(&account).unwrap(), before);
        assert_ok!(Executive::apply_extrinsic(tx.clone()).unwrap());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            1
        );
        assert_eq!(System::account_nonce(&account), 2);
        assert!(Executive::apply_extrinsic(tx).is_err());
        assert_ok!(Executive::apply_extrinsic(sign(42, call)).unwrap());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(account).unwrap().generation,
            2
        );
    });
}

#[test]
fn malformed_markers_and_transcript_mutations_never_rotate_or_charge() {
    with_profiles(|| {
        let account = register(43);
        let tx = sign(43, call());
        let state = MlDsaAccounts::<Runtime>::get(&account);
        let balance = Balances::free_balance(&account);
        for mutation in 0..8 {
            let mut bad = tx.clone();
            let UncheckedExtrinsic::Legacy(ref mut legacy) = bad;
            let Preamble::Signed(Address::Raw(raw), marker, extension) = &mut legacy.0.preamble
            else {
                panic!("raw signer")
            };
            match mutation {
                0 => *marker = Signature::Ed25519(sp_core::ed25519::Signature::from_raw([0; 64])),
                1 => {
                    *marker =
                        Signature::Sr25519(sr25519::Signature::from_raw(mldsa::CARRIER_SIGNATURE))
                }
                2 => raw[0] ^= 1,
                3 => raw[34] ^= 1,
                4 => raw[100] ^= 1,
                5 => raw[2000] ^= 1,
                6 => raw[3000] ^= 1,
                _ => extension.0 = extra(8),
            }
            assert!(
                Executive::apply_extrinsic(bad).is_err(),
                "mutation {mutation}"
            );
            assert_eq!(MlDsaAccounts::<Runtime>::get(&account), state);
            assert_eq!(Balances::free_balance(&account), balance);
            assert_eq!(System::account_nonce(&account), 1);
        }
        let mut changed_call = tx;
        let UncheckedExtrinsic::Legacy(ref mut legacy) = changed_call;
        legacy.0.function = RuntimeCall::System(SystemCall::remark { remark: vec![9] });
        assert!(Executive::apply_extrinsic(changed_call).is_err());
        assert_eq!(MlDsaAccounts::<Runtime>::get(account), state);
    });
}

#[test]
fn included_dispatch_error_rotates_but_disabled_and_overweight_do_not() {
    with_profiles(|| {
        let account = register(44);
        let nonce = System::account_nonce(&account);
        System::reset_events();
        let fail = RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
            dest: AccountId::new([99; 32]).into(),
            value: TaoBalance::new(u64::MAX),
        });
        let tx = sign(44, fail);
        assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            false
        ));
        assert!(Executive::apply_extrinsic(tx.clone()).is_err());
        assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            true
        ));
        hashed_auth::TestVerificationWeight::set(Weight::MAX);
        assert!(Executive::apply_extrinsic(tx.clone()).is_err());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            0
        );
        hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0));
        assert!(Executive::apply_extrinsic(tx.clone()).unwrap().is_err());
        let record = MlDsaAccounts::<Runtime>::get(&account).unwrap();
        assert_eq!(record.generation, 1);
        assert_eq!(
            record.commitment,
            mldsa::commitment(&mldsa::public_key_from_seed(&seed(44, 1)).unwrap())
        );
        assert_eq!(System::account_nonce(&account), nonce + 1);
        assert!(Executive::apply_extrinsic(tx).is_err());
        assert_ok!(Executive::apply_extrinsic(sign(44, call())).unwrap());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            2
        );
        assert_eq!(System::account_nonce(&account), nonce + 2);
        assert!(
            !System::events()
                .iter()
                .any(|record| matches!(record.event, RuntimeEvent::HashedAccounts(_)))
        );
    });
}

#[test]
fn single_admin_toggle_controls_all_hashed_schemes_and_preserves_authority() {
    use pallet_hashed_accounts::EvmAliases;

    with_profiles(|| {
        // Registered rotating ML-DSA is gated through the same AdminUtils flag.
        let account = register(44);
        let tx = sign(44, call());
        let before = MlDsaAccounts::<Runtime>::get(&account);
        let nonce = System::account_nonce(&account);
        let balance = Balances::free_balance(&account);
        let sponsor = pair(70);
        let sponsor_id = AccountId::from(sponsor.public());
        let mut classical = Vec::new();
        for scheme in [Scheme::Sr25519, Scheme::Ed25519] {
            let sr = pair(92);
            let ed = sp_core::ed25519::Pair::from_seed(&[93; 32]);
            let (public_key, next_public) = match scheme {
                Scheme::Sr25519 => (sr.public().0, pair(94).public().0),
                Scheme::Ed25519 => (
                    ed.public().0,
                    sp_core::ed25519::Pair::from_seed(&[95; 32]).public().0,
                ),
            };
            let descriptor = Descriptor {
                version: 1,
                scheme,
                initial_commitment: subtensor_hashed::key_commitment(scheme, &public_key),
            };
            let who = AccountId::new(subtensor_hashed::account_id(&descriptor));
            assert_ok!(
                Executive::apply_extrinsic(legacy(
                    &sponsor,
                    RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                        descriptor: descriptor.into(),
                    })
                ))
                .unwrap()
            );
            let _ = Balances::make_free_balance_be(&who, TaoBalance::new(1_000_000_000_000));
            let extra = extra(System::account_nonce(&who));
            let call = call();
            let address = subtensor_hashed::SignerAddress {
                version: 1,
                account: *who.as_ref(),
                scheme,
                generation: 0,
                public_key,
                next_commitment: subtensor_hashed::key_commitment(scheme, &next_public),
            };
            let message = hashed_extrinsic::signing_hash(&address, &call, &extra).unwrap();
            let signature = match scheme {
                Scheme::Sr25519 => Signature::Sr25519(sr.sign(&message)),
                Scheme::Ed25519 => Signature::Ed25519(ed.sign(&message)),
            };
            let signed = UncheckedExtrinsic::new_signed(
                call,
                Address::Raw(address.encode()),
                signature,
                extra,
            );
            let state = (
                HashedAccounts::accounts(&who),
                System::account_nonce(&who),
                Balances::free_balance(&who),
            );
            classical.push((who, descriptor, signed, state));
        }
        assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            false
        ));
        assert!(!HashedEnabled::get());
        assert!(Executive::apply_extrinsic(tx.clone()).is_err());
        assert_eq!(MlDsaAccounts::<Runtime>::get(&account), before);
        assert_eq!(System::account_nonce(&account), nonce);
        assert_eq!(Balances::free_balance(&account), balance);
        assert!(HashedAccounts::is_protected(&account));
        assert_eq!(
            EvmAliases::<Runtime>::get(HashedAccounts::evm_alias(&account)),
            Some(account.clone())
        );
        let fresh_ms = descriptor(45);
        assert_noop!(
            HashedAccounts::register(RuntimeOrigin::signed(sponsor_id.clone()), fresh_ms.into()),
            pallet_hashed_accounts::Error::<Runtime>::Disabled
        );
        for (who, descriptor, signed, state) in &classical {
            assert!(Executive::apply_extrinsic(signed.clone()).is_err());
            assert_eq!(
                (
                    HashedAccounts::accounts(who),
                    System::account_nonce(who),
                    Balances::free_balance(who)
                ),
                *state
            );
            assert!(HashedAccounts::is_protected(who));
            assert_eq!(
                EvmAliases::<Runtime>::get(HashedAccounts::evm_alias(who)),
                Some(who.clone())
            );
            let mut fresh = *descriptor;
            fresh.initial_commitment[0] ^= 1;
            assert_noop!(
                HashedAccounts::register(RuntimeOrigin::signed(sponsor_id.clone()), fresh.into()),
                pallet_hashed_accounts::Error::<Runtime>::Disabled
            );
        }

        // Ordinary keys keep transferring while every registered hashed scheme is paused.
        let recipient = AccountId::new([98; 32]);
        let transfer = RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
            dest: recipient.clone().into(),
            value: TaoBalance::new(1_000_000_000),
        });
        let sr = pair(90);
        let _ = Balances::make_free_balance_be(
            &AccountId::from(sr.public()),
            TaoBalance::new(1_000_000_000_000),
        );
        assert_ok!(Executive::apply_extrinsic(legacy(&sr, transfer.clone())).unwrap());
        let ed = sp_core::ed25519::Pair::from_seed(&[91; 32]);
        let who = AccountId::from(ed.public());
        let _ = Balances::make_free_balance_be(&who, TaoBalance::new(1_000_000_000_000));
        let extra = extra(System::account_nonce(&who));
        let signature = SignedPayload::new(transfer.clone(), extra.clone())
            .unwrap()
            .using_encoded(|m| ed.sign(m));
        let ordinary = UncheckedExtrinsic::new_signed(
            transfer,
            who.into(),
            Signature::Ed25519(signature),
            extra,
        );
        assert_ok!(Executive::apply_extrinsic(ordinary).unwrap());
        assert_eq!(
            Balances::free_balance(recipient),
            TaoBalance::new(2_000_000_000)
        );

        assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            true
        ));
        assert_ok!(Executive::apply_extrinsic(tx.clone()).unwrap());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            1
        );
        assert_ok!(HashedAccounts::register(
            RuntimeOrigin::signed(sponsor_id),
            fresh_ms.into()
        ));
        for (who, _, signed, _) in &classical {
            assert_ok!(Executive::apply_extrinsic(signed.clone()).unwrap());
            assert_eq!(HashedAccounts::accounts(who).unwrap().generation, 1);
        }
        // A later pause/resume must not reset consumed generations.
        assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            false
        ));
        assert_ok!(AdminUtils::sudo_set_hashed_accounts_enabled(
            RuntimeOrigin::root(),
            true
        ));
        assert!(Executive::apply_extrinsic(tx).is_err());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            1
        );
        for (who, _, signed, _) in classical {
            assert!(Executive::apply_extrinsic(signed).is_err());
            assert_eq!(HashedAccounts::accounts(who).unwrap().generation, 1);
        }
    });
}

#[test]
fn sponsor_registration_and_funding_are_atomic_under_normal_signed_v4() {
    with_profiles(|| {
        System::set_block_number(1);
        hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0));
        let sponsor = pair(71);
        let sponsor_id = AccountId::from(sponsor.public());
        let _ = Balances::make_free_balance_be(&sponsor_id, TaoBalance::new(1_000_000_000_000));
        let descriptor = descriptor(45);
        let account = AccountId::new(mldsa::account_id(&descriptor));
        let register_and_fund = |amount| {
            RuntimeCall::Utility(pallet_utility::Call::batch_all {
                calls: vec![
                    RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register {
                        descriptor: descriptor.into(),
                    }),
                    RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
                        dest: account.clone().into(),
                        value: TaoBalance::new(amount),
                    }),
                ],
            })
        };
        assert!(
            Executive::apply_extrinsic(legacy(&sponsor, register_and_fund(u64::MAX)))
                .unwrap()
                .is_err()
        );
        assert!(MlDsaAccounts::<Runtime>::get(&account).is_none());
        assert_eq!(System::account_nonce(&account), 0);
        assert_eq!(Balances::reserved_balance(&sponsor_id), TaoBalance::new(0));
        assert_ok!(
            Executive::apply_extrinsic(legacy(&sponsor, register_and_fund(10_000_000_000)))
                .unwrap()
        );
        assert_eq!(
            Balances::reserved_balance(&sponsor_id),
            TaoBalance::new(200_000_000)
        );
        assert_ok!(Executive::apply_extrinsic(sign(45, call())).unwrap());
    });
}

#[test]
fn mixed_and_all_pq_multisig_execute_under_the_unchanged_account_and_call_hash() {
    for all_pq in [false, true] {
        with_profiles(|| {
            let first = register(46);
            let ordinary = pair(72);
            let second = if all_pq {
                register(47)
            } else {
                AccountId::from(ordinary.public())
            };
            let _ = Balances::make_free_balance_be(&second, TaoBalance::new(1_000_000_000_000));
            let mut members = vec![first.clone(), second.clone()];
            members.sort();
            let descriptor = DerivedDescriptor::Multisig {
                threshold: 2,
                signatories: members.clone().try_into().unwrap(),
            };
            let composite = Multisig::multi_account_id(&members, 2);
            assert_eq!(descriptor.account_id(), composite);
            assert_noop!(
                HashedAccounts::register_derived(
                    RuntimeOrigin::signed(first.clone()),
                    descriptor.clone()
                ),
                sp_runtime::DispatchError::Other("DerivedProtectionRequiresOwner")
            );
            let protect = RuntimeCall::HashedAccounts(
                pallet_hashed_accounts::Call::register_derived_sponsored {
                    descriptor,
                    deposit_payer: second.clone(),
                },
            );
            let protection_hash = sp_io::hashing::blake2_256(&protect.encode());
            let protection_weight = protect.get_dispatch_info().total_weight();
            assert_ok!(
                Executive::apply_extrinsic(sign(
                    46,
                    RuntimeCall::Multisig(pallet_multisig::Call::approve_as_multi {
                        threshold: 2,
                        other_signatories: vec![second.clone()],
                        maybe_timepoint: None,
                        call_hash: protection_hash,
                        max_weight: protection_weight,
                    })
                ))
                .unwrap()
            );
            let pending =
                pallet_multisig::Multisigs::<Runtime>::get(&composite, protection_hash).unwrap();
            let execute_protection = RuntimeCall::Multisig(pallet_multisig::Call::as_multi {
                threshold: 2,
                other_signatories: vec![first.clone()],
                maybe_timepoint: Some(pending.when),
                call: Box::new(protect),
                max_weight: protection_weight,
            });
            let registration = if all_pq {
                sign(47, execute_protection)
            } else {
                legacy(&ordinary, execute_protection)
            };
            let sponsor_reserve = Balances::reserved_balance(&second);
            assert_ok!(Executive::apply_extrinsic(registration).unwrap());
            assert!(HashedAccounts::is_protected(&composite));
            assert_eq!(
                Balances::reserved_balance(&second),
                sponsor_reserve + TaoBalance::new(200_000_000)
            );
            let _ = Balances::make_free_balance_be(&composite, TaoBalance::new(10_000_000_000));
            let recipient = AccountId::new([98; 32]);
            let transfer = RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
                dest: recipient.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 approve = RuntimeCall::Multisig(pallet_multisig::Call::approve_as_multi {
                threshold: 2,
                other_signatories: vec![second.clone()],
                maybe_timepoint: None,
                call_hash: hash,
                max_weight: weight,
            });
            assert_ok!(Executive::apply_extrinsic(sign(46, approve)).unwrap());
            let pending = pallet_multisig::Multisigs::<Runtime>::get(&composite, hash).unwrap();
            let execute = RuntimeCall::Multisig(pallet_multisig::Call::as_multi {
                threshold: 2,
                other_signatories: vec![first.clone()],
                maybe_timepoint: Some(pending.when),
                call: Box::new(transfer),
                max_weight: weight,
            });
            let tx = if all_pq {
                sign(47, execute)
            } else {
                legacy(&ordinary, execute)
            };
            assert_ok!(Executive::apply_extrinsic(tx).unwrap());
            assert_eq!(
                Balances::free_balance(recipient),
                TaoBalance::new(1_000_000_000)
            );
            assert!(pallet_multisig::Multisigs::<Runtime>::get(&composite, hash).is_none());
            assert!(HashedAccounts::is_protected(&composite));
            assert_eq!(MlDsaAccounts::<Runtime>::get(first).unwrap().generation, 2);
            assert_eq!(MlDsaAccounts::<Runtime>::get(composite), None);
        });
    }
}

#[test]
fn pq_delegates_preserve_identity_and_classical_delegates_are_rejected() {
    with_profiles(|| {
        let account = register(48);
        let delegate = register(49);
        let ordinary = AccountId::from(pair(73).public());
        assert_noop!(
            Proxy::add_proxy(
                RuntimeOrigin::signed(account.clone()),
                ordinary.into(),
                ProxyType::Any,
                0
            ),
            pallet_proxy::Error::<Runtime>::AccountPolicyViolation
        );
        assert_ok!(
            Executive::apply_extrinsic(sign(
                48,
                RuntimeCall::Proxy(pallet_proxy::Call::add_proxy {
                    delegate: delegate.clone().into(),
                    proxy_type: ProxyType::Any,
                    delay: 0,
                })
            ))
            .unwrap()
        );
        let before = MlDsaAccounts::<Runtime>::get(&account);
        let recipient = AccountId::new([97; 32]);
        let inner = RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
            dest: recipient.clone().into(),
            value: TaoBalance::new(1_000_000_000),
        });
        assert_ok!(
            Executive::apply_extrinsic(sign(
                49,
                RuntimeCall::Proxy(pallet_proxy::Call::proxy {
                    real: account.clone().into(),
                    force_proxy_type: Some(ProxyType::Any),
                    call: Box::new(inner),
                })
            ))
            .unwrap()
        );
        assert_eq!(
            Balances::free_balance(recipient),
            TaoBalance::new(1_000_000_000)
        );
        assert_eq!(MlDsaAccounts::<Runtime>::get(account), before);
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(delegate).unwrap().generation,
            1
        );
    });
}

// PROTECTED_REGRESSION: derived protection cannot elevate restricted proxies through evm; changes require explicit human approval.
#[test]
fn derived_protection_cannot_elevate_restricted_proxies_through_evm() {
    use pallet_evm::{AddressMapping, FeeCalculator};
    use sp_core::{H160, U256};
    for kind in 0..3 {
        with_profiles(|| {
            let parent = register(54);
            let member = register(55);
            let delegate = register(56);
            let attacker = register(57);
            let descriptor = match kind {
                0 => {
                    let mut members = vec![parent.clone(), member];
                    members.sort();
                    DerivedDescriptor::Multisig {
                        threshold: 2,
                        signatories: members.try_into().unwrap(),
                    }
                }
                1 => DerivedDescriptor::Derivative {
                    parent: parent.clone(),
                    index: 42,
                },
                _ => {
                    assert_ok!(Proxy::create_protected_pure(
                        RuntimeOrigin::signed(parent.clone()),
                        ProxyType::Any,
                        0,
                        42
                    ));
                    DerivedDescriptor::PureProxy {
                        spawner: parent.clone(),
                        proxy_type: ProxyType::Any.encode().try_into().unwrap(),
                        index: 42,
                        height: System::block_number(),
                        extrinsic_index: System::extrinsic_index().unwrap_or_default(),
                    }
                }
            };
            let account = descriptor.account_id();
            let _ =
                Balances::make_free_balance_be(&account, TaoBalance::new(1_000_000_000_000_000));
            assert_ok!(Proxy::add_proxy(
                RuntimeOrigin::signed(account.clone()),
                delegate.clone().into(),
                ProxyType::NonCritical,
                0
            ));
            assert_ok!(HashedAccounts::register_derived(
                RuntimeOrigin::signed(account.clone()),
                descriptor
            ));
            let source = H160::from_slice(&AsRef::<[u8; 32]>::as_ref(&account)[..20]);
            assert_eq!(
                crate::evm_origin::HashedAddressMapping::into_account_id(source),
                account
            );
            assert!(
                Proxy::proxies(account.clone())
                    .0
                    .iter()
                    .any(|p| p.delegate == delegate && p.proxy_type == ProxyType::NonCritical)
            );
            let escalation = RuntimeCall::Proxy(pallet_proxy::Call::add_proxy {
                delegate: attacker.clone().into(),
                proxy_type: ProxyType::Any,
                delay: 0,
            });
            let evm = RuntimeCall::EVM(pallet_evm::Call::call {
                source,
                target: H160::from_low_u64_be(6),
                input: escalation.encode(),
                value: U256::zero(),
                gas_limit: 5_000_000,
                max_fee_per_gas: <Runtime as pallet_evm::Config>::FeeCalculator::min_gas_price().0,
                max_priority_fee_per_gas: None,
                nonce: None,
                access_list: vec![],
                authorization_list: Default::default(),
            });
            let nested = RuntimeCall::Utility(pallet_utility::Call::batch_all {
                calls: vec![evm.clone()],
            });
            for inner in [evm.clone(), nested] {
                System::reset_events();
                assert_ok!(Proxy::proxy(
                    RuntimeOrigin::signed(delegate.clone()),
                    account.clone().into(),
                    Some(ProxyType::NonCritical),
                    Box::new(inner)
                ));
                assert!(System::events().iter().any(|record| matches!(&record.event,
                    RuntimeEvent::Proxy(pallet_proxy::Event::ProxyExecuted { result: Err(error) })
                    if *error == frame_system::Error::<Runtime>::CallFiltered.into()
                )));
                assert!(
                    !Proxy::proxies(account.clone())
                        .0
                        .iter()
                        .any(|p| p.delegate == attacker)
                );
            }
            let call_hash = sp_io::hashing::blake2_256(&evm.encode()).into();
            assert_ok!(Proxy::announce(
                RuntimeOrigin::signed(delegate.clone()),
                account.clone().into(),
                call_hash
            ));
            System::reset_events();
            assert_ok!(Proxy::proxy_announced(
                RuntimeOrigin::signed(parent),
                delegate.clone().into(),
                account.clone().into(),
                Some(ProxyType::NonCritical),
                Box::new(evm.clone())
            ));
            assert!(System::events().iter().any(|record| matches!(&record.event,
                RuntimeEvent::Proxy(pallet_proxy::Event::ProxyExecuted { result: Err(error) })
                if *error == frame_system::Error::<Runtime>::CallFiltered.into()
            )));
            assert!(
                !Proxy::proxies(account.clone())
                    .0
                    .iter()
                    .any(|p| p.delegate == attacker)
            );
            // A grant of full authority may use precisely the same EVM call.
            // This proves the negative cases are filter enforcement, not an
            // unusable payload, gas shortage or unrelated precompile failure.
            assert_ok!(Proxy::add_proxy(
                RuntimeOrigin::signed(account.clone()),
                delegate.clone().into(),
                ProxyType::Any,
                0
            ));
            assert_ok!(Proxy::proxy(
                RuntimeOrigin::signed(delegate),
                account.clone().into(),
                Some(ProxyType::Any),
                Box::new(evm)
            ));
            assert!(
                Proxy::proxies(account)
                    .0
                    .iter()
                    .any(|p| p.delegate == attacker && p.proxy_type == ProxyType::Any)
            );
        });
    }
}

#[test]
fn protected_derivatives_and_pure_proxies_preserve_their_floor_through_lifecycle() {
    with_profiles(|| {
        let parent = register(50);
        let derivative = Utility::derivative_account_id(parent.clone(), 7).unwrap();
        assert_ok!(
            Executive::apply_extrinsic(sign(
                50,
                RuntimeCall::Utility(pallet_utility::Call::as_protected_derivative {
                    index: 7,
                    call: Box::new(call()),
                })
            ))
            .unwrap()
        );
        assert!(HashedAccounts::authority_meets(
            &derivative,
            pallet_hashed_accounts::SecurityFloor::PqRequired
        ));
        assert_eq!(
            Balances::reserved_balance(&parent),
            TaoBalance::new(200_000_000)
        );
        let failed_child = Utility::derivative_account_id(parent.clone(), 8).unwrap();
        let before = MlDsaAccounts::<Runtime>::get(&parent).unwrap().generation;
        let fail = RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
            dest: AccountId::new([98; 32]).into(),
            value: TaoBalance::new(u64::MAX),
        });
        assert!(
            Executive::apply_extrinsic(sign(
                50,
                RuntimeCall::Utility(pallet_utility::Call::as_protected_derivative {
                    index: 8,
                    call: Box::new(fail),
                })
            ))
            .unwrap()
            .is_err()
        );
        assert!(!HashedAccounts::is_protected(&failed_child));
        assert_eq!(
            Balances::reserved_balance(&parent),
            TaoBalance::new(200_000_000)
        );
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&parent).unwrap().generation,
            before + 1
        );
        let ext_index = System::extrinsic_index().unwrap_or_default();
        let height = System::block_number();
        let pure =
            Proxy::pure_account(&parent, &ProxyType::Any, 9, Some((height, ext_index))).unwrap();
        assert_ok!(
            Executive::apply_extrinsic(sign(
                50,
                RuntimeCall::Proxy(pallet_proxy::Call::create_protected_pure {
                    proxy_type: ProxyType::Any,
                    delay: 0,
                    index: 9,
                })
            ))
            .unwrap()
        );
        assert!(HashedAccounts::authority_meets(
            &pure,
            pallet_hashed_accounts::SecurityFloor::PqRequired
        ));
        let kill = RuntimeCall::Proxy(pallet_proxy::Call::kill_pure {
            spawner: parent.clone().into(),
            proxy_type: ProxyType::Any,
            index: 9,
            height,
            ext_index,
        });
        assert_ok!(
            Executive::apply_extrinsic(sign(
                50,
                RuntimeCall::Proxy(pallet_proxy::Call::proxy {
                    real: pure.clone().into(),
                    force_proxy_type: None,
                    call: Box::new(kill),
                })
            ))
            .unwrap()
        );
        assert!(Proxy::proxies(pure.clone()).0.is_empty());
        assert!(HashedAccounts::is_protected(&pure));
        assert!(HashedAccounts::is_protected(&derivative));
        // Removing a pure proxy releases its proxy deposit, but both account
        // registration reserves remain permanent.
        assert_eq!(
            Balances::reserved_balance(&parent),
            TaoBalance::new(400_000_000)
        );
    });
}

#[test]
fn restricted_proxies_cannot_reserve_permanent_protected_account_deposits() {
    use frame_support::traits::InstanceFilter;
    use pallet_hashed_accounts::DerivedAuthorities;

    for proxy_type in [
        ProxyType::NonTransfer,
        ProxyType::NonFungible,
        ProxyType::NonCritical,
    ] {
        for pure in [false, true] {
            with_profiles(|| {
                let [owner, delegate, bridge] = [61, 62, 63].map(register);
                assert_ok!(Proxy::add_proxy_delegate(
                    &owner,
                    delegate.clone(),
                    proxy_type,
                    0
                ));
                assert_ok!(Proxy::add_proxy_delegate(
                    &bridge,
                    delegate.clone(),
                    proxy_type,
                    0
                ));
                assert_ok!(Proxy::add_proxy_delegate(
                    &owner,
                    bridge.clone(),
                    ProxyType::Any,
                    0
                ));
                let index = 23;
                let (child, creation) = if pure {
                    (
                        Proxy::pure_account(&owner, &ProxyType::Any, index, None).unwrap(),
                        RuntimeCall::Proxy(pallet_proxy::Call::create_protected_pure {
                            proxy_type: ProxyType::Any,
                            delay: 0,
                            index,
                        }),
                    )
                } else {
                    (
                        Utility::derivative_account_id(owner.clone(), index).unwrap(),
                        RuntimeCall::Utility(pallet_utility::Call::as_protected_derivative {
                            index,
                            call: Box::new(call()),
                        }),
                    )
                };
                for grant in (0..=u8::MAX).filter_map(|i| ProxyType::try_from(i).ok()) {
                    assert_eq!(grant.filter(&creation), grant == ProxyType::Any);
                }
                let balances = [&owner, &delegate, &bridge].map(|account| {
                    (
                        Balances::free_balance(account),
                        Balances::reserved_balance(account),
                    )
                });
                let routes = [
                    (owner.clone(), creation.clone()),
                    (
                        owner.clone(),
                        RuntimeCall::Utility(pallet_utility::Call::batch_all {
                            calls: vec![creation.clone()],
                        }),
                    ),
                    (
                        bridge.clone(),
                        RuntimeCall::Proxy(pallet_proxy::Call::proxy {
                            real: owner.clone().into(),
                            force_proxy_type: Some(ProxyType::Any),
                            call: Box::new(creation.clone()),
                        }),
                    ),
                ];
                for (real, call) in routes {
                    assert_ok!(Proxy::proxy(
                        RuntimeOrigin::signed(delegate.clone()),
                        real.into(),
                        Some(proxy_type),
                        Box::new(call)
                    ));
                    assert_eq!(
                        pallet_proxy::LastCallResult::<Runtime>::get(&owner),
                        Some(Err(frame_system::Error::<Runtime>::CallFiltered.into()))
                    );
                    assert_eq!(
                        [&owner, &delegate, &bridge].map(|account| {
                            (
                                Balances::free_balance(account),
                                Balances::reserved_balance(account),
                            )
                        }),
                        balances
                    );
                    assert!(!DerivedAuthorities::<Runtime>::contains_key(&child));
                    assert!(
                        !pallet_hashed_accounts::EvmAliases::<Runtime>::contains_key(
                            HashedAccounts::evm_alias(&child)
                        )
                    );
                    assert!(Proxy::proxies(child.clone()).0.is_empty());
                    assert_eq!(System::account_nonce(&child), 0);
                }
                // The identical call succeeds with full authority. This proves
                // the failed routes were denied before registration, rather
                // than relying on an invalid fixture or insufficient funds.
                assert_ok!(Proxy::proxy(
                    RuntimeOrigin::signed(bridge),
                    owner.clone().into(),
                    Some(ProxyType::Any),
                    Box::new(creation)
                ));
                assert_eq!(
                    pallet_proxy::LastCallResult::<Runtime>::get(&owner),
                    Some(Ok(()))
                );
                assert!(DerivedAuthorities::<Runtime>::contains_key(&child));
                assert!(
                    Balances::reserved_balance(&owner)
                        >= balances[0]
                            .1
                            .saturating_add(DerivedRegistrationDeposit::get())
                );
                assert!(HashedAccounts::is_protected(&child));
            });
        }
    }
}

#[test]
fn shield_mldsa_outer_and_inner_consume_exactly_the_two_prepared_generations() {
    with_profiles(|| {
        let account = register(51);
        frame_system::BlockHash::<Runtime>::insert(1, H256::repeat_byte(1));
        let mut outer_extra = extra(1);
        outer_extra.0.4 = check_mortality::CheckMortality::from(Era::mortal(8, 1));
        let mut inner_extra = extra(2);
        inner_extra.0.4 = check_mortality::CheckMortality::from(Era::mortal(8, 1));
        let inner = sign_with_extra(51, 1, call(), inner_extra);
        // Runtime admission validates the carrier's framing. Proposer crypto
        // decryption is tested by the Shield crate; the inner bytes stay exact.
        let mut ciphertext = vec![0xab; 16];
        ciphertext.extend_from_slice(&1088u16.to_le_bytes());
        ciphertext.extend_from_slice(&[0xcc; 1088]);
        ciphertext.extend_from_slice(&[0xdd; 24]);
        ciphertext.extend_from_slice(&inner.encode());
        ciphertext.extend_from_slice(&[0xee; 16]);
        let carrier = RuntimeCall::MevShield(pallet_shield::Call::submit_encrypted {
            ciphertext: ciphertext.try_into().unwrap(),
        });
        let immortal = sign(51, carrier.clone());
        assert!(Executive::apply_extrinsic(immortal).is_err());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            0
        );
        // An inner generation cannot execute before its own carrier.
        assert!(Executive::apply_extrinsic(inner.clone()).is_err());
        let outer = sign_with_extra(51, 0, carrier, outer_extra);
        assert!(hashed_extrinsic::shielded_transaction(outer.clone()).is_some());
        // Recognition is deliberately syntactic. A forged proof remains a
        // carrier but cannot pass the outer apply step that precedes unshielding.
        let mut forged = outer.clone();
        let UncheckedExtrinsic::Legacy(ref mut raw) = forged;
        if let Preamble::Signed(Address::Raw(proof), _, _) = &mut raw.0.preamble {
            *proof.last_mut().unwrap() ^= 1;
        } else {
            panic!("expected signed ML-DSA carrier");
        }
        assert!(hashed_extrinsic::shielded_transaction(forged.clone()).is_some());
        assert!(Executive::apply_extrinsic(forged).is_err());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            0
        );
        assert_eq!(System::account_nonce(&account), 1);
        assert!(
            hashed_extrinsic::shielded_transaction(UncheckedExtrinsic::new_bare(
                outer.clone().into_call()
            ))
            .is_none()
        );
        assert_ok!(Executive::apply_extrinsic(outer.clone()).unwrap());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            1
        );
        assert_ok!(Executive::apply_extrinsic(inner.clone()).unwrap());
        assert_eq!(
            MlDsaAccounts::<Runtime>::get(&account).unwrap().generation,
            2
        );
        assert_eq!(System::account_nonce(account), 3);
        assert!(Executive::apply_extrinsic(outer).is_err());
        assert!(Executive::apply_extrinsic(inner).is_err());
    });
}