Skip to content

Commit 3e53570

Browse files
Add StakingOperator proxy type to Westend AssetHub (#10980)
Introduces StakingOperator proxy type that allows validator operational tasks (validate, chill, kick) and session key management (set_keys, purge_keys) without access to fund management operations. This enables pure proxy stashes to delegate validator operations: now that pallet_staking_async_rc_client provides set_keys/purge_keys on AssetHub, pure proxies can fully utilize StakingOperator. *NOTE**: This is similar to polkadot-fellows/runtimes#1033, which introduced StakingOperator on Polkadot and Kusama. That change predated the introduction of session key handling on AssetHub. Now that session key handling is available, a follow-up PR will be implemented in the runtimes repository to restrict StakingOperator solely to AssetHub and enable session key handling via StakingOperator on AssetHub. --------- Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 89669b3 commit 3e53570

File tree

3 files changed

+211
-3
lines changed

3 files changed

+211
-3
lines changed

cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,13 @@ pub enum ProxyType {
708708
OldAuction,
709709
/// Placeholder variant to track the state before the Asset Hub Migration.
710710
OldParaRegistration,
711+
/// Operator proxy for validators. Can perform operational tasks: validating, chilling,
712+
/// kicking, and managing session keys. Cannot bond/unbond funds, change reward
713+
/// destinations, or nominate.
714+
///
715+
/// Contains `Staking` (validate, chill, kick), `StakingRcClient` (set_keys, purge_keys),
716+
/// and `Utility` pallets.
717+
StakingOperator,
711718
}
712719
impl Default for ProxyType {
713720
fn default() -> Self {
@@ -843,12 +850,30 @@ impl InstanceFilter<RuntimeCall> for ProxyType {
843850
RuntimeCall::Session(..) |
844851
RuntimeCall::Utility(..) |
845852
RuntimeCall::NominationPools(..) |
846-
RuntimeCall::VoterList(..)
853+
RuntimeCall::VoterList(..) |
854+
RuntimeCall::Proxy(pallet_proxy::Call::add_proxy {
855+
proxy_type: ProxyType::StakingOperator,
856+
..
857+
}) | RuntimeCall::Proxy(pallet_proxy::Call::remove_proxy {
858+
proxy_type: ProxyType::StakingOperator,
859+
..
860+
})
847861
)
848862
},
849863
ProxyType::NominationPools => {
850864
matches!(c, RuntimeCall::NominationPools(..) | RuntimeCall::Utility(..))
851865
},
866+
ProxyType::StakingOperator => matches!(
867+
c,
868+
RuntimeCall::Staking(pallet_staking_async::Call::validate { .. }) |
869+
RuntimeCall::Staking(pallet_staking_async::Call::chill { .. }) |
870+
RuntimeCall::Staking(pallet_staking_async::Call::kick { .. }) |
871+
RuntimeCall::StakingRcClient(
872+
pallet_staking_async_rc_client::Call::set_keys { .. }
873+
) | RuntimeCall::StakingRcClient(
874+
pallet_staking_async_rc_client::Call::purge_keys { .. }
875+
) | RuntimeCall::Utility { .. }
876+
),
852877
}
853878
}
854879

@@ -859,12 +884,14 @@ impl InstanceFilter<RuntimeCall> for ProxyType {
859884
(_, ProxyType::Any) => false,
860885
(ProxyType::Assets, ProxyType::AssetOwner) => true,
861886
(ProxyType::Assets, ProxyType::AssetManager) => true,
887+
(ProxyType::Staking, ProxyType::StakingOperator) => true,
862888
(
863889
ProxyType::NonTransfer,
864890
ProxyType::Collator |
865891
ProxyType::Governance |
866892
ProxyType::Staking |
867-
ProxyType::NominationPools,
893+
ProxyType::NominationPools |
894+
ProxyType::StakingOperator,
868895
) => true,
869896
_ => false,
870897
}

cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use asset_hub_westend_runtime::{
3030
},
3131
AllPalletsWithoutSystem, Assets, Balances, Block, ExistentialDeposit, ForeignAssets,
3232
ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem,
33-
PolkadotXcm, Revive, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys,
33+
PolkadotXcm, Proxy, Revive, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys,
3434
ToRococoXcmRouterInstance, TrustBackedAssetsInstance, Uniques, WeightToFee, XcmpQueue,
3535
};
3636
pub use asset_hub_westend_runtime::{AssetConversion, AssetDeposit, CollatorSelection, System};
@@ -2090,3 +2090,173 @@ fn session_keys_are_compatible_between_ah_and_rc() {
20902090
"Session key type IDs must match between AssetHub and Westend"
20912091
);
20922092
}
2093+
2094+
#[test]
2095+
fn staking_proxy_can_manage_staking_operator() {
2096+
use asset_hub_westend_runtime::ProxyType;
2097+
use frame_support::traits::InstanceFilter;
2098+
2099+
// GIVEN: Staking proxy type
2100+
let staking_proxy = ProxyType::Staking;
2101+
2102+
// WHEN: checking if Staking can add/remove StakingOperator proxies
2103+
let add_call = RuntimeCall::Proxy(pallet_proxy::Call::add_proxy {
2104+
delegate: AccountId::from(BOB).into(),
2105+
proxy_type: ProxyType::StakingOperator,
2106+
delay: 0,
2107+
});
2108+
let remove_call = RuntimeCall::Proxy(pallet_proxy::Call::remove_proxy {
2109+
delegate: AccountId::from(BOB).into(),
2110+
proxy_type: ProxyType::StakingOperator,
2111+
delay: 0,
2112+
});
2113+
2114+
// THEN: Staking proxy can manage StakingOperator proxies and is its superset
2115+
assert!(staking_proxy.filter(&add_call));
2116+
assert!(staking_proxy.filter(&remove_call));
2117+
assert!(staking_proxy.is_superset(&ProxyType::StakingOperator));
2118+
}
2119+
2120+
/// Verifies StakingOperator filter allows validator operations and session key management,
2121+
/// but forbids fund management.
2122+
#[test]
2123+
fn staking_operator_filter_allows_validator_ops_and_session_keys() {
2124+
use asset_hub_westend_runtime::ProxyType;
2125+
use frame_support::traits::InstanceFilter;
2126+
use pallet_staking_async::{Call as StakingCall, RewardDestination, ValidatorPrefs};
2127+
use pallet_staking_async_rc_client::Call as RcClientCall;
2128+
2129+
let operator = ProxyType::StakingOperator;
2130+
2131+
// StakingOperator can perform validator operations
2132+
assert!(operator
2133+
.filter(&RuntimeCall::Staking(StakingCall::validate { prefs: ValidatorPrefs::default() })));
2134+
assert!(operator.filter(&RuntimeCall::Staking(StakingCall::chill {})));
2135+
assert!(operator.filter(&RuntimeCall::Staking(StakingCall::kick { who: vec![] })));
2136+
2137+
// StakingOperator can manage session keys
2138+
assert!(operator.filter(&RuntimeCall::StakingRcClient(RcClientCall::set_keys {
2139+
keys: Default::default(),
2140+
proof: Default::default(),
2141+
max_delivery_and_remote_execution_fee: None,
2142+
})));
2143+
assert!(operator.filter(&RuntimeCall::StakingRcClient(RcClientCall::purge_keys {
2144+
max_delivery_and_remote_execution_fee: None,
2145+
})));
2146+
2147+
// StakingOperator can batch operations
2148+
assert!(operator.filter(&RuntimeCall::Utility(pallet_utility::Call::batch { calls: vec![] })));
2149+
2150+
// StakingOperator cannot manage funds or nominations
2151+
assert!(!operator.filter(&RuntimeCall::Staking(StakingCall::bond {
2152+
value: 100,
2153+
payee: RewardDestination::Staked
2154+
})));
2155+
assert!(!operator.filter(&RuntimeCall::Staking(StakingCall::unbond { value: 100 })));
2156+
assert!(!operator.filter(&RuntimeCall::Staking(StakingCall::nominate { targets: vec![] })));
2157+
assert!(!operator.filter(&RuntimeCall::Staking(StakingCall::set_payee {
2158+
payee: RewardDestination::Staked
2159+
})));
2160+
}
2161+
2162+
/// Test that a pure proxy stash can delegate to a StakingOperator
2163+
/// who can then call validate, chill, and manage session keys.
2164+
#[test]
2165+
fn pure_proxy_stash_can_delegate_to_staking_operator() {
2166+
use asset_hub_westend_runtime::ProxyType;
2167+
2168+
let controller: AccountId = ALICE.into();
2169+
let operator: AccountId = BOB.into();
2170+
2171+
ExtBuilder::<Runtime>::default()
2172+
.with_collators(vec![AccountId::from(ALICE)])
2173+
.with_session_keys(vec![(
2174+
AccountId::from(ALICE),
2175+
AccountId::from(ALICE),
2176+
SessionKeys { aura: AuraId::from(sp_core::sr25519::Public::from_raw(ALICE)) },
2177+
)])
2178+
.build()
2179+
.execute_with(|| {
2180+
// GIVEN: fund controller and operator
2181+
assert_ok!(Balances::mint_into(&controller, 100 * UNITS));
2182+
assert_ok!(Balances::mint_into(&operator, 100 * UNITS));
2183+
2184+
// WHEN: controller creates a pure proxy stash with Staking proxy type
2185+
assert_ok!(Proxy::create_pure(
2186+
RuntimeOrigin::signed(controller.clone()),
2187+
ProxyType::Staking,
2188+
0,
2189+
0
2190+
));
2191+
let pure_stash = Proxy::pure_account(&controller, &ProxyType::Staking, 0, None);
2192+
2193+
// Fund the pure proxy stash
2194+
assert_ok!(Balances::mint_into(&pure_stash, 100 * UNITS));
2195+
2196+
// WHEN: controller (via Staking proxy) adds StakingOperator proxy for the operator
2197+
let add_operator_call = RuntimeCall::Proxy(pallet_proxy::Call::add_proxy {
2198+
delegate: operator.clone().into(),
2199+
proxy_type: ProxyType::StakingOperator,
2200+
delay: 0,
2201+
});
2202+
assert_ok!(Proxy::proxy(
2203+
RuntimeOrigin::signed(controller.clone()),
2204+
pure_stash.clone().into(),
2205+
None,
2206+
Box::new(add_operator_call),
2207+
));
2208+
2209+
// THEN: operator can call chill on behalf of pure proxy stash
2210+
let chill_call = RuntimeCall::Staking(pallet_staking_async::Call::chill {});
2211+
assert_ok!(Proxy::proxy(
2212+
RuntimeOrigin::signed(operator.clone()),
2213+
pure_stash.clone().into(),
2214+
None,
2215+
Box::new(chill_call),
2216+
));
2217+
2218+
// THEN: operator can call validate on behalf of pure proxy stash
2219+
let validate_call = RuntimeCall::Staking(pallet_staking_async::Call::validate {
2220+
prefs: Default::default(),
2221+
});
2222+
assert_ok!(Proxy::proxy(
2223+
RuntimeOrigin::signed(operator.clone()),
2224+
pure_stash.clone().into(),
2225+
None,
2226+
Box::new(validate_call),
2227+
));
2228+
2229+
// THEN: operator can call purge_keys (session key management on AssetHub)
2230+
let purge_keys_call =
2231+
RuntimeCall::StakingRcClient(pallet_staking_async_rc_client::Call::purge_keys {
2232+
max_delivery_and_remote_execution_fee: None,
2233+
});
2234+
assert_ok!(Proxy::proxy(
2235+
RuntimeOrigin::signed(operator.clone()),
2236+
pure_stash.clone().into(),
2237+
None,
2238+
Box::new(purge_keys_call),
2239+
));
2240+
2241+
// THEN: operator CANNOT call bond (fund management is forbidden)
2242+
// Note: Proxy::proxy returns Ok(()) even when the proxied call fails due to filter.
2243+
// The actual result is emitted as a ProxyExecuted event.
2244+
let bond_call = RuntimeCall::Staking(pallet_staking_async::Call::bond {
2245+
value: 10 * UNITS,
2246+
payee: pallet_staking_async::RewardDestination::Staked,
2247+
});
2248+
assert_ok!(Proxy::proxy(
2249+
RuntimeOrigin::signed(operator.clone()),
2250+
pure_stash.clone().into(),
2251+
None,
2252+
Box::new(bond_call),
2253+
));
2254+
// Check that the proxied call failed due to filter (CallFiltered error)
2255+
System::assert_last_event(
2256+
pallet_proxy::Event::ProxyExecuted {
2257+
result: Err(frame_system::Error::<Runtime>::CallFiltered.into()),
2258+
}
2259+
.into(),
2260+
);
2261+
});
2262+
}

prdoc/pr_10980.prdoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
title: Add StakingOperator proxy type to Westend AssetHub
2+
doc:
3+
- audience: Runtime Dev
4+
description: |-
5+
Introduces StakingOperator proxy type that allows validator operational tasks (validate, chill, kick) and session key management (set_keys, purge_keys) without access to fund management operations.
6+
7+
This enables pure proxy stashes to delegate validator operations: now that pallet_staking_async_rc_client provides
8+
set_keys/purge_keys on AssetHub, pure proxies can fully utilize StakingOperator.
9+
crates:
10+
- name: asset-hub-westend-runtime
11+
bump: major

0 commit comments

Comments
 (0)