//! Exhaustive metadata-to-Executive envelope coverage. Minimal arguments exercise //! authentication and dispatch, not every operation's successful business state. use super::*; use fp_self_contained::SelfContainedCall; use scale_info::{PortableRegistry, TypeDef, TypeDefPrimitive}; use sp_core::ed25519; use sp_runtime::traits::TransactionExtension; use subtensor_hashed::mldsa; /// Construct finite canonical values from metadata, trying other enum variants /// when a recursive branch cannot terminate. Unknown or uninhabited types are /// returned explicitly rather than silently omitted from the coverage report. fn minimal_value(registry: &PortableRegistry, id: u32, path: &mut Vec) -> Option> { if path.contains(&id) || path.len() >= 64 { return None; } path.push(id); let result = (|| { let ty = ®istry.resolve(id)?.type_def; let mut fields = |ids: Vec| -> Option> { let mut bytes = Vec::new(); for id in ids { bytes.extend(minimal_value(registry, id, path)?); } Some(bytes) }; match ty { TypeDef::Composite(composite) => { fields(composite.fields.iter().map(|f| f.ty.id).collect()) } TypeDef::Tuple(tuple) => fields(tuple.fields.iter().map(|f| f.id).collect()), TypeDef::Array(array) => { let element = minimal_value(registry, array.type_param.id, path)?; Some(element.repeat(usize::try_from(array.len).ok()?)) } TypeDef::Sequence(_) | TypeDef::BitSequence(_) => Some(vec![0]), TypeDef::Compact(_) => Some(vec![4]), TypeDef::Variant(variants) => variants.variants.iter().find_map(|variant| { let mut bytes = vec![variant.index]; for field in &variant.fields { bytes.extend(minimal_value(registry, field.ty.id, path)?); } Some(bytes) }), TypeDef::Primitive(primitive) => { let len = match primitive { TypeDefPrimitive::Bool | TypeDefPrimitive::U8 | TypeDefPrimitive::I8 => 1, TypeDefPrimitive::U16 | TypeDefPrimitive::I16 => 2, TypeDefPrimitive::U32 | TypeDefPrimitive::I32 | TypeDefPrimitive::Char => 4, TypeDefPrimitive::U64 | TypeDefPrimitive::I64 => 8, TypeDefPrimitive::U128 | TypeDefPrimitive::I128 => 16, TypeDefPrimitive::U256 | TypeDefPrimitive::I256 => 32, TypeDefPrimitive::Str => return Some(vec![0]), }; let mut bytes = vec![0; len]; bytes[0] = 1; Some(bytes) } } })(); path.pop(); result } #[derive(Clone, Copy, Debug)] enum Mode { Sr, Ed, HashedMs, FixedMs, Falcon(u8), } fn signed(call: RuntimeCall, mode: Mode) -> (AccountId, UncheckedExtrinsic) { if let Mode::Falcon(profile) = mode { let wallet = super::falcon::Wallet::new(91, profile); wallet.prepare(); return (wallet.account.clone(), wallet.sign(call)); } if matches!(mode, Mode::FixedMs) { pallet_admin_utils::HashedAccountsEnabled::::put(true); let wallet = super::mldsa::stateless::Wallet::new(91); hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0)); let _ = Balances::make_free_balance_be(&wallet.account, TaoBalance::new(1_000_000_000_000_000)); return (wallet.account.clone(), wallet.sign(call)); } let key = [91; 32]; let (account, ms) = match mode { Mode::Sr => (AccountId::from(pair(91).public()), None), Mode::Ed => ( AccountId::new(ed25519::Pair::from_seed(&key).public().0), None, ), Mode::FixedMs | Mode::Falcon(_) => unreachable!("handled before registered signing modes"), Mode::HashedMs => { pallet_admin_utils::HashedAccountsEnabled::::put(true); hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0)); let public_key = mldsa::public_key_from_seed(&key).unwrap(); let descriptor = mldsa::Descriptor { version: mldsa::VERSION, algorithm: mldsa::ALGORITHM, initial_commitment: mldsa::commitment(&public_key), }; let account = AccountId::new(mldsa::account_id(&descriptor)); let sponsor = AccountId::from(pair(70).public()); let _ = Balances::make_free_balance_be(&sponsor, TaoBalance::new(1_000_000_000_000_000)); frame_support::assert_ok!(HashedAccounts::register( RuntimeOrigin::signed(sponsor), descriptor.into() )); (account, Some((descriptor, public_key))) } }; let _ = Balances::make_free_balance_be(&account, TaoBalance::new(1_000_000_000_000_000)); let extra = extra(System::account_nonce(&account)); let xt = if let Some((descriptor, public_key)) = ms { let next_commitment = mldsa::commitment(&mldsa::public_key_from_seed(&[92; 32]).unwrap()); let header = mldsa::Header { account: *account.as_ref(), version: mldsa::VERSION, algorithm: descriptor.algorithm, generation: 0, public_key, next_commitment, }; let payload = (&call, &extra, extra.implicit().unwrap()).encode(); let address = header.sign(&key, &payload, &[0; 32]).unwrap(); UncheckedExtrinsic::new_signed( call, Address::Raw(address.encode()), Signature::Ed25519(ed25519::Signature::from_raw(mldsa::CARRIER_SIGNATURE)), extra, ) } else { let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap(); let signature = match mode { Mode::Sr => Signature::Sr25519(payload.using_encoded(|bytes| pair(91).sign(bytes))), _ => Signature::Ed25519( payload.using_encoded(|bytes| ed25519::Pair::from_seed(&key).sign(bytes)), ), }; UncheckedExtrinsic::new_signed(call, account.clone().into(), signature, extra) }; (account, xt) } #[test] fn every_metadata_call_has_explicit_envelope_and_dispatch_coverage() { let encoded = hashed_extrinsic::metadata_at_version(15).unwrap(); let metadata = frame_metadata::RuntimeMetadataPrefixed::decode(&mut &encoded[..]).unwrap(); let frame_metadata::RuntimeMetadata::V15(metadata) = metadata.1 else { panic!("metadata15 unavailable"); }; let mut unresolved = Vec::new(); let mut covered = 0usize; let mut self_contained = 0usize; let mut uninhabited = Vec::new(); let mut registration_deposit_setter_covered = false; let mut registration_deposit_guard_covered = false; for pallet in &metadata.pallets { let Some(calls) = &pallet.calls else { continue }; let TypeDef::Variant(calls) = &metadata.types.resolve(calls.ty.id).unwrap().type_def else { panic!("call metadata is not an enum"); }; for variant in &calls.variants { let name = format!("{}.{}", pallet.name, variant.name); let is_registration_deposit_setter = name == "AdminUtils.sudo_set_hashed_account_registration_deposit"; let is_registration_deposit_guard = name == "HashedAccounts.check_registration_deposit"; if is_registration_deposit_guard { assert_eq!((pallet.index, variant.index), (33, 5)); registration_deposit_guard_covered = true; } if is_registration_deposit_setter { assert_eq!((pallet.index, variant.index), (19, 119)); registration_deposit_setter_covered = true; } // Grandpa equivocation reports are intentionally uncallable in this // runtime: KeyOwnerProof is Void, an enum with no inhabitants. // Assert that metadata proves this instead of silently skipping it. if variant.fields.iter().any(|field| { matches!( &metadata.types.resolve(field.ty.id).unwrap().type_def, TypeDef::Variant(variants) if variants.variants.is_empty() ) }) { uninhabited.push(name.clone()); println!("MS_MATRIX {name} uninhabited-call-type"); continue; } let mut encoded = vec![pallet.index, variant.index]; let mut supported = true; for field in &variant.fields { match minimal_value(&metadata.types, field.ty.id, &mut Vec::new()) { Some(value) => encoded.extend(value), None => { supported = false; break; } } } // Ethereum encodes its transaction using a custom codec. Supply a // valid typed legacy transaction so all native signing modes prove // that the self-contained ECDSA route cannot be wrapped as signed. if name == "Ethereum.transact" { encoded = RuntimeCall::Ethereum(pallet_ethereum::Call::transact { transaction: ethereum::TransactionV3::Legacy(ethereum::LegacyTransaction { nonce: sp_core::U256::zero(), gas_price: sp_core::U256::from(1_000_000_000_u64), gas_limit: sp_core::U256::from(21_000), action: ethereum::TransactionAction::Call(sp_core::H160::repeat_byte(2)), value: sp_core::U256::zero(), input: Vec::new(), signature: ethereum::legacy::TransactionSignature::new( 27, sp_core::H256::repeat_byte(1), sp_core::H256::repeat_byte(1), ) .unwrap(), }), }) .encode(); supported = true; } let mut input = encoded.as_slice(); let call = RuntimeCall::decode(&mut input); let Ok(call) = call else { unresolved.push(format!("{name}: custom decoding or finite value required")); continue; }; if !supported || !input.is_empty() || call.encode() != encoded { unresolved.push(format!("{name}: noncanonical or recursive metadata")); continue; } // The pinned upstream contracts pallet consumes migration_noop on // the no-migration branch even if the caller supplies a smaller // limit (migration.rs:399). Its WeightMeter debug assertion then // panics. Exercise the dispatch with the sufficient documented // budget; keep that preexisting weight issue in the release report. let call = match call { RuntimeCall::Contracts(pallet_contracts::Call::migrate { .. }) => { RuntimeCall::Contracts(pallet_contracts::Call::migrate { weight_limit: <::WeightInfo as pallet_contracts::WeightInfo>::migration_noop(), }) } call => call, }; covered += 1; let ethereum = call.is_self_contained(); self_contained += >::from(ethereum); let mut outcomes = Vec::new(); for mode in [ Mode::Sr, Mode::Ed, Mode::HashedMs, Mode::FixedMs, Mode::Falcon(6), Mode::Falcon(7), Mode::Falcon(8), Mode::Falcon(9), ] { ext().execute_with(|| { System::set_block_number(1); pallet_admin_utils::HashedAccountsEnabled::::put(true); let (account, tx) = signed(call.clone(), mode); let protected = matches!(mode, Mode::HashedMs | Mode::Falcon(6 | 8)); let checked = tx.clone().check(&ChainContext::default()); if ethereum { assert!( checked.is_err(), "{name} {mode:?}: signed Ethereum wrapper accepted" ); println!("MS_MATRIX {name} {mode:?} self-contained-signature-required"); return; } assert!( checked.is_ok(), "{name} {mode:?}: signature/envelope rejected" ); let before = System::account_nonce(&account); let validity = frame_support::storage::with_transaction_unchecked(|| { frame_support::storage::TransactionOutcome::Rollback( Executive::validate_transaction( sp_runtime::transaction_validity::TransactionSource::External, tx.clone(), System::parent_hash(), ), ) }); // BlockBuilder rolls back an extrinsic rejected by Executive, // including a signed mandatory call whose dispatch failure is // classified as BadMandatory after extension preparation. let result = frame_support::storage::with_transaction_unchecked(|| { let result = Executive::apply_extrinsic(tx); if result.is_ok() { frame_support::storage::TransactionOutcome::Commit(result) } else { frame_support::storage::TransactionOutcome::Rollback(result) } }); if is_registration_deposit_setter { assert_eq!(result, Ok(Err(sp_runtime::DispatchError::BadOrigin))); assert!(!pallet_hashed_accounts::HashedAccountRegistrationDeposit::< Runtime, >::exists()); } if is_registration_deposit_guard { assert_eq!(result, Ok(Err(pallet_hashed_accounts::Error::::RegistrationDepositTooHigh.into()))); } if protected { let generation = HashedAccounts::mldsa_accounts(&account).unwrap().generation; assert_eq!( generation, >::from(result.is_ok()), "{name} {mode:?}" ); assert_eq!( System::account_nonce(&account), before + >::from(result.is_ok()), "{name} {mode:?}" ); } if matches!(mode, Mode::FixedMs | Mode::Falcon(7 | 9)) { assert!(HashedAccounts::mldsa_accounts(&account).is_none()); assert_eq!( System::account_nonce(&account), before + >::from(result.is_ok()), "{name} {mode:?}" ); } println!("MS_MATRIX {name} {mode:?} {result:?} pool={validity:?}"); outcomes.push((result, validity.map(|_| ()))); }); } if !ethereum { let [ sr, ed, hashed_ms, fixed_ms, hashed512, fixed512, hashed1024, fixed1024, ] = outcomes.as_slice() else { panic!("{name}: missing wallet result"); }; assert_eq!(sr, ed, "{name}: ordinary SR/ED outcome differs"); assert_eq!(sr, fixed512, "{name}: fixed Falcon512 outcome differs"); assert_eq!(sr, fixed1024, "{name}: fixed Falcon1024 outcome differs"); assert_eq!( hashed_ms, hashed512, "{name}: hashed Falcon512 outcome differs" ); assert_eq!( hashed_ms, hashed1024, "{name}: hashed Falcon1024 outcome differs" ); assert_eq!(sr, fixed_ms, "{name}: registrationless MS outcome differs"); assert_eq!(sr.1, hashed_ms.1, "{name}: pool validity differs"); // These are the only expected dispatch differences for the // common minimal fixture. Enforce their exact authority errors // rather than allowing a whole pallet to escape parity checks. let expected: Option<(sp_runtime::DispatchResult, sp_runtime::DispatchResult)> = match name.as_str() { "SubtensorModule.announce_coldkey_swap_v2" => Some(( Ok(()), Err( pallet_subtensor::Error::::HotkeyOwnerPolicyViolation .into(), ), )), "Utility.as_derivative" => Some(( Ok(()), Err(sp_runtime::DispatchError::Other( "ProtectedDerivativeRequired", )), )), "Utility.as_protected_derivative" => Some(( Err(sp_runtime::DispatchError::Other("PqParentRequired")), Ok(()), )), "Proxy.add_proxy" | "Proxy.create_pure" => Some(( Ok(()), Err( crate::pallet_proxy::Error::::AccountPolicyViolation .into(), ), )), "Proxy.set_real_pays_fee" => Some(( Err(crate::pallet_proxy::Error::::NotProxy.into()), Err( crate::pallet_proxy::Error::::AccountPolicyViolation .into(), ), )), "Proxy.create_protected_pure" => Some(( Err( crate::pallet_proxy::Error::::AccountPolicyViolation .into(), ), Ok(()), )), _ => None, }; if let Some((classical, protected)) = expected { assert_eq!(sr.0, Ok(classical), "{name}: classical policy changed"); assert_eq!( hashed_ms.0, Ok(protected), "{name}: protected policy changed" ); } else { assert_eq!(sr.0, hashed_ms.0, "{name}: dispatch parity failed"); } } } } println!( "MS_MATRIX covered={covered} self_contained={self_contained} uninhabited={uninhabited:?} unresolved={unresolved:?}" ); // New calls must never silently disappear from the report. A custom codec // needs an explicit fixture before this matrix can be called exhaustive. assert!( unresolved.is_empty(), "unresolved call fixtures: {unresolved:?}" ); assert!(covered > 300, "unexpectedly incomplete runtime metadata"); assert!(registration_deposit_setter_covered); assert!(registration_deposit_guard_covered); } #[test] #[cfg(feature = "pow-faucet")] fn malformed_faucet_work_is_a_dispatch_error_for_every_signing_mode() { for mode in [ Mode::Sr, Mode::Ed, Mode::HashedMs, Mode::FixedMs, Mode::Falcon(6), Mode::Falcon(7), Mode::Falcon(8), Mode::Falcon(9), ] { for len in [0, 1, 31, 33, 64] { ext().execute_with(|| { System::set_block_number(1); let (account, tx) = signed( RuntimeCall::SubtensorModule(pallet_subtensor::Call::faucet { block_number: 1, nonce: 0, work: vec![0; len], }), mode, ); let before = System::account_nonce(&account); assert_eq!( Executive::apply_extrinsic(tx).unwrap(), Err(pallet_subtensor::Error::::InvalidSeal.into()), "{mode:?}: work length {len}" ); assert_eq!(System::account_nonce(&account), before + 1); if matches!(mode, Mode::HashedMs | Mode::Falcon(6 | 8)) { assert_eq!( HashedAccounts::mldsa_accounts(account).unwrap().generation, 1 ); } assert!(!System::events().iter().any(|event| matches!( event.event, RuntimeEvent::SubtensorModule(pallet_subtensor::Event::Faucet(..)) ))); }); } } } #[test] fn disclosed_unregistered_ms_address_can_be_blocked_by_hostile_hotkey_association() { super::mldsa::with_profiles(|| { System::set_block_number(1); hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0)); let descriptor = super::mldsa::descriptor(74); let victim = AccountId::new(mldsa::account_id(&descriptor)); // An ordinary transfer to bare SS58 supplies no descriptor registration // guard. This public transaction discloses the target without its key. let amount = TaoBalance::new(100_000_000_000); let (_, funding) = signed( RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest: victim.clone().into(), value: amount, }), Mode::Ed, ); let public_funding_bytes = funding.encode(); frame_support::assert_ok!(Executive::apply_extrinsic(funding).unwrap()); assert_eq!(Balances::free_balance(&victim), amount); // The attacker learns the AccountId from the already included payment, // not by guessing its hash, seeing a descriptor, or racing that payment. let observed_call = UncheckedExtrinsic::decode(&mut public_funding_bytes.as_slice()) .unwrap() .into_call(); let observed_recipient = match observed_call { RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest: Address::Id(recipient), .. }) => recipient, _ => panic!("funding fixture must disclose a bare destination AccountId"), }; let (attacker, claim) = signed( RuntimeCall::SubtensorModule(pallet_subtensor::Call::try_associate_hotkey { hotkey: observed_recipient, }), Mode::Sr, ); frame_support::assert_ok!(Executive::apply_extrinsic(claim).unwrap()); assert_eq!(pallet_subtensor::Owner::::get(&victim), attacker); assert!(!HashedAccounts::is_protected(&victim)); let (_, registration) = signed( RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register { descriptor: descriptor.into(), }), Mode::Ed, ); assert_eq!( Executive::apply_extrinsic(registration).unwrap(), Err(sp_runtime::DispatchError::Other( "MlDsaHotkeyRequiresPqOwner" )) ); assert!(!HashedAccounts::is_protected(&victim)); assert_eq!(Balances::free_balance(&victim), amount); // Possession of the genuine initial MS key cannot bypass the missing // registry record, even for a transfer recovering only native TAO. let call = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest: attacker.clone().into(), value: TaoBalance::new(1_000_000_000), }); let extra = extra(System::account_nonce(&victim)); let seed = super::mldsa::seed(74, 0); let header = mldsa::Header { account: *victim.as_ref(), version: mldsa::VERSION, algorithm: descriptor.algorithm, generation: 0, public_key: mldsa::public_key_from_seed(&seed).unwrap(), next_commitment: mldsa::commitment( &mldsa::public_key_from_seed(&super::mldsa::seed(74, 1)).unwrap(), ), }; let address = header .sign( &seed, &(&call, &extra, extra.implicit().unwrap()).encode(), &[0; 32], ) .unwrap(); let recovery = UncheckedExtrinsic::new_signed( call, Address::Raw(address.encode()), Signature::Ed25519(ed25519::Signature::from_raw(mldsa::CARRIER_SIGNATURE)), extra, ); assert!(matches!( recovery.check(&ChainContext::default()), Err( sp_runtime::transaction_validity::TransactionValidityError::Invalid( InvalidTransaction::BadProof ) ) )); assert_eq!(pallet_subtensor::Owner::::get(&victim), attacker); assert_eq!(Balances::free_balance(&victim), amount); }); } #[test] fn registered_ms_address_rejects_hostile_weak_owner_and_remains_spendable() { super::mldsa::with_profiles(|| { let victim = super::mldsa::register(74); let record = HashedAccounts::mldsa_accounts(&victim).unwrap(); let (attacker, claim) = signed( RuntimeCall::SubtensorModule(pallet_subtensor::Call::try_associate_hotkey { hotkey: victim.clone(), }), Mode::Sr, ); assert_eq!( Executive::apply_extrinsic(claim).unwrap(), Err(pallet_subtensor::Error::::HotkeyOwnerPolicyViolation.into()) ); assert!(!pallet_subtensor::Owner::::contains_key(&victim)); assert_eq!(HashedAccounts::mldsa_accounts(&victim).unwrap(), record); let before = Balances::free_balance(&attacker); let amount = TaoBalance::new(1_000_000_000); let transfer = super::mldsa::sign( 74, RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest: attacker.clone().into(), value: amount, }), ); frame_support::assert_ok!(Executive::apply_extrinsic(transfer).unwrap()); assert_eq!(Balances::free_balance(&attacker), before + amount); assert_eq!( HashedAccounts::mldsa_accounts(&victim).unwrap().generation, 1 ); }); } #[test] fn atomic_ms_registration_payment_does_not_fund_a_hostile_association() { super::mldsa::with_profiles(|| { System::set_block_number(1); hashed_auth::TestVerificationWeight::set(Weight::from_parts(10_000_000, 0)); let descriptor = super::mldsa::descriptor(74); let victim = AccountId::new(mldsa::account_id(&descriptor)); let (attacker, claim) = signed( RuntimeCall::SubtensorModule(pallet_subtensor::Call::try_associate_hotkey { hotkey: victim.clone(), }), Mode::Sr, ); frame_support::assert_ok!(Executive::apply_extrinsic(claim).unwrap()); let reserved = Balances::reserved_balance(&attacker); let (_, payment) = signed( RuntimeCall::Utility(pallet_utility::Call::batch_all { calls: vec![ RuntimeCall::HashedAccounts(pallet_hashed_accounts::Call::register { descriptor: descriptor.into(), }), RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest: victim.clone().into(), value: TaoBalance::new(1_000_000_000), }), ], }), Mode::Sr, ); assert_eq!( Executive::apply_extrinsic(payment).unwrap(), Err(sp_runtime::DispatchError::Other( "MlDsaHotkeyRequiresPqOwner" )) ); assert_eq!(Balances::free_balance(&victim), TaoBalance::new(0)); assert_eq!(Balances::reserved_balance(&attacker), reserved); assert_eq!(pallet_subtensor::Owner::::get(&victim), attacker); assert!(!HashedAccounts::is_protected(&victim)); assert!( pallet_hashed_accounts::EvmAliases::::get(HashedAccounts::evm_alias(&victim)) .is_none() ); }); }