From 7f2f939d9aef0689912a97127012edd25c067fa3 Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 19 Mar 2026 12:47:05 +0100 Subject: [PATCH 1/6] Avoid cloning RuntimeOrigin in extract_signer Use origin.caller() to match on OriginCaller by reference instead of cloning the entire RuntimeOrigin via into_caller(). Only the AccountId is cloned, which is needed for the return value. --- runtimes/bulletin-polkadot/src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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, } } From 61f2e223ebc7444e9cc34a82fa60e64f16a661a4 Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 19 Mar 2026 12:52:20 +0100 Subject: [PATCH 2/6] Avoid unnecessary clone of scope in ValidateStorageCalls Move scope into Authorized origin instead of cloning it, since maybe_scope is owned and not used after this point. --- pallets/transaction-storage/src/extension.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pallets/transaction-storage/src/extension.rs b/pallets/transaction-storage/src/extension.rs index 33feade2f..08283cf5b 100644 --- a/pallets/transaction-storage/src/extension.rs +++ b/pallets/transaction-storage/src/extension.rs @@ -226,10 +226,10 @@ 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 { + if let Some(scope) = maybe_scope { origin.set_caller_from(Origin::::Authorized { who: who.clone(), - scope: scope.clone(), + scope, }); } return Ok((valid_tx, Some(who), origin)); From 9d45197f63d2d47b1e7fab5b1dd704111ffbcf3a Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 19 Mar 2026 13:13:45 +0100 Subject: [PATCH 3/6] Allow sudo(store) on bulletin-westend Remove Sudo from StorageCallInspector on Westend so the sudo key holder can store data via sudo(store) without authorization. Root origin is already accepted by ensure_authorized. Update existing sudo_as tests to reflect that sudo calls now pass validation (failing at dispatch instead when no sudo key is configured). --- pallets/transaction-storage/src/extension.rs | 5 +- runtimes/bulletin-westend/src/storage.rs | 8 +- runtimes/bulletin-westend/tests/tests.rs | 93 +++++++++++++------- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/pallets/transaction-storage/src/extension.rs b/pallets/transaction-storage/src/extension.rs index 08283cf5b..cec72dfad 100644 --- a/pallets/transaction-storage/src/extension.rs +++ b/pallets/transaction-storage/src/extension.rs @@ -227,10 +227,7 @@ where if let Some(inner_call) = call.is_sub_type() { let (valid_tx, maybe_scope) = Pallet::::validate_signed(&who, inner_call)?; if let Some(scope) = maybe_scope { - origin.set_caller_from(Origin::::Authorized { - who: who.clone(), - scope, - }); + origin.set_caller_from(Origin::::Authorized { who: who.clone(), scope }); } return Ok((valid_tx, Some(who), origin)); } 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 caa66f5f8..2b1f3dc8f 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,6 +1328,39 @@ fn max_recursion_depth_is_enforced() { }); } +/// 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 }, + ); + }); +} + // 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. From 60db6cc9dfcf6e0149bb46954c6e81f59f429f1d Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 19 Mar 2026 13:15:02 +0100 Subject: [PATCH 4/6] nit --- runtimes/bulletin-westend/tests/tests.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/runtimes/bulletin-westend/tests/tests.rs b/runtimes/bulletin-westend/tests/tests.rs index 2b1f3dc8f..3e76a7fa4 100644 --- a/runtimes/bulletin-westend/tests/tests.rs +++ b/runtimes/bulletin-westend/tests/tests.rs @@ -1085,17 +1085,17 @@ fn wrapped_renew_requires_authorization() { ); } - // 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), - }), + // 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)), ); - assert!(sudo_as_result.is_ok(), "sudo_as should pass validation"); - assert!(sudo_as_result.unwrap().is_err(), "sudo_as should fail at dispatch"); }); } From c0fc6bff544964f70eab41227967cbd7a000f895 Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 19 Mar 2026 13:16:22 +0100 Subject: [PATCH 5/6] nit --- runtimes/bulletin-westend/tests/tests.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/runtimes/bulletin-westend/tests/tests.rs b/runtimes/bulletin-westend/tests/tests.rs index 3e76a7fa4..2b1f3dc8f 100644 --- a/runtimes/bulletin-westend/tests/tests.rs +++ b/runtimes/bulletin-westend/tests/tests.rs @@ -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"); }); } From fe76ccd2859adb56f5f56a87cc57f635136b1c1f Mon Sep 17 00:00:00 2001 From: Branislav Kontur Date: Thu, 19 Mar 2026 13:18:15 +0100 Subject: [PATCH 6/6] Remove stale test note about Westend call allowlist --- runtimes/bulletin-westend/tests/tests.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/runtimes/bulletin-westend/tests/tests.rs b/runtimes/bulletin-westend/tests/tests.rs index 2b1f3dc8f..a9aaef01d 100644 --- a/runtimes/bulletin-westend/tests/tests.rs +++ b/runtimes/bulletin-westend/tests/tests.rs @@ -1361,8 +1361,3 @@ fn sudo_store_works_for_sudo_key_holder() { }); } -// 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.