diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs index 8c2409f61a402..16993522e087e 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs @@ -332,6 +332,7 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; type RelayChainSessionKeys = RelayChainSessionKeys; type Balance = Balance; + type MinSetKeysBond = ConstU128<{ 10 * UNITS }>; // | Key | Crypto | Public Key | Signature | // |---------------------|---------|------------|-----------| // | grandpa | Ed25519 | 32 bytes | 64 bytes | diff --git a/prdoc/pr_11168.prdoc b/prdoc/pr_11168.prdoc new file mode 100644 index 0000000000000..4e446a0f5dee4 --- /dev/null +++ b/prdoc/pr_11168.prdoc @@ -0,0 +1,16 @@ +title: Add MinSetKeysBond check in rc_client::set_keys +doc: +- audience: Runtime Dev + description: |- + Add a configurable MinSetKeysBond threshold (hardcoded to 10 WND on asset-hub-westend) that rejects set_keys when active bond + is insufficient. Set to 0 to disable. +crates: +- name: asset-hub-westend-runtime + bump: minor + validate: false +- name: pallet-staking-async-rc-client + bump: minor + validate: false +- name: pallet-staking-async + bump: minor + validate: false diff --git a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs index 7ac54266b506a..06200a5dbc566 100644 --- a/substrate/frame/staking-async/ahm-test/src/ah/mock.rs +++ b/substrate/frame/staking-async/ahm-test/src/ah/mock.rs @@ -473,6 +473,10 @@ frame::deps::sp_runtime::impl_opaque_keys! { } } +parameter_types! { + pub static MinSetKeysBond: Balance = 0; +} + impl pallet_staking_async_rc_client::Config for Runtime { type AHStakingInterface = Staking; type SendToRelayChain = DeliverToRelay; @@ -482,6 +486,7 @@ impl pallet_staking_async_rc_client::Config for Runtime { type RelayChainSessionKeys = RCSessionKeys; type Balance = Balance; type MaxSessionKeysLength = ConstU32<256>; + type MinSetKeysBond = MinSetKeysBond; type WeightInfo = (); } diff --git a/substrate/frame/staking-async/ahm-test/src/ah/test.rs b/substrate/frame/staking-async/ahm-test/src/ah/test.rs index dc6b4405da68c..989a550f7293e 100644 --- a/substrate/frame/staking-async/ahm-test/src/ah/test.rs +++ b/substrate/frame/staking-async/ahm-test/src/ah/test.rs @@ -1082,7 +1082,7 @@ fn era_lifecycle_test() { mod session_keys { use super::*; use crate::ah::mock::{ - Balances, LocalQueue, OutgoingMessages, ProxyType, PurgeKeysExecutionCost, + Balances, LocalQueue, MinSetKeysBond, OutgoingMessages, ProxyType, PurgeKeysExecutionCost, SetKeysExecutionCost, }; use frame_support::{assert_noop, BoundedVec}; @@ -1458,6 +1458,55 @@ mod session_keys { }); } + #[test] + fn set_keys_insufficient_bond() { + ExtBuilder::default().local_queue().build().execute_with(|| { + let validator: AccountId = 1; + let keys = make_session_keys(); + + // GIVEN: MinSetKeysBond is set higher than the validator's active bond (100) + MinSetKeysBond::set(101); + + // WHEN: Validator tries to set keys + // THEN: InsufficientBond error is returned + assert_noop!( + rc_client::Pallet::::set_keys( + RuntimeOrigin::signed(validator), + keys.clone(), + None, + ), + rc_client::Error::::InsufficientBond + ); + + // GIVEN: MinSetKeysBond equals the validator's active bond + MinSetKeysBond::set(100); + + // WHEN: Validator sets keys with exact bond + // THEN: Succeeds + assert_ok!(rc_client::Pallet::::set_keys( + RuntimeOrigin::signed(validator), + keys.clone(), + None, + )); + }); + } + + #[test] + fn set_keys_min_bond_zero_disables_check() { + ExtBuilder::default().local_queue().build().execute_with(|| { + // GIVEN: MinSetKeysBond is 0 (default in tests) — check is disabled + let validator: AccountId = 1; + let keys = make_session_keys(); + + // WHEN/THEN: set_keys succeeds regardless of bond amount + assert_ok!(rc_client::Pallet::::set_keys( + RuntimeOrigin::signed(validator), + keys, + None, + )); + }); + } + /// End-to-end test: set keys on AssetHub, verify on RelayChain, then purge and verify. #[test] fn set_and_purge_keys_e2e() { diff --git a/substrate/frame/staking-async/rc-client/src/lib.rs b/substrate/frame/staking-async/rc-client/src/lib.rs index 214d4fcf30274..65daa8270eb96 100644 --- a/substrate/frame/staking-async/rc-client/src/lib.rs +++ b/substrate/frame/staking-async/rc-client/src/lib.rs @@ -831,6 +831,8 @@ where pub trait AHStakingInterface { /// The validator account id type. type AccountId; + /// The balance type. + type Balance: BalanceTrait; /// Maximum number of validators that the staking system may have. type MaxValidatorSet: Get; @@ -864,6 +866,9 @@ pub trait AHStakingInterface { /// /// Returns true if the account has called `validate()` and is in the `Validators` storage. fn is_validator(who: &Self::AccountId) -> bool; + + /// Returns the active bonded amount for a stash, or `None` if not bonded. + fn active_stake(who: &Self::AccountId) -> Option; } /// The communication trait of `pallet-staking-async` -> `pallet-staking-async-rc-client`. @@ -1006,7 +1011,10 @@ pub mod pallet { type RelayChainOrigin: EnsureOrigin; /// Our communication handle to the local staking pallet. - type AHStakingInterface: AHStakingInterface; + type AHStakingInterface: AHStakingInterface< + AccountId = Self::AccountId, + Balance = Self::Balance, + >; /// Our communication handle to the relay chain. type SendToRelayChain: SendToRelayChain< @@ -1058,6 +1066,10 @@ pub mod pallet { #[pallet::constant] type MaxSessionKeysLength: Get; + /// Minimum active bond required to call `set_keys`. Set to 0 to disable. + #[pallet::constant] + type MinSetKeysBond: Get>; + /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } @@ -1076,6 +1088,8 @@ pub mod pallet { InvalidKeys, /// Delivery fees exceeded the specified maximum. FeesExceededMax, + /// The stash's active bond is below `MinSetKeysBond`. + InsufficientBond, } #[pallet::event] @@ -1293,6 +1307,13 @@ pub mod pallet { // Only registered validators can set session keys ensure!(T::AHStakingInterface::is_validator(&stash), Error::::NotValidator); + let min_bond = T::MinSetKeysBond::get(); + if !min_bond.is_zero() { + let active = T::AHStakingInterface::active_stake(&stash) + .ok_or(Error::::InsufficientBond)?; + ensure!(active >= min_bond, Error::::InsufficientBond); + } + // Validate keys: decode as RelayChainSessionKeys to ensure correct format let _ = T::RelayChainSessionKeys::decode(&mut &keys[..]) .map_err(|_| Error::::InvalidKeys)?; diff --git a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs index 0d946f7ac5571..0a1c23100fecb 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs @@ -489,6 +489,7 @@ impl pallet_staking_async_rc_client::Config for Runtime { type RelayChainSessionKeys = RelayChainSessionKeys; type Balance = Balance; type MaxSessionKeysLength = ConstU32<256>; + type MinSetKeysBond = ConstU128<{ 10_000 * UNITS }>; type WeightInfo = (); } diff --git a/substrate/frame/staking-async/src/pallet/impls.rs b/substrate/frame/staking-async/src/pallet/impls.rs index f318fa9fd6f87..b8b74d22a5d87 100644 --- a/substrate/frame/staking-async/src/pallet/impls.rs +++ b/substrate/frame/staking-async/src/pallet/impls.rs @@ -1111,6 +1111,7 @@ impl ElectionDataProvider for Pallet { impl rc_client::AHStakingInterface for Pallet { type AccountId = T::AccountId; + type Balance = BalanceOf; type MaxValidatorSet = T::MaxValidatorSet; /// When we receive a session report from the relay chain, it kicks off the next session. @@ -1351,6 +1352,10 @@ impl rc_client::AHStakingInterface for Pallet { fn is_validator(who: &Self::AccountId) -> bool { Validators::::contains_key(who) } + + fn active_stake(who: &Self::AccountId) -> Option> { + Self::ledger(StakingAccount::Stash(who.clone())).ok().map(|l| l.active) + } } impl ScoreProvider for Pallet {