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 8c9b95ba71bc5..5ddba7e1fea49 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs @@ -328,6 +328,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..9f5988b9b9e80 --- /dev/null +++ b/prdoc/pr_11168.prdoc @@ -0,0 +1,13 @@ +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: major +- name: pallet-staking-async-rc-client + bump: major +- name: pallet-staking-async + bump: major diff --git a/substrate/frame/staking-async/integration-tests/src/ah/mock.rs b/substrate/frame/staking-async/integration-tests/src/ah/mock.rs index c240ab79ca591..f091fc0089471 100644 --- a/substrate/frame/staking-async/integration-tests/src/ah/mock.rs +++ b/substrate/frame/staking-async/integration-tests/src/ah/mock.rs @@ -491,6 +491,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; @@ -499,6 +503,7 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ValidatorSetExportSession; type RelayChainSessionKeys = RCSessionKeys; type Balance = Balance; + type MinSetKeysBond = MinSetKeysBond; type WeightInfo = (); } diff --git a/substrate/frame/staking-async/integration-tests/src/ah/test.rs b/substrate/frame/staking-async/integration-tests/src/ah/test.rs index dadc2c998c17d..9be57d5d84672 100644 --- a/substrate/frame/staking-async/integration-tests/src/ah/test.rs +++ b/substrate/frame/staking-async/integration-tests/src/ah/test.rs @@ -1415,7 +1415,7 @@ mod poll_operations { mod session_keys { use super::*; use crate::ah::mock::{ - Balances, LocalQueue, OutgoingMessages, ProxyType, PurgeKeysExecutionCost, + Balances, LocalQueue, MinSetKeysBond, OutgoingMessages, ProxyType, PurgeKeysExecutionCost, SetKeysExecutionCost, }; use codec::Encode; @@ -1881,6 +1881,58 @@ mod session_keys { }); } + #[test] + fn set_keys_insufficient_bond() { + ExtBuilder::default().local_queue().build().execute_with(|| { + let validator: AccountId = 1; + let (keys, proof) = make_session_keys_and_proof(validator); + + // 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(), + proof.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(), + proof.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, proof) = make_session_keys_and_proof(validator); + + // WHEN/THEN: set_keys succeeds regardless of bond amount + assert_ok!(rc_client::Pallet::::set_keys( + RuntimeOrigin::signed(validator), + keys, + proof, + 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 94c788a7ea7fa..12afda79b85f3 100644 --- a/substrate/frame/staking-async/rc-client/src/lib.rs +++ b/substrate/frame/staking-async/rc-client/src/lib.rs @@ -835,6 +835,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; @@ -868,6 +870,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`. @@ -1010,7 +1015,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 { /// The balance type used for delivery fee limits. type Balance: BalanceTrait; + /// 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; } @@ -1078,6 +1090,8 @@ pub mod pallet { InvalidProof, /// Delivery fees exceeded the specified maximum. FeesExceededMax, + /// The stash's active bond is below `MinSetKeysBond`. + InsufficientBond, } #[pallet::event] @@ -1297,6 +1311,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 session_keys = 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 48b262ab1ed6e..6d6b1a025a70a 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs @@ -487,6 +487,7 @@ impl pallet_staking_async_rc_client::Config for Runtime { type ValidatorSetExportSession = ConstU32<4>; type RelayChainSessionKeys = RelayChainSessionKeys; type Balance = Balance; + 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 19e4420e2ba91..949bcc9c5f9d7 100644 --- a/substrate/frame/staking-async/src/pallet/impls.rs +++ b/substrate/frame/staking-async/src/pallet/impls.rs @@ -1112,6 +1112,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. @@ -1345,6 +1346,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 {