diff --git a/pallets/transaction-storage/src/extension.rs b/pallets/transaction-storage/src/extension.rs index 33feade2f..cec72dfad 100644 --- a/pallets/transaction-storage/src/extension.rs +++ b/pallets/transaction-storage/src/extension.rs @@ -226,11 +226,8 @@ where // Direct storage call if let Some(inner_call) = call.is_sub_type() { let (valid_tx, maybe_scope) = Pallet::::validate_signed(&who, inner_call)?; - if let Some(ref scope) = maybe_scope { - origin.set_caller_from(Origin::::Authorized { - who: who.clone(), - scope: scope.clone(), - }); + if let Some(scope) = maybe_scope { + origin.set_caller_from(Origin::::Authorized { who: who.clone(), scope }); } return Ok((valid_tx, Some(who), origin)); } diff --git a/runtimes/bulletin-polkadot/src/lib.rs b/runtimes/bulletin-polkadot/src/lib.rs index ceb865251..a28334c93 100644 --- a/runtimes/bulletin-polkadot/src/lib.rs +++ b/runtimes/bulletin-polkadot/src/lib.rs @@ -569,9 +569,10 @@ fn extract_signer(origin: &RuntimeOrigin) -> Option { if let Some(who) = origin.as_system_origin_signer() { return Some(who.clone()); } - match origin.clone().into_caller().try_into() { - Ok(pallet_transaction_storage::pallet::Origin::::Authorized { who, .. }) => - Some(who), + match origin.caller() { + OriginCaller::TransactionStorage( + pallet_transaction_storage::pallet::Origin::::Authorized { who, .. }, + ) => Some(who.clone()), _ => None, } } diff --git a/runtimes/bulletin-westend/src/storage.rs b/runtimes/bulletin-westend/src/storage.rs index e5f9e3bcd..c7ef38fff 100644 --- a/runtimes/bulletin-westend/src/storage.rs +++ b/runtimes/bulletin-westend/src/storage.rs @@ -26,7 +26,7 @@ use frame_support::{ use frame_system::EnsureSignedBy; use pallet_transaction_storage::CallInspector; use pallet_xcm::EnsureXcm; -use pallets_common::{inspect_sudo_wrapper, inspect_utility_wrapper, NoCurrency}; +use pallets_common::{inspect_utility_wrapper, NoCurrency}; use sp_keyring::Sr25519Keyring; use sp_runtime::transaction_validity::{TransactionLongevity, TransactionPriority}; use testnet_parachains_constants::westend::locations::PeopleLocation; @@ -64,14 +64,16 @@ impl pallet_transaction_storage::CallInspector for StorageCallInspector fn inspect_wrapper(call: &RuntimeCall) -> Option> { match call { RuntimeCall::Utility(c) => inspect_utility_wrapper(c), - RuntimeCall::Sudo(c) => inspect_sudo_wrapper(c), + // Sudo is intentionally not inspected: the sudo key holder can store + // data via `sudo(store)` without authorization, as Root origin is + // accepted by `ensure_authorized`. _ => None, } } } /// Returns `true` for storage-mutating TransactionStorage calls (store, store_with_cid_config, -/// renew). Recursively inspects wrapper calls (Utility, Sudo) to prevent bypass via nesting. +/// renew). Recursively inspects wrapper calls (Utility) to prevent bypass via nesting. /// Used with `EverythingBut` as the XCM `SafeCallFilter`. impl Contains for StorageCallInspector { fn contains(call: &RuntimeCall) -> bool { diff --git a/runtimes/bulletin-westend/tests/tests.rs b/runtimes/bulletin-westend/tests/tests.rs index e688b67d4..0854ced10 100644 --- a/runtimes/bulletin-westend/tests/tests.rs +++ b/runtimes/bulletin-westend/tests/tests.rs @@ -768,17 +768,17 @@ fn wrapped_store_requires_authorization() { ); } - // sudo_as: store inside wrapper is rejected. - assert_eq!( - construct_and_apply_extrinsic( - Some(account.pair()), - RuntimeCall::Sudo(pallet_sudo::Call::sudo_as { - who: sp_runtime::MultiAddress::Id(account.to_account_id()), - call: Box::new(store_call), - }), - ), - Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + // sudo_as: passes validation (sudo not inspected) but fails at dispatch + // because no sudo key is configured in default genesis. + let sudo_as_result = construct_and_apply_extrinsic( + Some(account.pair()), + RuntimeCall::Sudo(pallet_sudo::Call::sudo_as { + who: sp_runtime::MultiAddress::Id(account.to_account_id()), + call: Box::new(store_call), + }), ); + assert!(sudo_as_result.is_ok(), "sudo_as should pass validation"); + assert!(sudo_as_result.unwrap().is_err(), "sudo_as should fail at dispatch"); }); } @@ -816,17 +816,17 @@ fn wrapped_store_with_cid_config_requires_authorization() { ); } - // sudo_as: store inside wrapper is rejected. - assert_eq!( - construct_and_apply_extrinsic( - Some(account.pair()), - RuntimeCall::Sudo(pallet_sudo::Call::sudo_as { - who: sp_runtime::MultiAddress::Id(account.to_account_id()), - call: Box::new(store_call), - }), - ), - Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + // sudo_as: passes validation (sudo not inspected) but fails at dispatch + // because no sudo key is configured in default genesis. + let sudo_as_result = construct_and_apply_extrinsic( + Some(account.pair()), + RuntimeCall::Sudo(pallet_sudo::Call::sudo_as { + who: sp_runtime::MultiAddress::Id(account.to_account_id()), + call: Box::new(store_call), + }), ); + assert!(sudo_as_result.is_ok(), "sudo_as should pass validation"); + assert!(sudo_as_result.unwrap().is_err(), "sudo_as should fail at dispatch"); }); } @@ -1085,17 +1085,17 @@ fn wrapped_renew_requires_authorization() { ); } - // sudo_as: renew inside wrapper is rejected. - assert_eq!( - construct_and_apply_extrinsic( - Some(account.pair()), - RuntimeCall::Sudo(pallet_sudo::Call::sudo_as { - who: sp_runtime::MultiAddress::Id(who), - call: Box::new(renew_call), - }), - ), - Err(TransactionValidityError::Invalid(InvalidTransaction::Call)), + // sudo_as: passes validation (sudo not inspected) but fails at dispatch + // because no sudo key is configured. + let sudo_as_result = construct_and_apply_extrinsic( + Some(account.pair()), + RuntimeCall::Sudo(pallet_sudo::Call::sudo_as { + who: sp_runtime::MultiAddress::Id(who), + call: Box::new(renew_call), + }), ); + assert!(sudo_as_result.is_ok(), "sudo_as should pass validation"); + assert!(sudo_as_result.unwrap().is_err(), "sudo_as should fail at dispatch"); }); } @@ -1328,11 +1328,38 @@ fn max_recursion_depth_is_enforced() { }); } -// NOTE: No `wrapped_call_respects_validate_inner_calls_allowlist` test on Westend. -// Unlike the feeless Polkadot solochain, Westend is a parachain with transaction fees, -// so there is no call allowlist — fees provide the spam gate for non-storage calls. -// TransactionStorage calls inside wrappers are validated for authorization by the pallet's -// `ValidateStorageCalls` extension to prevent DoS via large unauthorized data. +/// The sudo key holder can store data via `sudo(store)` without authorization. +/// Sudo dispatches with Root origin, and `ensure_authorized` accepts Root. +#[test] +fn sudo_store_works_for_sudo_key_holder() { + let mut t = RuntimeGenesisConfig::default().build_storage().unwrap(); + let sudo_account = Sr25519Keyring::Alice; + pallet_sudo::GenesisConfig:: { key: Some(sudo_account.to_account_id()) } + .assimilate_storage(&mut t) + .unwrap(); + + sp_io::TestExternalities::new(t).execute_with(|| { + advance_block(); + let who: AccountId = sudo_account.to_account_id(); + + use frame_support::traits::fungible::Mutate; + Balances::mint_into(&who, 1_000_000_000_000).unwrap(); + + let data = vec![42u8; 100]; + let store_call = + RuntimeCall::TransactionStorage(TxStorageCall::::store { data: data.clone() }); + + // sudo(store) should work — Root origin is accepted by ensure_authorized. + let sudo_call = RuntimeCall::Sudo(pallet_sudo::Call::sudo { call: Box::new(store_call) }); + assert_ok_ok(construct_and_apply_extrinsic(Some(sudo_account.pair()), sudo_call)); + + // No account authorization was needed or consumed. + assert_eq!( + TransactionStorage::account_authorization_extent(who), + AuthorizationExtent { transactions: 0, bytes: 0 }, + ); + }); +} // ============================================================================ // XCM SafeCallFilter tests — verify that storage-mutating calls are blocked