Skip to content
Open
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
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pallet-sudo = { version = "47.0.0", default-features = false }
pallet-timestamp = { version = "46.0.0", default-features = false }
pallet-transaction-payment = { version = "47.0.0", default-features = false }
pallet-transaction-payment-rpc-runtime-api = { version = "47.0.0", default-features = false }
pallet-tx-pause = { version = "28.0.0", default-features = false }

pallet-xcm = { version = "27.0.0", default-features = false }
xcm = { version = "23.0.0", package = "staging-xcm", default-features = false }
Expand Down
3 changes: 2 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ test-zombienet-auto-renew runtime="westend" group="all":
auto_renew_storage::parachain_auto_renew_test \
auto_renew_storage::parachain_auto_renew_many_items_test \
auto_renew_storage::parachain_auto_renew_quota_exhaustion_test \
auto_renew_storage::parachain_auto_renew_authorization_expires_mid_cycle_test)
auto_renew_storage::parachain_auto_renew_authorization_expires_mid_cycle_test \
auto_renew_storage::parachain_pause_renew_filters_dispatch_test)
;;
pruning)
filter_args=(--exact \
Expand Down
4 changes: 4 additions & 0 deletions pallets/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,31 @@ polkadot-sdk-frame = { workspace = true, default-features = false, features = [
] }
pallet-proxy = { workspace = true }
pallet-sudo = { workspace = true }
pallet-tx-pause = { workspace = true }
pallet-utility = { workspace = true }

[features]
default = ["std"]
runtime-benchmarks = [
"pallet-proxy/runtime-benchmarks",
"pallet-sudo/runtime-benchmarks",
"pallet-tx-pause/runtime-benchmarks",
"pallet-utility/runtime-benchmarks",
"polkadot-sdk-frame/runtime-benchmarks",
]
std = [
"codec/std",
"pallet-proxy/std",
"pallet-sudo/std",
"pallet-tx-pause/std",
"pallet-utility/std",
"polkadot-sdk-frame/std",
"scale-info/std",
]
try-runtime = [
"pallet-proxy/try-runtime",
"pallet-sudo/try-runtime",
"pallet-tx-pause/try-runtime",
"pallet-utility/try-runtime",
"polkadot-sdk-frame/try-runtime",
]
23 changes: 23 additions & 0 deletions pallets/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,29 @@ pub fn proxy_inner_calls<T: pallet_proxy::Config>(
}
}

/// `WhitelistedCalls` impl that inverts a runtime-supplied "pausable" set.
pub struct TxPauseWhitelist<T, Pausable>(core::marker::PhantomData<(T, Pausable)>);

impl<T, Pausable> frame_support::traits::Contains<pallet_tx_pause::RuntimeCallNameOf<T>>
for TxPauseWhitelist<T, Pausable>
where
T: pallet_tx_pause::Config,
Pausable: frame_support::traits::Contains<pallet_tx_pause::RuntimeCallNameOf<T>>,
{
fn contains(name: &pallet_tx_pause::RuntimeCallNameOf<T>) -> bool {
// Benches need every call pausable to exercise `pause`/`unpause`.
#[cfg(feature = "runtime-benchmarks")]
{
let _ = name;
false
}
#[cfg(not(feature = "runtime-benchmarks"))]
{
!Pausable::contains(name)
}
}
}

/// Extract inner calls from a sudo call variant.
pub fn sudo_inner_calls<T: pallet_sudo::Config>(
call: &pallet_sudo::Call<T>,
Expand Down
4 changes: 4 additions & 0 deletions runtimes/bulletin-paseo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pallet-session = { workspace = true }
pallet-skip-feeless-payment = { workspace = true }
pallet-sudo = { workspace = true }
pallet-timestamp = { workspace = true }
pallet-tx-pause = { workspace = true }
pallet-utility = { workspace = true }
pallet-transaction-payment = { workspace = true }
pallet-transaction-payment-rpc-runtime-api = { workspace = true }
Expand Down Expand Up @@ -132,6 +133,7 @@ std = [
"pallet-timestamp/std",
"pallet-transaction-payment-rpc-runtime-api/std",
"pallet-transaction-payment/std",
"pallet-tx-pause/std",
"pallet-utility/std",
"pallet-xcm-benchmarks?/std",
"pallet-xcm/std",
Expand Down Expand Up @@ -188,6 +190,7 @@ runtime-benchmarks = [
"pallet-sudo/runtime-benchmarks",
"pallet-timestamp/runtime-benchmarks",
"pallet-transaction-payment/runtime-benchmarks",
"pallet-tx-pause/runtime-benchmarks",
"pallet-utility/runtime-benchmarks",
"pallet-xcm-benchmarks/runtime-benchmarks",
"pallet-xcm/runtime-benchmarks",
Expand Down Expand Up @@ -224,6 +227,7 @@ try-runtime = [
"pallet-sudo/try-runtime",
"pallet-timestamp/try-runtime",
"pallet-transaction-payment/try-runtime",
"pallet-tx-pause/try-runtime",
"pallet-utility/try-runtime",
"pallet-xcm/try-runtime",
"parachain-info/try-runtime",
Expand Down
47 changes: 47 additions & 0 deletions runtimes/bulletin-paseo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ parameter_types! {
// Configure FRAME pallets to include in runtime.
#[derive_impl(frame_system::config_preludes::ParaChainDefaultConfig)]
impl frame_system::Config for Runtime {
type BaseCallFilter = TxPause;
/// The identifier used to distinguish between accounts.
type AccountId = AccountId;
/// The nonce type for storing how many extrinsics an account has signed.
Expand Down Expand Up @@ -541,6 +542,49 @@ impl pallet_utility::Config for Runtime {
type WeightInfo = weights::pallet_utility::WeightInfo<Runtime>;
}

parameter_types! {
// Bounds the byte length of pallet and call names tracked in `pallet_tx_pause::PausedCalls`.
// Names exceeding this length cannot be encoded into `RuntimeCallNameOf` and are therefore
// treated as paused by the pallet's `Contains<RuntimeCall>` filter.
pub const TxPauseMaxNameLen: u32 = 256;
}

/// Calls pausable via `pallet_tx_pause`. Names derive from typed variants so a
/// rename in the storage pallet is a compile error here.
pub struct PausableCalls;
impl frame_support::traits::Contains<pallet_tx_pause::RuntimeCallNameOf<Runtime>>
for PausableCalls
{
fn contains(name: &pallet_tx_pause::RuntimeCallNameOf<Runtime>) -> bool {
use frame_support::traits::{GetCallName, PalletInfoAccess};
use pallet_bulletin_transaction_storage::{Call as TxStorageCall, TransactionRef};
if name.0.as_slice() != TransactionStorage::name().as_bytes() {
return false;
}
let entry: TransactionRef<BlockNumber> = TransactionRef::ContentHash([0u8; 32]);
let zero_hash = [0u8; 32];
let pausable = [
TxStorageCall::<Runtime>::renew { entry: entry.clone() }.get_call_name(),
TxStorageCall::<Runtime>::force_renew { entry }.get_call_name(),
TxStorageCall::<Runtime>::enable_auto_renew { content_hash: zero_hash }.get_call_name(),
TxStorageCall::<Runtime>::disable_auto_renew { content_hash: zero_hash }
.get_call_name(),
];
let call_name = name.1.as_slice();
pausable.iter().any(|n| n.as_bytes() == call_name)
}
}

impl pallet_tx_pause::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type RuntimeCall = RuntimeCall;
type PauseOrigin = EnsureRoot<AccountId>;
type UnpauseOrigin = EnsureRoot<AccountId>;
type WhitelistedCalls = bulletin_pallets_common::TxPauseWhitelist<Runtime, PausableCalls>;
type MaxNameLen = TxPauseMaxNameLen;
type WeightInfo = pallet_tx_pause::weights::SubstrateWeight<Runtime>;
}

impl<C> frame_system::offchain::CreateTransactionBase<C> for Runtime
where
RuntimeCall: From<C>,
Expand Down Expand Up @@ -632,6 +676,8 @@ mod runtime {
pub type Utility = pallet_utility;
#[runtime::pallet_index(7)]
pub type MultiBlockMigrations = pallet_migrations;
#[runtime::pallet_index(8)]
pub type TxPause = pallet_tx_pause;

// Monetary stuff.
#[runtime::pallet_index(10)]
Expand Down Expand Up @@ -695,6 +741,7 @@ mod benches {
[pallet_xcm_benchmarks::generic, XcmGeneric]
[cumulus_pallet_weight_reclaim, WeightReclaim]
[pallet_utility, Utility]
[pallet_tx_pause, TxPause]
);
}

Expand Down
149 changes: 147 additions & 2 deletions runtimes/bulletin-paseo/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ use bulletin_paseo_runtime::{
xcm_config::{GovernanceLocation, LocationToAccountId},
AllPalletsWithoutSystem, Balances, Block, HopPromotion, Runtime, RuntimeCall, RuntimeEvent,
RuntimeGenesisConfig, RuntimeOrigin, SessionKeys, System, TransactionStorage, TxExtension,
UncheckedExtrinsic,
TxPauseMaxNameLen, UncheckedExtrinsic,
};
use bulletin_transaction_storage_primitives::cids::{calculate_cid, CidConfig, HashingAlgorithm};
use frame_support::{
assert_err, assert_ok, dispatch::GetDispatchInfo, pallet_prelude::Hooks, traits::Get,
assert_err, assert_noop, assert_ok,
dispatch::GetDispatchInfo,
pallet_prelude::Hooks,
traits::{Contains, Get, GetCallMetadata},
BoundedVec,
};
use pallet_bulletin_transaction_storage::{
extension::{AllowanceBasedPriority, ALLOWANCE_PRIORITY_BOOST},
AuthorizationExtent, AuthorizationScope, Call as TxStorageCall, Config as TxStorageConfig,
Origin as TxStorageOrigin, TransactionRef,
};
use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf};
use parachains_common::{AccountId, AuraId, BlockNumber, Hash as PcHash, Signature as PcSignature};
use parachains_runtimes_test_utils::{ExtBuilder, GovernanceOrigin, RuntimeHelper};
use sp_core::{crypto::Ss58Codec, Encode, Pair};
Expand Down Expand Up @@ -2110,3 +2115,143 @@ fn non_sudo_cannot_add_authorizer() {
assert_eq!(applied.map(|_| ()), Err(DispatchError::BadOrigin));
});
}

type TxPauseWhitelist =
bulletin_pallets_common::TxPauseWhitelist<Runtime, bulletin_paseo_runtime::PausableCalls>;

const RENEW_CALLS: &[&[u8]] =
&[b"renew", b"force_renew", b"enable_auto_renew", b"disable_auto_renew"];

fn pause_call_name(pallet: &[u8], call: &[u8]) -> RuntimeCallNameOf<Runtime> {
let max = TxPauseMaxNameLen::get() as usize;
(
BoundedVec::try_from(pallet.to_vec())
.unwrap_or_else(|_| panic!("pallet name '{pallet:?}' > MaxNameLen={max}")),
BoundedVec::try_from(call.to_vec())
.unwrap_or_else(|_| panic!("call name '{call:?}' > MaxNameLen={max}")),
)
}

fn sample_renew_call() -> RuntimeCall {
RuntimeCall::TransactionStorage(TxStorageCall::<Runtime>::renew {
entry: TransactionRef::Position { block: 0, index: 0 },
})
}

#[test]
fn tx_pause_renew_call_metadata_fits_in_max_name_len() {
let max = TxPauseMaxNameLen::get() as usize;
let call = sample_renew_call();
let meta = call.get_call_metadata();
assert!(meta.pallet_name.len() <= max, "pallet '{}' > MaxNameLen", meta.pallet_name);
assert!(meta.function_name.len() <= max, "call '{}' > MaxNameLen", meta.function_name);
assert_eq!(meta.pallet_name, "TransactionStorage");
for call in RENEW_CALLS {
assert!(call.len() <= max);
}
}

#[test]
fn tx_pause_whitelist_blocks_every_non_renew_call() {
for pallet in [&b"System"[..], b"Sudo", b"Balances", b"TxPause", b"PolkadotXcm", b"Utility"] {
assert!(
TxPauseWhitelist::contains(&pause_call_name(pallet, b"some_call")),
"pallet {:?} must be whitelisted",
core::str::from_utf8(pallet).unwrap()
);
}
for call in [&b"store"[..], b"add_authorizer", b"apply_block_inherents"] {
assert!(
TxPauseWhitelist::contains(&pause_call_name(b"TransactionStorage", call)),
"TransactionStorage.{} must be whitelisted",
core::str::from_utf8(call).unwrap()
);
}
}

#[test]
fn tx_pause_whitelist_allows_renew_family_to_be_paused() {
for call in RENEW_CALLS {
assert!(
!TxPauseWhitelist::contains(&pause_call_name(b"TransactionStorage", call)),
"TransactionStorage.{} must be pausable",
core::str::from_utf8(call).unwrap()
);
}
}

#[test]
fn tx_pause_pause_renew_via_root_filters_dispatch() {
sp_io::TestExternalities::new(RuntimeGenesisConfig::default().build_storage().unwrap())
.execute_with(|| {
let call = sample_renew_call();
assert!(
<Runtime as frame_system::Config>::BaseCallFilter::contains(&call),
"renew should dispatch when not paused"
);

assert_ok!(pallet_tx_pause::Pallet::<Runtime>::pause(
RuntimeOrigin::root(),
pause_call_name(b"TransactionStorage", b"renew"),
));

assert!(
!<Runtime as frame_system::Config>::BaseCallFilter::contains(&call),
"BaseCallFilter must reject renew after pause"
);

assert_ok!(pallet_tx_pause::Pallet::<Runtime>::unpause(
RuntimeOrigin::root(),
pause_call_name(b"TransactionStorage", b"renew"),
));
assert!(<Runtime as frame_system::Config>::BaseCallFilter::contains(&call));
});
}

#[test]
fn tx_pause_pause_non_renew_is_rejected_as_unpausable() {
sp_io::TestExternalities::new(RuntimeGenesisConfig::default().build_storage().unwrap())
.execute_with(|| {
for (pallet, call) in [
(&b"System"[..], &b"set_storage"[..]),
(b"Sudo", b"sudo"),
(b"Balances", b"transfer_keep_alive"),
(b"TransactionStorage", b"store"),
] {
assert_noop!(
pallet_tx_pause::Pallet::<Runtime>::pause(
RuntimeOrigin::root(),
pause_call_name(pallet, call),
),
TxPauseError::<Runtime>::Unpausable
);
}
});
}

#[test]
fn tx_pause_pause_requires_root_origin() {
sp_io::TestExternalities::new(RuntimeGenesisConfig::default().build_storage().unwrap())
.execute_with(|| {
let signed: RuntimeOrigin =
frame_system::RawOrigin::Signed(AccountId::new([1u8; 32])).into();
assert!(pallet_tx_pause::Pallet::<Runtime>::pause(
signed,
pause_call_name(b"TransactionStorage", b"renew"),
)
.is_err());
});
}

#[test]
fn tx_pause_paused_call_name_round_trip_fits_storage() {
// Belt-and-braces: encode a `RuntimeCallNameOf` for every renew variant so the
// `BoundedVec<u8, MaxNameLen>` conversion can't silently regress if MaxNameLen shrinks.
let max = TxPauseMaxNameLen::get() as usize;
for call in RENEW_CALLS {
let key = pause_call_name(b"TransactionStorage", call);
let encoded = key.encode();
assert!(!encoded.is_empty());
assert!(call.len() <= max);
}
}
Loading
Loading