Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions pallets/transaction-storage/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,8 @@ where
// Direct storage call
if let Some(inner_call) = call.is_sub_type() {
let (valid_tx, maybe_scope) = Pallet::<T>::validate_signed(&who, inner_call)?;
if let Some(ref scope) = maybe_scope {
origin.set_caller_from(Origin::<T>::Authorized {
who: who.clone(),
scope: scope.clone(),
});
if let Some(scope) = maybe_scope {
origin.set_caller_from(Origin::<T>::Authorized { who: who.clone(), scope });
}
return Ok((valid_tx, Some(who), origin));
}
Expand Down
7 changes: 4 additions & 3 deletions runtimes/bulletin-polkadot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,10 @@ fn extract_signer(origin: &RuntimeOrigin) -> Option<AccountId> {
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::<Runtime>::Authorized { who, .. }) =>
Some(who),
match origin.caller() {
OriginCaller::TransactionStorage(
pallet_transaction_storage::pallet::Origin::<Runtime>::Authorized { who, .. },
) => Some(who.clone()),
_ => None,
}
}
Expand Down
8 changes: 5 additions & 3 deletions runtimes/bulletin-westend/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,14 +64,16 @@ impl pallet_transaction_storage::CallInspector<Runtime> for StorageCallInspector
fn inspect_wrapper(call: &RuntimeCall) -> Option<Vec<&RuntimeCall>> {
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<RuntimeCall> for StorageCallInspector {
fn contains(call: &RuntimeCall) -> bool {
Expand Down
97 changes: 62 additions & 35 deletions runtimes/bulletin-westend/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
}

Expand Down Expand Up @@ -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");
});
}

Expand Down Expand Up @@ -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");
});
}

Expand Down Expand Up @@ -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::<Runtime> { 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::<Runtime>::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
Expand Down