diff --git a/Cargo.lock b/Cargo.lock
index 6cca0a018ca72..40a4fc29cd213 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7561,6 +7561,7 @@ version = "4.0.0-dev"
 dependencies = [
  "parity-scale-codec",
  "sp-api",
+ "sp-staking",
 ]
 
 [[package]]
diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs
index 4e1b6d4e8bec0..05e69f5256768 100644
--- a/bin/node/runtime/src/lib.rs
+++ b/bin/node/runtime/src/lib.rs
@@ -416,7 +416,7 @@ impl pallet_babe::Config for Runtime {
 	type DisabledValidators = Session;
 	type WeightInfo = ();
 	type MaxAuthorities = MaxAuthorities;
-	type MaxNominators = MaxNominatorRewardedPerValidator;
+	type MaxNominators = MaxExposurePageSize;
 	type KeyOwnerProof =
 		<Historical as KeyOwnerProofSystem<(KeyTypeId, pallet_babe::AuthorityId)>>::Proof;
 	type EquivocationReportSystem =
@@ -557,7 +557,7 @@ parameter_types! {
 	pub const BondingDuration: sp_staking::EraIndex = 24 * 28;
 	pub const SlashDeferDuration: sp_staking::EraIndex = 24 * 7; // 1/4 the bonding duration.
 	pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
-	pub const MaxNominatorRewardedPerValidator: u32 = 256;
+	pub const MaxExposurePageSize: u32 = 256;
 	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17);
 	pub OffchainRepeat: BlockNumber = 5;
 	pub HistoryDepth: u32 = 84;
@@ -592,7 +592,7 @@ impl pallet_staking::Config for Runtime {
 	type SessionInterface = Self;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
-	type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
+	type MaxExposurePageSize = ConstU32<256>;
 	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = ElectionProviderMultiPhase;
 	type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
@@ -615,8 +615,6 @@ impl pallet_fast_unstake::Config for Runtime {
 	type Currency = Balances;
 	type Staking = Staking;
 	type MaxErasToCheckPerBlock = ConstU32<1>;
-	#[cfg(feature = "runtime-benchmarks")]
-	type MaxBackersPerValidator = MaxNominatorRewardedPerValidator;
 	type WeightInfo = ();
 }
 
@@ -1367,7 +1365,7 @@ impl pallet_grandpa::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type WeightInfo = ();
 	type MaxAuthorities = MaxAuthorities;
-	type MaxNominators = MaxNominatorRewardedPerValidator;
+	type MaxNominators = MaxExposurePageSize;
 	type MaxSetIdSessionEntries = MaxSetIdSessionEntries;
 	type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::Proof;
 	type EquivocationReportSystem =
@@ -2203,10 +2201,14 @@ impl_runtime_apis! {
 		}
 	}
 
-	impl pallet_staking_runtime_api::StakingApi<Block, Balance> for Runtime {
+	impl pallet_staking_runtime_api::StakingApi<Block, Balance, AccountId> for Runtime {
 		fn nominations_quota(balance: Balance) -> u32 {
 			Staking::api_nominations_quota(balance)
 		}
+
+		fn eras_stakers_page_count(era: sp_staking::EraIndex, account: AccountId) -> sp_staking::PageIndex {
+			Staking::api_eras_stakers_page_count(era, account)
+		}
 	}
 
 	impl sp_consensus_babe::BabeApi<Block> for Runtime {
diff --git a/frame/babe/src/mock.rs b/frame/babe/src/mock.rs
index dbffe9f312e60..9a4268e2f3323 100644
--- a/frame/babe/src/mock.rs
+++ b/frame/babe/src/mock.rs
@@ -193,7 +193,7 @@ impl pallet_staking::Config for Test {
 	type SessionInterface = Self;
 	type UnixTime = pallet_timestamp::Pallet<Test>;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
diff --git a/frame/babe/src/tests.rs b/frame/babe/src/tests.rs
index ae0c3e3873c50..55266b9027112 100644
--- a/frame/babe/src/tests.rs
+++ b/frame/babe/src/tests.rs
@@ -440,7 +440,7 @@ fn report_equivocation_current_session_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(1, validator),
+				Staking::eras_stakers(1, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -481,7 +481,7 @@ fn report_equivocation_current_session_works() {
 		assert_eq!(Balances::total_balance(&offending_validator_id), 10_000_000 - 10_000);
 		assert_eq!(Staking::slashable_balance_of(&offending_validator_id), 0);
 		assert_eq!(
-			Staking::eras_stakers(2, offending_validator_id),
+			Staking::eras_stakers(2, &offending_validator_id),
 			pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
 		);
 
@@ -494,7 +494,7 @@ fn report_equivocation_current_session_works() {
 			assert_eq!(Balances::total_balance(validator), 10_000_000);
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 			assert_eq!(
-				Staking::eras_stakers(2, validator),
+				Staking::eras_stakers(2, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -553,7 +553,7 @@ fn report_equivocation_old_session_works() {
 		assert_eq!(Balances::total_balance(&offending_validator_id), 10_000_000 - 10_000);
 		assert_eq!(Staking::slashable_balance_of(&offending_validator_id), 0);
 		assert_eq!(
-			Staking::eras_stakers(3, offending_validator_id),
+			Staking::eras_stakers(3, &offending_validator_id),
 			pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
 		);
 	})
diff --git a/frame/beefy/src/mock.rs b/frame/beefy/src/mock.rs
index f2d8415bc01f7..5f44e49cfb9eb 100644
--- a/frame/beefy/src/mock.rs
+++ b/frame/beefy/src/mock.rs
@@ -215,7 +215,7 @@ impl pallet_staking::Config for Test {
 	type SessionInterface = Self;
 	type UnixTime = pallet_timestamp::Pallet<Test>;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
diff --git a/frame/beefy/src/tests.rs b/frame/beefy/src/tests.rs
index e04dc330d0c07..4af1026e24401 100644
--- a/frame/beefy/src/tests.rs
+++ b/frame/beefy/src/tests.rs
@@ -277,7 +277,7 @@ fn report_equivocation_current_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(1, validator),
+				Staking::eras_stakers(1, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -314,7 +314,7 @@ fn report_equivocation_current_set_works() {
 		assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000);
 		assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
 		assert_eq!(
-			Staking::eras_stakers(2, equivocation_validator_id),
+			Staking::eras_stakers(2, &equivocation_validator_id),
 			pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
 		);
 
@@ -328,7 +328,7 @@ fn report_equivocation_current_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(2, validator),
+				Staking::eras_stakers(2, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -363,7 +363,7 @@ fn report_equivocation_old_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(2, validator),
+				Staking::eras_stakers(2, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -397,7 +397,7 @@ fn report_equivocation_old_set_works() {
 		assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000);
 		assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
 		assert_eq!(
-			Staking::eras_stakers(3, equivocation_validator_id),
+			Staking::eras_stakers(3, &equivocation_validator_id),
 			pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
 		);
 
@@ -411,7 +411,7 @@ fn report_equivocation_old_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(3, validator),
+				Staking::eras_stakers(3, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
diff --git a/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs b/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs
index 9c3511ae35751..22b37683dfbfe 100644
--- a/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs
+++ b/frame/election-provider-multi-phase/test-staking-e2e/src/mock.rs
@@ -238,7 +238,6 @@ parameter_types! {
 	pub const SessionsPerEra: sp_staking::SessionIndex = 2;
 	pub const BondingDuration: sp_staking::EraIndex = 28;
 	pub const SlashDeferDuration: sp_staking::EraIndex = 7; // 1/4 the bonding duration.
-	pub const MaxNominatorRewardedPerValidator: u32 = 256;
 	pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(40);
 	pub HistoryDepth: u32 = 84;
 }
@@ -270,7 +269,7 @@ impl pallet_staking::Config for Runtime {
 	type SessionInterface = Self;
 	type EraPayout = ();
 	type NextNewSession = Session;
-	type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
+	type MaxExposurePageSize = ConstU32<256>;
 	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = ElectionProviderMultiPhase;
 	type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
@@ -810,7 +809,7 @@ pub(crate) fn on_offence_now(
 pub(crate) fn add_slash(who: &AccountId) {
 	on_offence_now(
 		&[OffenceDetails {
-			offender: (*who, Staking::eras_stakers(active_era(), *who)),
+			offender: (*who, Staking::eras_stakers(active_era(), who)),
 			reporters: vec![],
 		}],
 		&[Perbill::from_percent(10)],
diff --git a/frame/fast-unstake/src/benchmarking.rs b/frame/fast-unstake/src/benchmarking.rs
index 5ec997e8eaa2a..851483e3697bf 100644
--- a/frame/fast-unstake/src/benchmarking.rs
+++ b/frame/fast-unstake/src/benchmarking.rs
@@ -74,9 +74,9 @@ fn setup_staking<T: Config>(v: u32, until: EraIndex) {
 		.collect::<Vec<_>>();
 
 	for era in 0..=until {
-		let others = (0..T::MaxBackersPerValidator::get())
+		let others = (0..T::Staking::max_exposure_page_size())
 			.map(|s| {
-				let who = frame_benchmarking::account::<T::AccountId>("nominator", era, s);
+				let who = frame_benchmarking::account::<T::AccountId>("nominator", era, s.into());
 				let value = ed;
 				(who, value)
 			})
diff --git a/frame/fast-unstake/src/lib.rs b/frame/fast-unstake/src/lib.rs
index 39783271e6569..11f3ce5794fa2 100644
--- a/frame/fast-unstake/src/lib.rs
+++ b/frame/fast-unstake/src/lib.rs
@@ -203,10 +203,6 @@ pub mod pallet {
 
 		/// The weight information of this pallet.
 		type WeightInfo: WeightInfo;
-
-		/// Use only for benchmarking.
-		#[cfg(feature = "runtime-benchmarks")]
-		type MaxBackersPerValidator: Get<u32>;
 	}
 
 	/// The current "head of the queue" being unstaked.
diff --git a/frame/fast-unstake/src/mock.rs b/frame/fast-unstake/src/mock.rs
index 3a3d9e2d4b133..54420cca099fa 100644
--- a/frame/fast-unstake/src/mock.rs
+++ b/frame/fast-unstake/src/mock.rs
@@ -151,7 +151,7 @@ impl pallet_staking::Config for Runtime {
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = ();
 	type HistoryDepth = ConstU32<84>;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = ();
 	type ElectionProvider = MockElection;
 	type GenesisElectionProvider = Self::ElectionProvider;
@@ -192,8 +192,6 @@ impl fast_unstake::Config for Runtime {
 	type BatchSize = BatchSize;
 	type WeightInfo = ();
 	type MaxErasToCheckPerBlock = ConstU32<16>;
-	#[cfg(feature = "runtime-benchmarks")]
-	type MaxBackersPerValidator = ConstU32<128>;
 }
 
 type Block = frame_system::mocking::MockBlock<Runtime>;
diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs
index fd4d737dc493f..7eaa7dd6485f2 100644
--- a/frame/grandpa/src/mock.rs
+++ b/frame/grandpa/src/mock.rs
@@ -195,7 +195,7 @@ impl pallet_staking::Config for Test {
 	type SessionInterface = Self;
 	type UnixTime = pallet_timestamp::Pallet<Test>;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type NextNewSession = Session;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
diff --git a/frame/grandpa/src/tests.rs b/frame/grandpa/src/tests.rs
index 59d73ee729ee8..993d72af6d410 100644
--- a/frame/grandpa/src/tests.rs
+++ b/frame/grandpa/src/tests.rs
@@ -333,7 +333,7 @@ fn report_equivocation_current_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(1, validator),
+				Staking::eras_stakers(1, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -371,7 +371,7 @@ fn report_equivocation_current_set_works() {
 		assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000);
 		assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
 		assert_eq!(
-			Staking::eras_stakers(2, equivocation_validator_id),
+			Staking::eras_stakers(2, &equivocation_validator_id),
 			pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
 		);
 
@@ -385,7 +385,7 @@ fn report_equivocation_current_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(2, validator),
+				Staking::eras_stakers(2, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -417,7 +417,7 @@ fn report_equivocation_old_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(2, validator),
+				Staking::eras_stakers(2, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
@@ -450,7 +450,7 @@ fn report_equivocation_old_set_works() {
 		assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0);
 
 		assert_eq!(
-			Staking::eras_stakers(3, equivocation_validator_id),
+			Staking::eras_stakers(3, &equivocation_validator_id),
 			pallet_staking::Exposure { total: 0, own: 0, others: vec![] },
 		);
 
@@ -464,7 +464,7 @@ fn report_equivocation_old_set_works() {
 			assert_eq!(Staking::slashable_balance_of(validator), 10_000);
 
 			assert_eq!(
-				Staking::eras_stakers(3, validator),
+				Staking::eras_stakers(3, &validator),
 				pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] },
 			);
 		}
diff --git a/frame/nomination-pools/benchmarking/src/mock.rs b/frame/nomination-pools/benchmarking/src/mock.rs
index 2d75df63b518a..5fba0ed7cfe57 100644
--- a/frame/nomination-pools/benchmarking/src/mock.rs
+++ b/frame/nomination-pools/benchmarking/src/mock.rs
@@ -109,7 +109,7 @@ impl pallet_staking::Config for Runtime {
 	type SessionInterface = ();
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = ();
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = ();
 	type ElectionProvider =
 		frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
diff --git a/frame/nomination-pools/src/mock.rs b/frame/nomination-pools/src/mock.rs
index 7d0d729a40d41..e977253a00e7f 100644
--- a/frame/nomination-pools/src/mock.rs
+++ b/frame/nomination-pools/src/mock.rs
@@ -162,6 +162,11 @@ impl sp_staking::StakingInterface for StakingMock {
 	fn set_current_era(_era: EraIndex) {
 		unimplemented!("method currently not used in testing")
 	}
+
+	#[cfg(feature = "runtime-benchmarks")]
+	fn max_exposure_page_size() -> sp_staking::PageIndex {
+		unimplemented!("method currently not used in testing")
+	}
 }
 
 impl frame_system::Config for Runtime {
diff --git a/frame/nomination-pools/test-staking/src/mock.rs b/frame/nomination-pools/test-staking/src/mock.rs
index 02c253e62c018..73f4180c7976b 100644
--- a/frame/nomination-pools/test-staking/src/mock.rs
+++ b/frame/nomination-pools/test-staking/src/mock.rs
@@ -123,7 +123,7 @@ impl pallet_staking::Config for Runtime {
 	type SessionInterface = ();
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = ();
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = ();
 	type ElectionProvider =
 		frame_election_provider_support::NoElection<(AccountId, BlockNumber, Staking, ())>;
diff --git a/frame/offences/benchmarking/src/mock.rs b/frame/offences/benchmarking/src/mock.rs
index 88f418dd3e2e8..ddbf68b7535e5 100644
--- a/frame/offences/benchmarking/src/mock.rs
+++ b/frame/offences/benchmarking/src/mock.rs
@@ -175,7 +175,7 @@ impl pallet_staking::Config for Test {
 	type SessionInterface = Self;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = ();
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
diff --git a/frame/root-offences/src/lib.rs b/frame/root-offences/src/lib.rs
index a93e7ff848718..e6bb5bb188199 100644
--- a/frame/root-offences/src/lib.rs
+++ b/frame/root-offences/src/lib.rs
@@ -111,7 +111,7 @@ pub mod pallet {
 				.clone()
 				.into_iter()
 				.map(|(o, _)| OffenceDetails::<T> {
-					offender: (o.clone(), Staking::<T>::eras_stakers(now, o)),
+					offender: (o.clone(), Staking::<T>::eras_stakers(now, &o)),
 					reporters: vec![],
 				})
 				.collect())
diff --git a/frame/root-offences/src/mock.rs b/frame/root-offences/src/mock.rs
index 2d2a5476149f6..387656ed06d8e 100644
--- a/frame/root-offences/src/mock.rs
+++ b/frame/root-offences/src/mock.rs
@@ -178,7 +178,7 @@ impl pallet_staking::Config for Test {
 	type SessionInterface = Self;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
diff --git a/frame/session/benchmarking/src/mock.rs b/frame/session/benchmarking/src/mock.rs
index 24a821ac87af5..803070584f587 100644
--- a/frame/session/benchmarking/src/mock.rs
+++ b/frame/session/benchmarking/src/mock.rs
@@ -172,7 +172,7 @@ impl pallet_staking::Config for Test {
 	type SessionInterface = Self;
 	type EraPayout = pallet_staking::ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = ConstU32<64>;
 	type OffendingValidatorsThreshold = ();
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
diff --git a/frame/staking/CHANGELOG.md b/frame/staking/CHANGELOG.md
new file mode 100644
index 0000000000000..719aa388755fc
--- /dev/null
+++ b/frame/staking/CHANGELOG.md
@@ -0,0 +1,27 @@
+# Changelog
+
+All notable changes and migrations to pallet-staking will be documented in this file.
+
+The format is loosely based
+on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). We maintain a
+single integer version number for staking pallet to keep track of all storage
+migrations.
+
+## [v14]
+
+### Added
+
+- New item `ErasStakersPaged` that keeps up to `MaxExposurePageSize`
+  individual nominator exposures by era, validator and page.
+- New item `ErasStakersOverview` complementary to `ErasStakersPaged` which keeps
+  state of own and total stake of the validator across pages.
+- New item `ClaimedRewards` to support paged rewards payout.
+
+### Deprecated
+
+- `ErasStakers` and `ErasStakersClipped` is deprecated, will not be used any longer for the exposures of the new era
+  post v14 and can be removed after 84 eras once all the exposures are stale.
+- Field `claimed_rewards` in item `Ledger` is renamed
+  to `legacy_claimed_rewards` and can be removed after 84 eras.
+
+[v14]: https://github.com/paritytech/substrate/pull/13498
diff --git a/frame/staking/README.md b/frame/staking/README.md
index ccb9901a6796e..19f0f7dc2c399 100644
--- a/frame/staking/README.md
+++ b/frame/staking/README.md
@@ -52,7 +52,7 @@ An account pair can become bonded using the [`bond`](https://docs.rs/pallet-stak
 
 Stash accounts can update their associated controller back to their stash account using the
 [`set_controller`](https://docs.rs/pallet-staking/latest/pallet_staking/enum.Call.html#variant.set_controller)
-call. 
+call.
 
 Note: Controller accounts are being deprecated in favor of proxy accounts, so it is no longer
 possible to set a unique address for a stash's controller.
@@ -92,11 +92,13 @@ An account can become a nominator via the [`nominate`](https://docs.rs/pallet-st
 The **reward and slashing** procedure is the core of the Staking module, attempting to _embrace
 valid behavior_ while _punishing any misbehavior or lack of availability_.
 
-Rewards must be claimed for each era before it gets too old by `$HISTORY_DEPTH` using the
-`payout_stakers` call. Any account can call `payout_stakers`, which pays the reward to the
-validator as well as its nominators. Only the [`Config::MaxNominatorRewardedPerValidator`]
-biggest stakers can claim their reward. This is to limit the i/o cost to mutate storage for each
-nominator's account.
+Rewards must be claimed on a per-era basis via the `payout_stakers` call, and must be claimed before
+[`HistoryDepth`] eras have passed since the reward's inclusion, after which any unclaimed rewards
+will expire and be removed from storage. Any account can call `payout_stakers`, which pays the reward
+to the validator as well as its nominators. Rewards are paged to maximum of
+[`Config::MaxExposurePageSize`] nominators per page. Each page of staker payout needs to be called
+separately to ensure all nominators are paid. This is to limit the i/o cost to mutate storage for
+each nominator's account.
 
 Slashing can occur at any point in time, once misbehavior is reported. Once slashing is
 determined, a value is deducted from the balance of the validator and all the nominators who
diff --git a/frame/staking/runtime-api/Cargo.toml b/frame/staking/runtime-api/Cargo.toml
index 5c9af0ad3cbe8..1e52d846e35b1 100644
--- a/frame/staking/runtime-api/Cargo.toml
+++ b/frame/staking/runtime-api/Cargo.toml
@@ -15,10 +15,12 @@ targets = ["x86_64-unknown-linux-gnu"]
 [dependencies]
 codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] }
 sp-api = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/api" }
+sp-staking = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/staking" }
 
 [features]
 default = ["std"]
 std = [
 	"codec/std",
 	"sp-api/std",
+	"sp-staking/std",
 ]
diff --git a/frame/staking/runtime-api/src/lib.rs b/frame/staking/runtime-api/src/lib.rs
index 378599c665506..35b3b0dc96443 100644
--- a/frame/staking/runtime-api/src/lib.rs
+++ b/frame/staking/runtime-api/src/lib.rs
@@ -22,11 +22,15 @@
 use codec::Codec;
 
 sp_api::decl_runtime_apis! {
-	pub trait StakingApi<Balance>
+	pub trait StakingApi<Balance, AccountId>
 		where
 			Balance: Codec,
+			AccountId: Codec,
 	{
 		/// Returns the nominations quota for a nominator with a given balance.
 		fn nominations_quota(balance: Balance) -> u32;
+
+		/// Returns the page count of exposures for a validator in a given era.
+		fn eras_stakers_page_count(era: sp_staking::EraIndex, account: AccountId) -> sp_staking::PageIndex;
 	}
 }
diff --git a/frame/staking/src/benchmarking.rs b/frame/staking/src/benchmarking.rs
index e72a9baf044fe..2438b6dc8735f 100644
--- a/frame/staking/src/benchmarking.rs
+++ b/frame/staking/src/benchmarking.rs
@@ -553,10 +553,10 @@ benchmarks! {
 	}
 
 	payout_stakers_dead_controller {
-		let n in 0 .. T::MaxNominatorRewardedPerValidator::get() as u32;
+		let n in 0 .. T::MaxExposurePageSize::get() as u32;
 		let (validator, nominators) = create_validator_with_nominators::<T>(
 			n,
-			T::MaxNominatorRewardedPerValidator::get() as u32,
+			T::MaxExposurePageSize::get() as u32,
 			true,
 			true,
 			RewardDestination::Controller,
@@ -573,7 +573,7 @@ benchmarks! {
 			let balance = T::Currency::free_balance(controller);
 			ensure!(balance.is_zero(), "Controller has balance, but should be dead.");
 		}
-	}: payout_stakers(RawOrigin::Signed(caller), validator, current_era)
+	}: payout_stakers_by_page(RawOrigin::Signed(caller), validator, current_era, 0)
 	verify {
 		let balance_after = T::Currency::free_balance(&validator_controller);
 		ensure!(
@@ -587,10 +587,10 @@ benchmarks! {
 	}
 
 	payout_stakers_alive_staked {
-		let n in 0 .. T::MaxNominatorRewardedPerValidator::get() as u32;
+		let n in 0 .. T::MaxExposurePageSize::get() as u32;
 		let (validator, nominators) = create_validator_with_nominators::<T>(
 			n,
-			T::MaxNominatorRewardedPerValidator::get() as u32,
+			T::MaxExposurePageSize::get() as u32,
 			false,
 			true,
 			RewardDestination::Staked,
@@ -690,7 +690,7 @@ benchmarks! {
 			active: T::Currency::minimum_balance() - One::one(),
 			total: T::Currency::minimum_balance() - One::one(),
 			unlocking: Default::default(),
-			claimed_rewards: Default::default(),
+			legacy_claimed_rewards: Default::default(),
 		};
 		Ledger::<T>::insert(&controller, l);
 
@@ -763,7 +763,7 @@ benchmarks! {
 		let caller: T::AccountId = whitelisted_caller();
 		let origin = RawOrigin::Signed(caller);
 		let calls: Vec<_> = payout_calls_arg.iter().map(|arg|
-			Call::<T>::payout_stakers { validator_stash: arg.0.clone(), era: arg.1 }.encode()
+			Call::<T>::payout_stakers_by_page { validator_stash: arg.0.clone(), era: arg.1, page: 0 }.encode()
 		).collect();
 	}: {
 		for call in calls {
@@ -987,7 +987,7 @@ mod tests {
 
 			let (validator_stash, nominators) = create_validator_with_nominators::<Test>(
 				n,
-				<<Test as Config>::MaxNominatorRewardedPerValidator as Get<_>>::get(),
+				<<Test as Config>::MaxExposurePageSize as Get<_>>::get(),
 				false,
 				false,
 				RewardDestination::Staked,
@@ -999,10 +999,11 @@ mod tests {
 			let current_era = CurrentEra::<Test>::get().unwrap();
 
 			let original_free_balance = Balances::free_balance(&validator_stash);
-			assert_ok!(Staking::payout_stakers(
+			assert_ok!(Staking::payout_stakers_by_page(
 				RuntimeOrigin::signed(1337),
 				validator_stash,
-				current_era
+				current_era,
+				0
 			));
 			let new_free_balance = Balances::free_balance(&validator_stash);
 
@@ -1017,7 +1018,7 @@ mod tests {
 
 			let (validator_stash, _nominators) = create_validator_with_nominators::<Test>(
 				n,
-				<<Test as Config>::MaxNominatorRewardedPerValidator as Get<_>>::get(),
+				<<Test as Config>::MaxExposurePageSize as Get<_>>::get(),
 				false,
 				false,
 				RewardDestination::Staked,
diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs
index e59b2a3324a62..47e6d68b3a516 100644
--- a/frame/staking/src/lib.rs
+++ b/frame/staking/src/lib.rs
@@ -112,11 +112,15 @@
 //! The **reward and slashing** procedure is the core of the Staking pallet, attempting to _embrace
 //! valid behavior_ while _punishing any misbehavior or lack of availability_.
 //!
-//! Rewards must be claimed for each era before it gets too old by `$HISTORY_DEPTH` using the
-//! `payout_stakers` call. Any account can call `payout_stakers`, which pays the reward to the
-//! validator as well as its nominators. Only the [`Config::MaxNominatorRewardedPerValidator`]
-//! biggest stakers can claim their reward. This is to limit the i/o cost to mutate storage for each
-//! nominator's account.
+//! Rewards must be claimed for each era before it gets too old by
+//! [`HistoryDepth`](`Config::HistoryDepth`) using the `payout_stakers` call. Any account can call
+//! `payout_stakers`, which pays the reward to the validator as well as its nominators. Only
+//! [`Config::MaxExposurePageSize`] nominator rewards can be claimed in a single call. When the
+//! number of nominators exceeds [`Config::MaxExposurePageSize`], then the exposed nominators are
+//! stored in multiple pages, with each page containing up to
+//! [`Config::MaxExposurePageSize`] nominators. To pay out all nominators, `payout_stakers` must be
+//! called once for each available page. Paging exists to limit the i/o cost to mutate storage for
+//! each nominator's account.
 //!
 //! Slashing can occur at any point in time, once misbehavior is reported. Once slashing is
 //! determined, a value is deducted from the balance of the validator and all the nominators who
@@ -224,13 +228,13 @@
 //! The validator can declare an amount, named [`commission`](ValidatorPrefs::commission), that does
 //! not get shared with the nominators at each reward payout through its [`ValidatorPrefs`]. This
 //! value gets deducted from the total reward that is paid to the validator and its nominators. The
-//! remaining portion is split pro rata among the validator and the top
-//! [`Config::MaxNominatorRewardedPerValidator`] nominators that nominated the validator,
-//! proportional to the value staked behind the validator (_i.e._ dividing the
+//! remaining portion is split pro rata among the validator and the nominators that nominated the
+//! validator, proportional to the value staked behind the validator (_i.e._ dividing the
 //! [`own`](Exposure::own) or [`others`](Exposure::others) by [`total`](Exposure::total) in
-//! [`Exposure`]). Note that the pro rata division of rewards uses the total exposure behind the
-//! validator, *not* just the exposure of the validator and the top
-//! [`Config::MaxNominatorRewardedPerValidator`] nominators.
+//! [`Exposure`]). Note that payouts are made in pages with each page capped at
+//! [`Config::MaxExposurePageSize`] nominators. The distribution of nominators across
+//! pages may be unsorted. The total commission is paid out proportionally across pages based on the
+//! total stake of the page.
 //!
 //! All entities who receive a reward have the option to choose their reward destination through the
 //! [`Payee`] storage item (see
@@ -302,7 +306,8 @@ mod pallet;
 
 use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
 use frame_support::{
-	traits::{ConstU32, Currency, Defensive, Get},
+	defensive, defensive_assert,
+	traits::{ConstU32, Currency, Defensive, DefensiveMax, DefensiveSaturating, Get},
 	weights::Weight,
 	BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
 };
@@ -312,11 +317,11 @@ use sp_runtime::{
 	traits::{AtLeast32BitUnsigned, Convert, Saturating, StaticLookup, Zero},
 	Perbill, Perquintill, Rounding, RuntimeDebug,
 };
-pub use sp_staking::StakerStatus;
 use sp_staking::{
 	offence::{Offence, OffenceError, ReportOffence},
-	EraIndex, OnStakingUpdate, SessionIndex,
+	EraIndex, ExposurePage, OnStakingUpdate, PageIndex, PagedExposureMetadata, SessionIndex,
 };
+pub use sp_staking::{Exposure, IndividualExposure, StakerStatus};
 use sp_std::{collections::btree_map::BTreeMap, prelude::*};
 pub use weights::WeightInfo;
 
@@ -461,7 +466,10 @@ pub struct StakingLedger<T: Config> {
 	pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
 	/// List of eras for which the stakers behind a validator have claimed rewards. Only updated
 	/// for validators.
-	pub claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>,
+	///
+	/// This is deprecated as of V14 in favor of `T::ClaimedRewards` and will be removed in future.
+	/// Refer to issue <https://github.com/paritytech/substrate/issues/13034>
+	pub legacy_claimed_rewards: BoundedVec<EraIndex, T::HistoryDepth>,
 }
 
 impl<T: Config> StakingLedger<T> {
@@ -472,7 +480,7 @@ impl<T: Config> StakingLedger<T> {
 			total: Zero::zero(),
 			active: Zero::zero(),
 			unlocking: Default::default(),
-			claimed_rewards: Default::default(),
+			legacy_claimed_rewards: Default::default(),
 		}
 	}
 
@@ -502,7 +510,7 @@ impl<T: Config> StakingLedger<T> {
 			total,
 			active: self.active,
 			unlocking,
-			claimed_rewards: self.claimed_rewards,
+			legacy_claimed_rewards: self.legacy_claimed_rewards,
 		}
 	}
 
@@ -696,32 +704,50 @@ pub struct Nominations<T: Config> {
 	pub suppressed: bool,
 }
 
-/// The amount of exposure (to slashing) than an individual nominator has.
-#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
-pub struct IndividualExposure<AccountId, Balance: HasCompact> {
-	/// The stash account of the nominator in question.
-	pub who: AccountId,
-	/// Amount of funds exposed.
-	#[codec(compact)]
-	pub value: Balance,
+/// Facade struct to encapsulate `PagedExposureMetadata` and a single page of `ExposurePage`.
+///
+/// This is useful where we need to take into account the validator's own stake and total exposure
+/// in consideration, in addition to the individual nominators backing them.
+#[derive(Encode, Decode, RuntimeDebug, TypeInfo, PartialEq, Eq)]
+struct PagedExposure<AccountId, Balance: HasCompact + codec::MaxEncodedLen> {
+	exposure_metadata: PagedExposureMetadata<Balance>,
+	exposure_page: ExposurePage<AccountId, Balance>,
 }
 
-/// A snapshot of the stake backing a single validator in the system.
-#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
-pub struct Exposure<AccountId, Balance: HasCompact> {
-	/// The total balance backing this validator.
-	#[codec(compact)]
-	pub total: Balance,
-	/// The validator's own stash that is exposed.
-	#[codec(compact)]
-	pub own: Balance,
-	/// The portions of nominators stashes that are exposed.
-	pub others: Vec<IndividualExposure<AccountId, Balance>>,
-}
+impl<AccountId, Balance: HasCompact + Copy + AtLeast32BitUnsigned + codec::MaxEncodedLen>
+	PagedExposure<AccountId, Balance>
+{
+	/// Create a new instance of `PagedExposure` from legacy clipped exposures.
+	pub fn from_clipped(exposure: Exposure<AccountId, Balance>) -> Self {
+		Self {
+			exposure_metadata: PagedExposureMetadata {
+				total: exposure.total,
+				own: exposure.own,
+				nominator_count: exposure.others.len() as u32,
+				page_count: 1,
+			},
+			exposure_page: ExposurePage { page_total: exposure.total, others: exposure.others },
+		}
+	}
 
-impl<AccountId, Balance: Default + HasCompact> Default for Exposure<AccountId, Balance> {
-	fn default() -> Self {
-		Self { total: Default::default(), own: Default::default(), others: vec![] }
+	/// Returns total exposure of this validator across pages
+	pub fn total(&self) -> Balance {
+		self.exposure_metadata.total
+	}
+
+	/// Returns total exposure of this validator for the current page
+	pub fn page_total(&self) -> Balance {
+		self.exposure_page.page_total + self.exposure_metadata.own
+	}
+
+	/// Returns validator's own stake that is exposed
+	pub fn own(&self) -> Balance {
+		self.exposure_metadata.own
+	}
+
+	/// Returns the portions of nominators stashes that are exposed in this page.
+	pub fn others(&self) -> &Vec<IndividualExposure<AccountId, Balance>> {
+		&self.exposure_page.others
 	}
 }
 
@@ -973,6 +999,195 @@ where
 	}
 }
 
+/// Wrapper struct for Era related information. It is not a pure encapsulation as these storage
+/// items can be accessed directly but nevertheless, its recommended to use `EraInfo` where we
+/// can and add more functions to it as needed.
+pub(crate) struct EraInfo<T>(sp_std::marker::PhantomData<T>);
+impl<T: Config> EraInfo<T> {
+	/// Temporary function which looks at both (1) passed param `T::StakingLedger` for legacy
+	/// non-paged rewards, and (2) `T::ClaimedRewards` for paged rewards. This function can be
+	/// removed once `T::HistoryDepth` eras have passed and none of the older non-paged rewards
+	/// are relevant/claimable.
+	// Refer tracker issue for cleanup: #13034
+	pub(crate) fn is_rewards_claimed_with_legacy_fallback(
+		era: EraIndex,
+		ledger: &StakingLedger<T>,
+		validator: &T::AccountId,
+		page: PageIndex,
+	) -> bool {
+		ledger.legacy_claimed_rewards.binary_search(&era).is_ok() ||
+			Self::is_rewards_claimed(era, validator, page)
+	}
+
+	/// Check if the rewards for the given era and page index have been claimed.
+	///
+	/// This is only used for paged rewards. Once older non-paged rewards are no longer
+	/// relevant, `is_rewards_claimed_with_legacy_fallback` can be removed and this function can
+	/// be made public.
+	fn is_rewards_claimed(era: EraIndex, validator: &T::AccountId, page: PageIndex) -> bool {
+		ClaimedRewards::<T>::get(era, validator).contains(&page)
+	}
+
+	/// Get exposure for a validator at a given era and page.
+	///
+	/// This builds a paged exposure from `PagedExposureMetadata` and `ExposurePage` of the
+	/// validator. For older non-paged exposure, it returns the clipped exposure directly.
+	pub(crate) fn get_paged_exposure(
+		era: EraIndex,
+		validator: &T::AccountId,
+		page: PageIndex,
+	) -> Option<PagedExposure<T::AccountId, BalanceOf<T>>> {
+		let overview = <ErasStakersOverview<T>>::get(&era, validator);
+
+		// return clipped exposure if page zero and paged exposure does not exist
+		// exists for backward compatibility and can be removed as part of #13034
+		if overview.is_none() && page == 0 {
+			return Some(PagedExposure::from_clipped(<ErasStakersClipped<T>>::get(era, validator)))
+		}
+
+		// no exposure for this validator
+		if overview.is_none() {
+			return None
+		}
+
+		let overview = overview.expect("checked above; qed");
+
+		// validator stake is added only in page zero
+		let validator_stake = if page == 0 { overview.own } else { Zero::zero() };
+
+		// since overview is present, paged exposure will always be present except when a
+		// validator has only own stake and no nominator stake.
+		let exposure_page = <ErasStakersPaged<T>>::get((era, validator, page)).unwrap_or_default();
+
+		// build the exposure
+		Some(PagedExposure {
+			exposure_metadata: PagedExposureMetadata { own: validator_stake, ..overview },
+			exposure_page,
+		})
+	}
+
+	/// Get full exposure of the validator at a given era.
+	pub(crate) fn get_full_exposure(
+		era: EraIndex,
+		validator: &T::AccountId,
+	) -> Exposure<T::AccountId, BalanceOf<T>> {
+		let overview = <ErasStakersOverview<T>>::get(&era, validator);
+
+		if overview.is_none() {
+			return ErasStakers::<T>::get(era, validator)
+		}
+
+		let overview = overview.expect("checked above; qed");
+
+		let mut others = Vec::with_capacity(overview.nominator_count as usize);
+		for page in 0..overview.page_count {
+			let nominators = <ErasStakersPaged<T>>::get((era, validator, page));
+			others.append(&mut nominators.map(|n| n.others).defensive_unwrap_or_default());
+		}
+
+		Exposure { total: overview.total, own: overview.own, others }
+	}
+
+	/// Returns the number of pages of exposure a validator has for the given era.
+	///
+	/// For eras where paged exposure does not exist, this returns 1 to keep backward compatibility.
+	pub(crate) fn get_page_count(era: EraIndex, validator: &T::AccountId) -> PageIndex {
+		<ErasStakersOverview<T>>::get(&era, validator)
+			.map(|overview| {
+				if overview.page_count == 0 && overview.own > Zero::zero() {
+					// Even though there are no nominator pages, there is still validator's own
+					// stake exposed which needs to be paid out in a page.
+					1
+				} else {
+					overview.page_count
+				}
+			})
+			// Always returns 1 page for older non-paged exposure.
+			// FIXME: Can be cleaned up with issue #13034.
+			.unwrap_or(1)
+	}
+
+	/// Returns the next page that can be claimed or `None` if nothing to claim.
+	pub(crate) fn get_next_claimable_page(
+		era: EraIndex,
+		validator: &T::AccountId,
+		ledger: &StakingLedger<T>,
+	) -> Option<PageIndex> {
+		if Self::is_non_paged_exposure(era, validator) {
+			return match ledger.legacy_claimed_rewards.binary_search(&era) {
+				// already claimed
+				Ok(_) => None,
+				// Non-paged exposure is considered as a single page
+				Err(_) => Some(0),
+			}
+		}
+
+		// Find next claimable page of paged exposure.
+		let page_count = Self::get_page_count(era, validator);
+		let all_claimable_pages: Vec<PageIndex> = (0..page_count).collect();
+		let claimed_pages = ClaimedRewards::<T>::get(era, validator);
+
+		all_claimable_pages.into_iter().find(|p| !claimed_pages.contains(p))
+	}
+
+	/// Checks if exposure is paged or not.
+	fn is_non_paged_exposure(era: EraIndex, validator: &T::AccountId) -> bool {
+		<ErasStakersClipped<T>>::contains_key(&era, validator)
+	}
+
+	/// Returns validator commission for this era and page.
+	pub(crate) fn get_validator_commission(
+		era: EraIndex,
+		validator_stash: &T::AccountId,
+	) -> Perbill {
+		<ErasValidatorPrefs<T>>::get(&era, validator_stash).commission
+	}
+
+	/// Creates an entry to track validator reward has been claimed for a given era and page.
+	/// Noop if already claimed.
+	pub(crate) fn set_rewards_as_claimed(era: EraIndex, validator: &T::AccountId, page: PageIndex) {
+		let mut claimed_pages = ClaimedRewards::<T>::get(era, validator);
+
+		// this should never be called if the reward has already been claimed
+		if claimed_pages.contains(&page) {
+			defensive!("Trying to set an already claimed reward");
+			// nevertheless don't do anything since the page already exist in claimed rewards.
+			return
+		}
+
+		// add page to claimed entries
+		claimed_pages.push(page);
+		ClaimedRewards::<T>::insert(era, validator, claimed_pages);
+	}
+
+	/// Store exposure for elected validators at start of an era.
+	pub(crate) fn set_exposure(
+		era: EraIndex,
+		validator: &T::AccountId,
+		exposure: Exposure<T::AccountId, BalanceOf<T>>,
+	) {
+		let page_size = T::MaxExposurePageSize::get().defensive_max(1);
+
+		let nominator_count = exposure.others.len();
+		// expected page count is the number of nominators divided by the page size, rounded up.
+		let expected_page_count =
+			nominator_count.defensive_saturating_add(page_size as usize - 1) / page_size as usize;
+
+		let (exposure_metadata, exposure_pages) = exposure.into_pages(page_size);
+		defensive_assert!(exposure_pages.len() == expected_page_count, "unexpected page count");
+
+		<ErasStakersOverview<T>>::insert(era, &validator, &exposure_metadata);
+		exposure_pages.iter().enumerate().for_each(|(page, paged_exposure)| {
+			<ErasStakersPaged<T>>::insert((era, &validator, page as PageIndex), &paged_exposure);
+		});
+	}
+
+	/// Store total exposure for all the elected validators in the era.
+	pub(crate) fn set_total_stake(era: EraIndex, total_stake: BalanceOf<T>) {
+		<ErasTotalStake<T>>::insert(era, total_stake);
+	}
+}
+
 /// Configurations of the benchmarking of the pallet.
 pub trait BenchmarkingConfig {
 	/// The maximum number of validators to use.
diff --git a/frame/staking/src/migrations.rs b/frame/staking/src/migrations.rs
index 0a27290daacd8..669e706e86845 100644
--- a/frame/staking/src/migrations.rs
+++ b/frame/staking/src/migrations.rs
@@ -14,7 +14,8 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 
-//! Storage migrations for the Staking pallet.
+//! Storage migrations for the Staking pallet. The changelog for this is maintained at
+//! [CHANGELOG.md](https://github.com/paritytech/substrate/blob/master/frame/staking/CHANGELOG.md).
 
 use super::*;
 use frame_election_provider_support::SortedListProvider;
@@ -57,6 +58,49 @@ impl Default for ObsoleteReleases {
 #[storage_alias]
 type StorageVersion<T: Config> = StorageValue<Pallet<T>, ObsoleteReleases, ValueQuery>;
 
+/// Migration of era exposure storage items to paged exposures.
+/// Changelog: [v14.](https://github.com/paritytech/substrate/blob/ankan/paged-rewards-rebased2/frame/staking/CHANGELOG.md#14)
+pub mod v14 {
+	use super::*;
+
+	pub struct MigrateToV14<T>(sp_std::marker::PhantomData<T>);
+	impl<T: Config> OnRuntimeUpgrade for MigrateToV14<T> {
+		fn on_runtime_upgrade() -> Weight {
+			let current = Pallet::<T>::current_storage_version();
+			let on_chain = Pallet::<T>::on_chain_storage_version();
+
+			if current == 14 && on_chain == 13 {
+				current.put::<Pallet<T>>();
+
+				log!(info, "v14 applied successfully.");
+				T::DbWeight::get().reads_writes(1, 1)
+			} else {
+				log!(warn, "v14 not applied.");
+				T::DbWeight::get().reads(1)
+			}
+		}
+
+		#[cfg(feature = "try-runtime")]
+		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
+			frame_support::ensure!(
+				Pallet::<T>::on_chain_storage_version() == 13,
+				"Required v13 before upgrading to v14."
+			);
+
+			Ok(Default::default())
+		}
+
+		#[cfg(feature = "try-runtime")]
+		fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
+			frame_support::ensure!(
+				Pallet::<T>::on_chain_storage_version() == 14,
+				"v14 not applied"
+			);
+			Ok(())
+		}
+	}
+}
+
 pub mod v13 {
 	use super::*;
 
@@ -112,9 +156,9 @@ pub mod v12 {
 	#[storage_alias]
 	type HistoryDepth<T: Config> = StorageValue<Pallet<T>, u32, ValueQuery>;
 
-	/// Clean up `HistoryDepth` from storage.
+	/// Clean up `T::HistoryDepth` from storage.
 	///
-	/// We will be depending on the configurable value of `HistoryDepth` post
+	/// We will be depending on the configurable value of `T::HistoryDepth` post
 	/// this release.
 	pub struct MigrateToV12<T>(sp_std::marker::PhantomData<T>);
 	impl<T: Config> OnRuntimeUpgrade for MigrateToV12<T> {
diff --git a/frame/staking/src/mock.rs b/frame/staking/src/mock.rs
index cf08f8be1f27d..adb7e03ba3fdd 100644
--- a/frame/staking/src/mock.rs
+++ b/frame/staking/src/mock.rs
@@ -25,8 +25,8 @@ use frame_election_provider_support::{
 use frame_support::{
 	assert_ok, ord_parameter_types, parameter_types,
 	traits::{
-		ConstU32, ConstU64, Currency, EitherOfDiverse, FindAuthor, Get, Hooks, Imbalance,
-		OnUnbalanced, OneSessionHandler,
+		ConstU64, Currency, EitherOfDiverse, FindAuthor, Get, Hooks, Imbalance, OnUnbalanced,
+		OneSessionHandler,
 	},
 	weights::constants::RocksDbWeight,
 };
@@ -232,6 +232,7 @@ const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] =
 parameter_types! {
 	pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
 	pub static HistoryDepth: u32 = 80;
+	pub static MaxExposurePageSize: u32 = 64;
 	pub static MaxUnlockingChunks: u32 = 32;
 	pub static RewardOnUnbalanceWasCalled: bool = false;
 	pub static MaxWinners: u32 = 100;
@@ -299,7 +300,7 @@ impl crate::pallet::pallet::Config for Test {
 	type SessionInterface = Self;
 	type EraPayout = ConvertCurve<RewardCurve>;
 	type NextNewSession = Session;
-	type MaxNominatorRewardedPerValidator = ConstU32<64>;
+	type MaxExposurePageSize = MaxExposurePageSize;
 	type OffendingValidatorsThreshold = OffendingValidatorsThreshold;
 	type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
 	type GenesisElectionProvider = Self::ElectionProvider;
@@ -755,7 +756,7 @@ pub(crate) fn on_offence_now(
 pub(crate) fn add_slash(who: &AccountId) {
 	on_offence_now(
 		&[OffenceDetails {
-			offender: (*who, Staking::eras_stakers(active_era(), *who)),
+			offender: (*who, Staking::eras_stakers(active_era(), who)),
 			reporters: vec![],
 		}],
 		&[Perbill::from_percent(10)],
@@ -773,7 +774,14 @@ pub(crate) fn make_all_reward_payment(era: EraIndex) {
 	// reward validators
 	for validator_controller in validators_with_reward.iter().filter_map(Staking::bonded) {
 		let ledger = <Ledger<Test>>::get(&validator_controller).unwrap();
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), ledger.stash, era));
+		for page in 0..EraInfo::<Test>::get_page_count(era, &ledger.stash) {
+			assert_ok!(Staking::payout_stakers_by_page(
+				RuntimeOrigin::signed(1337),
+				ledger.stash,
+				era,
+				page
+			));
+		}
 	}
 }
 
diff --git a/frame/staking/src/pallet/impls.rs b/frame/staking/src/pallet/impls.rs
index e0f5c95587818..d0537a66add78 100644
--- a/frame/staking/src/pallet/impls.rs
+++ b/frame/staking/src/pallet/impls.rs
@@ -27,8 +27,8 @@ use frame_support::{
 	dispatch::WithPostDispatchInfo,
 	pallet_prelude::*,
 	traits::{
-		Currency, Defensive, DefensiveResult, EstimateNextNewSession, Get, Imbalance,
-		LockableCurrency, OnUnbalanced, TryCollect, UnixTime, WithdrawReasons,
+		Currency, Defensive, EstimateNextNewSession, Get, Imbalance, Len, LockableCurrency,
+		OnUnbalanced, TryCollect, UnixTime, WithdrawReasons,
 	},
 	weights::Weight,
 };
@@ -41,15 +41,15 @@ use sp_runtime::{
 use sp_staking::{
 	currency_to_vote::CurrencyToVote,
 	offence::{DisableStrategy, OffenceDetails, OnOffenceHandler},
-	EraIndex, SessionIndex, Stake, StakingInterface,
+	EraIndex, PageIndex, SessionIndex, Stake, StakingInterface,
 };
 use sp_std::prelude::*;
 
 use crate::{
 	election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo,
-	BalanceOf, EraPayout, Exposure, ExposureOf, Forcing, IndividualExposure, MaxNominationsOf,
-	MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination,
-	SessionInterface, StakingLedger, ValidatorPrefs,
+	BalanceOf, EraInfo, EraPayout, Exposure, ExposureOf, Forcing, IndividualExposure,
+	MaxNominationsOf, MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf,
+	RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs,
 };
 
 use super::{pallet::*, STAKING_ID};
@@ -143,12 +143,31 @@ impl<T: Config> Pallet<T> {
 	pub(super) fn do_payout_stakers(
 		validator_stash: T::AccountId,
 		era: EraIndex,
+	) -> DispatchResultWithPostInfo {
+		let controller = Self::bonded(&validator_stash).ok_or_else(|| {
+			Error::<T>::NotStash.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
+		})?;
+		let ledger = <Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController)?;
+		let page = EraInfo::<T>::get_next_claimable_page(era, &validator_stash, &ledger)
+			.ok_or_else(|| {
+				Error::<T>::AlreadyClaimed
+					.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
+			})?;
+
+		Self::do_payout_stakers_by_page(validator_stash, era, page)
+	}
+
+	pub(super) fn do_payout_stakers_by_page(
+		validator_stash: T::AccountId,
+		era: EraIndex,
+		page: PageIndex,
 	) -> DispatchResultWithPostInfo {
 		// Validate input data
 		let current_era = CurrentEra::<T>::get().ok_or_else(|| {
 			Error::<T>::InvalidEraToReward
 				.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
 		})?;
+
 		let history_depth = T::HistoryDepth::get();
 		ensure!(
 			era <= current_era && era >= current_era.saturating_sub(history_depth),
@@ -156,8 +175,13 @@ impl<T: Config> Pallet<T> {
 				.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
 		);
 
+		ensure!(
+			page < EraInfo::<T>::get_page_count(era, &validator_stash),
+			Error::<T>::InvalidPage.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
+		);
+
 		// Note: if era has no reward to be claimed, era may be future. better not to update
-		// `ledger.claimed_rewards` in this case.
+		// `ledger.legacy_claimed_rewards` in this case.
 		let era_payout = <ErasValidatorReward<T>>::get(&era).ok_or_else(|| {
 			Error::<T>::InvalidEraToReward
 				.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
@@ -168,29 +192,28 @@ impl<T: Config> Pallet<T> {
 		})?;
 		let mut ledger = <Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController)?;
 
+		// clean up older claimed rewards
 		ledger
-			.claimed_rewards
+			.legacy_claimed_rewards
 			.retain(|&x| x >= current_era.saturating_sub(history_depth));
+		<Ledger<T>>::insert(&controller, &ledger);
 
-		match ledger.claimed_rewards.binary_search(&era) {
-			Ok(_) =>
-				return Err(Error::<T>::AlreadyClaimed
-					.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))),
-			Err(pos) => ledger
-				.claimed_rewards
-				.try_insert(pos, era)
-				// Since we retain era entries in `claimed_rewards` only upto
-				// `HistoryDepth`, following bound is always expected to be
-				// satisfied.
-				.defensive_map_err(|_| Error::<T>::BoundNotMet)?,
+		if EraInfo::<T>::is_rewards_claimed_with_legacy_fallback(era, &ledger, &ledger.stash, page)
+		{
+			return Err(Error::<T>::AlreadyClaimed
+				.with_weight(T::WeightInfo::payout_stakers_alive_staked(0)))
+		} else {
+			EraInfo::<T>::set_rewards_as_claimed(era, &ledger.stash, page);
 		}
 
-		let exposure = <ErasStakersClipped<T>>::get(&era, &ledger.stash);
+		let exposure =
+			EraInfo::<T>::get_paged_exposure(era, &ledger.stash, page).ok_or_else(|| {
+				Error::<T>::InvalidEraToReward
+					.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
+			})?;
 
 		// Input data seems good, no errors allowed after this point
 
-		<Ledger<T>>::insert(&controller, &ledger);
-
 		// Get Era reward points. It has TOTAL and INDIVIDUAL
 		// Find the fraction of the era reward that belongs to the validator
 		// Take that fraction of the eras rewards to split to nominator and validator
@@ -219,15 +242,17 @@ impl<T: Config> Pallet<T> {
 		// This is how much validator + nominators are entitled to.
 		let validator_total_payout = validator_total_reward_part * era_payout;
 
-		let validator_prefs = Self::eras_validator_prefs(&era, &validator_stash);
-		// Validator first gets a cut off the top.
-		let validator_commission = validator_prefs.commission;
-		let validator_commission_payout = validator_commission * validator_total_payout;
+		let validator_commission = EraInfo::<T>::get_validator_commission(era, &ledger.stash);
+		// total commission validator takes across all nominator pages
+		let validator_total_commission_payout = validator_commission * validator_total_payout;
 
-		let validator_leftover_payout = validator_total_payout - validator_commission_payout;
+		let validator_leftover_payout = validator_total_payout - validator_total_commission_payout;
 		// Now let's calculate how this is split to the validator.
-		let validator_exposure_part = Perbill::from_rational(exposure.own, exposure.total);
+		let validator_exposure_part = Perbill::from_rational(exposure.own(), exposure.total());
 		let validator_staking_payout = validator_exposure_part * validator_leftover_payout;
+		let page_stake_part = Perbill::from_rational(exposure.page_total(), exposure.total());
+		// validator commission is paid out in fraction across pages proportional to the page stake.
+		let validator_commission_payout = page_stake_part * validator_total_commission_payout;
 
 		Self::deposit_event(Event::<T>::PayoutStarted {
 			era_index: era,
@@ -253,8 +278,8 @@ impl<T: Config> Pallet<T> {
 
 		// Lets now calculate how this is split to the nominators.
 		// Reward only the clipped exposures. Note this is not necessarily sorted.
-		for nominator in exposure.others.iter() {
-			let nominator_exposure_part = Perbill::from_rational(nominator.value, exposure.total);
+		for nominator in exposure.others().iter() {
+			let nominator_exposure_part = Perbill::from_rational(nominator.value, exposure.total());
 
 			let nominator_reward: BalanceOf<T> =
 				nominator_exposure_part * validator_leftover_payout;
@@ -270,7 +295,8 @@ impl<T: Config> Pallet<T> {
 		}
 
 		T::Reward::on_unbalanced(total_imbalance);
-		debug_assert!(nominator_payout_count <= T::MaxNominatorRewardedPerValidator::get());
+		debug_assert!(nominator_payout_count <= T::MaxExposurePageSize::get());
+
 		Ok(Some(T::WeightInfo::payout_stakers_alive_staked(nominator_payout_count)).into())
 	}
 
@@ -570,31 +596,24 @@ impl<T: Config> Pallet<T> {
 		>,
 		new_planned_era: EraIndex,
 	) -> BoundedVec<T::AccountId, MaxWinnersOf<T>> {
-		let elected_stashes: BoundedVec<_, MaxWinnersOf<T>> = exposures
-			.iter()
-			.cloned()
-			.map(|(x, _)| x)
-			.collect::<Vec<_>>()
-			.try_into()
-			.expect("since we only map through exposures, size of elected_stashes is always same as exposures; qed");
-
-		// Populate stakers, exposures, and the snapshot of validator prefs.
+		// Populate elected stash, stakers, exposures, and the snapshot of validator prefs.
 		let mut total_stake: BalanceOf<T> = Zero::zero();
+		let mut elected_stashes = Vec::with_capacity(exposures.len());
+
 		exposures.into_iter().for_each(|(stash, exposure)| {
+			// build elected stash
+			elected_stashes.push(stash.clone());
+			// accumulate total stake
 			total_stake = total_stake.saturating_add(exposure.total);
-			<ErasStakers<T>>::insert(new_planned_era, &stash, &exposure);
-
-			let mut exposure_clipped = exposure;
-			let clipped_max_len = T::MaxNominatorRewardedPerValidator::get() as usize;
-			if exposure_clipped.others.len() > clipped_max_len {
-				exposure_clipped.others.sort_by(|a, b| a.value.cmp(&b.value).reverse());
-				exposure_clipped.others.truncate(clipped_max_len);
-			}
-			<ErasStakersClipped<T>>::insert(&new_planned_era, &stash, exposure_clipped);
+			// store staker exposure for this era
+			EraInfo::<T>::set_exposure(new_planned_era, &stash, exposure);
 		});
 
-		// Insert current era staking information
-		<ErasTotalStake<T>>::insert(&new_planned_era, total_stake);
+		let elected_stashes: BoundedVec<_, MaxWinnersOf<T>> = elected_stashes
+			.try_into()
+			.expect("elected_stashes.len() always equal to exposures.len(); qed");
+
+		EraInfo::<T>::set_total_stake(new_planned_era, total_stake);
 
 		// Collect the pref of all winners.
 		for stash in &elected_stashes {
@@ -677,12 +696,21 @@ impl<T: Config> Pallet<T> {
 
 	/// Clear all era information for given era.
 	pub(crate) fn clear_era_information(era_index: EraIndex) {
+		// FIXME: We can possibly set a reasonable limit since we do this only once per era and
+		// clean up state across multiple blocks.
 		let mut cursor = <ErasStakers<T>>::clear_prefix(era_index, u32::MAX, None);
 		debug_assert!(cursor.maybe_cursor.is_none());
 		cursor = <ErasStakersClipped<T>>::clear_prefix(era_index, u32::MAX, None);
 		debug_assert!(cursor.maybe_cursor.is_none());
 		cursor = <ErasValidatorPrefs<T>>::clear_prefix(era_index, u32::MAX, None);
 		debug_assert!(cursor.maybe_cursor.is_none());
+		cursor = <ClaimedRewards<T>>::clear_prefix(era_index, u32::MAX, None);
+		debug_assert!(cursor.maybe_cursor.is_none());
+		cursor = <ErasStakersPaged<T>>::clear_prefix((era_index,), u32::MAX, None);
+		debug_assert!(cursor.maybe_cursor.is_none());
+		cursor = <ErasStakersOverview<T>>::clear_prefix(era_index, u32::MAX, None);
+		debug_assert!(cursor.maybe_cursor.is_none());
+
 		<ErasValidatorReward<T>>::remove(era_index);
 		<ErasRewardPoints<T>>::remove(era_index);
 		<ErasTotalStake<T>>::remove(era_index);
@@ -1021,6 +1049,18 @@ impl<T: Config> Pallet<T> {
 			DispatchClass::Mandatory,
 		);
 	}
+
+	/// Returns full exposure of a validator for a given era.
+	///
+	/// History note: This used to be a getter for old storage item `ErasStakers` deprecated in v14.
+	/// Since this function is used in the codebase at various places, we kept it as a custom getter
+	/// that takes care of getting the full exposure of the validator in a backward compatible way.
+	pub fn eras_stakers(
+		era: EraIndex,
+		account: &T::AccountId,
+	) -> Exposure<T::AccountId, BalanceOf<T>> {
+		EraInfo::<T>::get_full_exposure(era, account)
+	}
 }
 
 impl<T: Config> Pallet<T> {
@@ -1030,6 +1070,17 @@ impl<T: Config> Pallet<T> {
 	pub fn api_nominations_quota(balance: BalanceOf<T>) -> u32 {
 		T::NominationsQuota::get_quota(balance)
 	}
+
+	pub fn api_eras_stakers(
+		era: EraIndex,
+		account: T::AccountId,
+	) -> Exposure<T::AccountId, BalanceOf<T>> {
+		Self::eras_stakers(era, &account)
+	}
+
+	pub fn api_eras_stakers_page_count(era: EraIndex, account: T::AccountId) -> PageIndex {
+		EraInfo::<T>::get_page_count(era, &account)
+	}
 }
 
 impl<T: Config> ElectionDataProvider for Pallet<T> {
@@ -1121,7 +1172,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> {
 				active: stake,
 				total: stake,
 				unlocking: Default::default(),
-				claimed_rewards: Default::default(),
+				legacy_claimed_rewards: Default::default(),
 			},
 		);
 
@@ -1139,7 +1190,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> {
 				active: stake,
 				total: stake,
 				unlocking: Default::default(),
-				claimed_rewards: Default::default(),
+				legacy_claimed_rewards: Default::default(),
 			},
 		);
 		Self::do_add_validator(
@@ -1180,7 +1231,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> {
 					active: stake,
 					total: stake,
 					unlocking: Default::default(),
-					claimed_rewards: Default::default(),
+					legacy_claimed_rewards: Default::default(),
 				},
 			);
 			Self::do_add_validator(
@@ -1201,7 +1252,7 @@ impl<T: Config> ElectionDataProvider for Pallet<T> {
 					active: stake,
 					total: stake,
 					unlocking: Default::default(),
-					claimed_rewards: Default::default(),
+					legacy_claimed_rewards: Default::default(),
 				},
 			);
 			Self::do_add_nominator(
@@ -1631,31 +1682,12 @@ impl<T: Config> StakingInterface for Pallet<T> {
 		MinValidatorBond::<T>::get()
 	}
 
-	fn desired_validator_count() -> u32 {
-		ValidatorCount::<T>::get()
-	}
-
-	fn election_ongoing() -> bool {
-		T::ElectionProvider::ongoing()
-	}
-
-	fn force_unstake(who: Self::AccountId) -> sp_runtime::DispatchResult {
-		let num_slashing_spans = Self::slashing_spans(&who).map_or(0, |s| s.iter().count() as u32);
-		Self::force_unstake(RawOrigin::Root.into(), who.clone(), num_slashing_spans)
-	}
-
 	fn stash_by_ctrl(controller: &Self::AccountId) -> Result<Self::AccountId, DispatchError> {
 		Self::ledger(controller)
 			.map(|l| l.stash)
 			.ok_or(Error::<T>::NotController.into())
 	}
 
-	fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool {
-		ErasStakers::<T>::iter_prefix(era).any(|(validator, exposures)| {
-			validator == *who || exposures.others.iter().any(|i| i.who == *who)
-		})
-	}
-
 	fn bonding_duration() -> EraIndex {
 		T::BondingDuration::get()
 	}
@@ -1671,15 +1703,22 @@ impl<T: Config> StakingInterface for Pallet<T> {
 			.ok_or(Error::<T>::NotStash.into())
 	}
 
-	fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult {
-		Self::bond_extra(RawOrigin::Signed(who.clone()).into(), extra)
+	fn bond(
+		who: &Self::AccountId,
+		value: Self::Balance,
+		payee: &Self::AccountId,
+	) -> DispatchResult {
+		Self::bond(
+			RawOrigin::Signed(who.clone()).into(),
+			value,
+			RewardDestination::Account(payee.clone()),
+		)
 	}
 
-	fn unbond(who: &Self::AccountId, value: Self::Balance) -> DispatchResult {
+	fn nominate(who: &Self::AccountId, targets: Vec<Self::AccountId>) -> DispatchResult {
 		let ctrl = Self::bonded(who).ok_or(Error::<T>::NotStash)?;
-		Self::unbond(RawOrigin::Signed(ctrl).into(), value)
-			.map_err(|with_post| with_post.error)
-			.map(|_| ())
+		let targets = targets.into_iter().map(T::Lookup::unlookup).collect::<Vec<_>>();
+		Self::nominate(RawOrigin::Signed(ctrl).into(), targets)
 	}
 
 	fn chill(who: &Self::AccountId) -> DispatchResult {
@@ -1689,6 +1728,17 @@ impl<T: Config> StakingInterface for Pallet<T> {
 		Self::chill(RawOrigin::Signed(ctrl).into())
 	}
 
+	fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult {
+		Self::bond_extra(RawOrigin::Signed(who.clone()).into(), extra)
+	}
+
+	fn unbond(who: &Self::AccountId, value: Self::Balance) -> DispatchResult {
+		let ctrl = Self::bonded(who).ok_or(Error::<T>::NotStash)?;
+		Self::unbond(RawOrigin::Signed(ctrl).into(), value)
+			.map_err(|with_post| with_post.error)
+			.map(|_| ())
+	}
+
 	fn withdraw_unbonded(
 		who: Self::AccountId,
 		num_slashing_spans: u32,
@@ -1699,24 +1749,24 @@ impl<T: Config> StakingInterface for Pallet<T> {
 			.map_err(|with_post| with_post.error)
 	}
 
-	fn bond(
-		who: &Self::AccountId,
-		value: Self::Balance,
-		payee: &Self::AccountId,
-	) -> DispatchResult {
-		Self::bond(
-			RawOrigin::Signed(who.clone()).into(),
-			value,
-			RewardDestination::Account(payee.clone()),
-		)
+	fn desired_validator_count() -> u32 {
+		ValidatorCount::<T>::get()
 	}
 
-	fn nominate(who: &Self::AccountId, targets: Vec<Self::AccountId>) -> DispatchResult {
-		let ctrl = Self::bonded(who).ok_or(Error::<T>::NotStash)?;
-		let targets = targets.into_iter().map(T::Lookup::unlookup).collect::<Vec<_>>();
-		Self::nominate(RawOrigin::Signed(ctrl).into(), targets)
+	fn election_ongoing() -> bool {
+		T::ElectionProvider::ongoing()
+	}
+
+	fn force_unstake(who: Self::AccountId) -> sp_runtime::DispatchResult {
+		let num_slashing_spans = Self::slashing_spans(&who).map_or(0, |s| s.iter().count() as u32);
+		Self::force_unstake(RawOrigin::Root.into(), who.clone(), num_slashing_spans)
 	}
 
+	fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool {
+		ErasStakers::<T>::iter_prefix(era).any(|(validator, exposures)| {
+			validator == *who || exposures.others.iter().any(|i| i.who == *who)
+		})
+	}
 	fn status(
 		who: &Self::AccountId,
 	) -> Result<sp_staking::StakerStatus<Self::AccountId>, DispatchError> {
@@ -1757,12 +1807,16 @@ impl<T: Config> StakingInterface for Pallet<T> {
 				.map(|(who, value)| IndividualExposure { who: who.clone(), value: value.clone() })
 				.collect::<Vec<_>>();
 			let exposure = Exposure { total: Default::default(), own: Default::default(), others };
-			<ErasStakers<T>>::insert(&current_era, &stash, &exposure);
+			EraInfo::<T>::set_exposure(*current_era, stash, exposure);
 		}
 
 		fn set_current_era(era: EraIndex) {
 			CurrentEra::<T>::put(era);
 		}
+
+		fn max_exposure_page_size() -> PageIndex {
+			T::MaxExposurePageSize::get()
+		}
 	}
 }
 
diff --git a/frame/staking/src/pallet/mod.rs b/frame/staking/src/pallet/mod.rs
index 40a2f5cf73eb1..9190739e74233 100644
--- a/frame/staking/src/pallet/mod.rs
+++ b/frame/staking/src/pallet/mod.rs
@@ -1,6 +1,6 @@
 // This file is part of Substrate.
 
-// Copyright (C) Parity Technologies (UK) Ltd.
+// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd.
 // SPDX-License-Identifier: Apache-2.0
 
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,9 +24,8 @@ use frame_support::{
 	dispatch::Codec,
 	pallet_prelude::*,
 	traits::{
-		Currency, Defensive, DefensiveResult, DefensiveSaturating, EnsureOrigin,
-		EstimateNextNewSession, Get, LockIdentifier, LockableCurrency, OnUnbalanced, TryCollect,
-		UnixTime,
+		Currency, Defensive, DefensiveSaturating, EnsureOrigin, EstimateNextNewSession, Get,
+		LockIdentifier, LockableCurrency, OnUnbalanced, UnixTime,
 	},
 	weights::Weight,
 	BoundedVec,
@@ -36,7 +35,7 @@ use sp_runtime::{
 	traits::{CheckedSub, SaturatedConversion, StaticLookup, Zero},
 	ArithmeticError, Perbill, Percent,
 };
-use sp_staking::{EraIndex, SessionIndex};
+use sp_staking::{EraIndex, PageIndex, SessionIndex};
 use sp_std::prelude::*;
 
 mod impls;
@@ -45,9 +44,9 @@ pub use impls::*;
 
 use crate::{
 	slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout,
-	EraRewardPoints, Exposure, Forcing, MaxNominationsOf, NegativeImbalanceOf, Nominations,
-	NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, StakingLedger,
-	UnappliedSlash, UnlockChunk, ValidatorPrefs,
+	EraRewardPoints, Exposure, ExposurePage, Forcing, MaxNominationsOf, NegativeImbalanceOf,
+	Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface,
+	StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs,
 };
 
 const STAKING_ID: LockIdentifier = *b"staking ";
@@ -60,12 +59,12 @@ pub(crate) const SPECULATIVE_NUM_SPANS: u32 = 32;
 pub mod pallet {
 	use frame_election_provider_support::ElectionDataProvider;
 
-	use crate::BenchmarkingConfig;
+	use crate::{BenchmarkingConfig, PagedExposureMetadata};
 
 	use super::*;
 
 	/// The current storage version.
-	const STORAGE_VERSION: StorageVersion = StorageVersion::new(13);
+	const STORAGE_VERSION: StorageVersion = StorageVersion::new(14);
 
 	#[pallet::pallet]
 	#[pallet::storage_version(STORAGE_VERSION)]
@@ -137,8 +136,8 @@ pub mod pallet {
 		/// Following information is kept for eras in `[current_era -
 		/// HistoryDepth, current_era]`: `ErasStakers`, `ErasStakersClipped`,
 		/// `ErasValidatorPrefs`, `ErasValidatorReward`, `ErasRewardPoints`,
-		/// `ErasTotalStake`, `ErasStartSessionIndex`,
-		/// `StakingLedger.claimed_rewards`.
+		/// `ErasTotalStake`, `ErasStartSessionIndex`, `ClaimedRewards`, `ErasStakersPaged`,
+		/// `ErasStakersOverview`.
 		///
 		/// Must be more than the number of eras delayed by session.
 		/// I.e. active era must always be in history. I.e. `active_era >
@@ -148,7 +147,7 @@ pub mod pallet {
 		/// this should be set to same value or greater as in storage.
 		///
 		/// Note: `HistoryDepth` is used as the upper bound for the `BoundedVec`
-		/// item `StakingLedger.claimed_rewards`. Setting this value lower than
+		/// item `StakingLedger.legacy_claimed_rewards`. Setting this value lower than
 		/// the existing value can lead to inconsistencies in the
 		/// `StakingLedger` and will need to be handled properly in a migration.
 		/// The test `reducing_history_depth_abrupt` shows this effect.
@@ -201,12 +200,19 @@ pub mod pallet {
 		/// guess.
 		type NextNewSession: EstimateNextNewSession<BlockNumberFor<Self>>;
 
-		/// The maximum number of nominators rewarded for each validator.
+		/// The maximum size of each `T::ExposurePage`.
 		///
-		/// For each validator only the `$MaxNominatorRewardedPerValidator` biggest stakers can
-		/// claim their reward. This used to limit the i/o cost for the nominator payout.
+		/// An `ExposurePage` is weakly bounded to a maximum of `MaxExposurePageSize`
+		/// nominators.
+		///
+		/// For older non-paged exposure, a reward payout was restricted to the top
+		/// `MaxExposurePageSize` nominators. This is to limit the i/o cost for the
+		/// nominator payout.
+		///
+		/// Note: `MaxExposurePageSize` is used to bound `ClaimedRewards` and is unsafe to reduce
+		/// without handling it in a migration.
 		#[pallet::constant]
-		type MaxNominatorRewardedPerValidator: Get<u32>;
+		type MaxExposurePageSize: Get<u32>;
 
 		/// The fraction of the validator set that is safe to be offending.
 		/// After the threshold is reached a new era will be forced.
@@ -389,7 +395,7 @@ pub mod pallet {
 	#[pallet::getter(fn active_era)]
 	pub type ActiveEra<T> = StorageValue<_, ActiveEraInfo>;
 
-	/// The session index at which the era start for the last `HISTORY_DEPTH` eras.
+	/// The session index at which the era start for the last [`Config::HistoryDepth`] eras.
 	///
 	/// Note: This tracks the starting session (i.e. session index when era start being active)
 	/// for the eras in `[CurrentEra - HISTORY_DEPTH, CurrentEra]`.
@@ -401,10 +407,11 @@ pub mod pallet {
 	///
 	/// This is keyed first by the era index to allow bulk deletion and then the stash account.
 	///
-	/// Is it removed after `HISTORY_DEPTH` eras.
+	/// Is it removed after [`Config::HistoryDepth`] eras.
 	/// If stakers hasn't been set or has been removed then empty exposure is returned.
+	///
+	/// Note: Deprecated since v14. Use `EraInfo` instead to work with exposures.
 	#[pallet::storage]
-	#[pallet::getter(fn eras_stakers)]
 	#[pallet::unbounded]
 	pub type ErasStakers<T: Config> = StorageDoubleMap<
 		_,
@@ -416,17 +423,45 @@ pub mod pallet {
 		ValueQuery,
 	>;
 
+	/// Summary of validator exposure at a given era.
+	///
+	/// This contains the total stake in support of the validator and their own stake. In addition,
+	/// it can also be used to get the number of nominators backing this validator and the number of
+	/// exposure pages they are divided into. The page count is useful to determine the number of
+	/// pages of rewards that needs to be claimed.
+	///
+	/// This is keyed first by the era index to allow bulk deletion and then the stash account.
+	/// Should only be accessed through `EraInfo`.
+	///
+	/// Is it removed after [`Config::HistoryDepth`] eras.
+	/// If stakers hasn't been set or has been removed then empty overview is returned.
+	#[pallet::storage]
+	pub type ErasStakersOverview<T: Config> = StorageDoubleMap<
+		_,
+		Twox64Concat,
+		EraIndex,
+		Twox64Concat,
+		T::AccountId,
+		PagedExposureMetadata<BalanceOf<T>>,
+		OptionQuery,
+	>;
+
 	/// Clipped Exposure of validator at era.
 	///
+	/// Note: This is deprecated, should be used as read-only and will be removed in the future.
+	/// New `Exposure`s are stored in a paged manner in `ErasStakersPaged` instead.
+	///
 	/// This is similar to [`ErasStakers`] but number of nominators exposed is reduced to the
-	/// `T::MaxNominatorRewardedPerValidator` biggest stakers.
+	/// `T::MaxExposurePageSize` biggest stakers.
 	/// (Note: the field `total` and `own` of the exposure remains unchanged).
 	/// This is used to limit the i/o cost for the nominator payout.
 	///
 	/// This is keyed fist by the era index to allow bulk deletion and then the stash account.
 	///
-	/// Is it removed after `HISTORY_DEPTH` eras.
+	/// It is removed after [`Config::HistoryDepth`] eras.
 	/// If stakers hasn't been set or has been removed then empty exposure is returned.
+	///
+	/// Note: Deprecated since v14. Use `EraInfo` instead to work with exposures.
 	#[pallet::storage]
 	#[pallet::unbounded]
 	#[pallet::getter(fn eras_stakers_clipped)]
@@ -440,11 +475,49 @@ pub mod pallet {
 		ValueQuery,
 	>;
 
+	/// Paginated exposure of a validator at given era.
+	///
+	/// This is keyed first by the era index to allow bulk deletion, then stash account and finally
+	/// the page. Should only be accessed through `EraInfo`.
+	///
+	/// This is cleared after [`Config::HistoryDepth`] eras.
+	#[pallet::storage]
+	#[pallet::unbounded]
+	pub type ErasStakersPaged<T: Config> = StorageNMap<
+		_,
+		(
+			NMapKey<Twox64Concat, EraIndex>,
+			NMapKey<Twox64Concat, T::AccountId>,
+			NMapKey<Twox64Concat, PageIndex>,
+		),
+		ExposurePage<T::AccountId, BalanceOf<T>>,
+		OptionQuery,
+	>;
+
+	/// History of claimed paged rewards by era and validator.
+	///
+	/// This is keyed by era and validator stash which maps to the set of page indexes which have
+	/// been claimed.
+	///
+	/// It is removed after [`Config::HistoryDepth`] eras.
+	#[pallet::storage]
+	#[pallet::getter(fn claimed_rewards)]
+	#[pallet::unbounded]
+	pub type ClaimedRewards<T: Config> = StorageDoubleMap<
+		_,
+		Twox64Concat,
+		EraIndex,
+		Twox64Concat,
+		T::AccountId,
+		Vec<PageIndex>,
+		ValueQuery,
+	>;
+
 	/// Similar to `ErasStakers`, this holds the preferences of validators.
 	///
 	/// This is keyed first by the era index to allow bulk deletion and then the stash account.
 	///
-	/// Is it removed after `HISTORY_DEPTH` eras.
+	/// Is it removed after [`Config::HistoryDepth`] eras.
 	// If prefs hasn't been set or has been removed then 0 commission is returned.
 	#[pallet::storage]
 	#[pallet::getter(fn eras_validator_prefs)]
@@ -458,14 +531,14 @@ pub mod pallet {
 		ValueQuery,
 	>;
 
-	/// The total validator era payout for the last `HISTORY_DEPTH` eras.
+	/// The total validator era payout for the last [`Config::HistoryDepth`] eras.
 	///
 	/// Eras that haven't finished yet or has been removed doesn't have reward.
 	#[pallet::storage]
 	#[pallet::getter(fn eras_validator_reward)]
 	pub type ErasValidatorReward<T: Config> = StorageMap<_, Twox64Concat, EraIndex, BalanceOf<T>>;
 
-	/// Rewards for the last `HISTORY_DEPTH` eras.
+	/// Rewards for the last [`Config::HistoryDepth`] eras.
 	/// If reward hasn't been set or has been removed then 0 reward is returned.
 	#[pallet::storage]
 	#[pallet::unbounded]
@@ -473,7 +546,7 @@ pub mod pallet {
 	pub type ErasRewardPoints<T: Config> =
 		StorageMap<_, Twox64Concat, EraIndex, EraRewardPoints<T::AccountId>, ValueQuery>;
 
-	/// The total amount staked for the last `HISTORY_DEPTH` eras.
+	/// The total amount staked for the last [`Config::HistoryDepth`] eras.
 	/// If total hasn't been set or has been removed then 0 stake is returned.
 	#[pallet::storage]
 	#[pallet::getter(fn eras_total_stake)]
@@ -738,6 +811,8 @@ pub mod pallet {
 		NotSortedAndUnique,
 		/// Rewards for this era have already been claimed for this validator.
 		AlreadyClaimed,
+		/// No nominators exist on this page.
+		InvalidPage,
 		/// Incorrect previous history depth input provided.
 		IncorrectHistoryDepth,
 		/// Incorrect number of slashing spans provided.
@@ -859,10 +934,6 @@ pub mod pallet {
 			<Bonded<T>>::insert(&stash, &stash);
 			<Payee<T>>::insert(&stash, payee);
 
-			let current_era = CurrentEra::<T>::get().unwrap_or(0);
-			let history_depth = T::HistoryDepth::get();
-			let last_reward_era = current_era.saturating_sub(history_depth);
-
 			let stash_balance = T::Currency::free_balance(&stash);
 			let value = value.min(stash_balance);
 			Self::deposit_event(Event::<T>::Bonded { stash: stash.clone(), amount: value });
@@ -871,12 +942,7 @@ pub mod pallet {
 				total: value,
 				active: value,
 				unlocking: Default::default(),
-				claimed_rewards: (last_reward_era..current_era)
-					.try_collect()
-					// Since last_reward_era is calculated as `current_era -
-					// HistoryDepth`, following bound is always expected to be
-					// satisfied.
-					.defensive_map_err(|_| Error::<T>::BoundNotMet)?,
+				legacy_claimed_rewards: Default::default(),
 			};
 			Self::update_ledger(&controller_to_be_deprecated, &item);
 			Ok(())
@@ -1460,21 +1526,21 @@ pub mod pallet {
 			Ok(())
 		}
 
-		/// Pay out all the stakers behind a single validator for a single era.
+		/// Pay out next page of the stakers behind a validator for the given era.
 		///
-		/// - `validator_stash` is the stash account of the validator. Their nominators, up to
-		///   `T::MaxNominatorRewardedPerValidator`, will also receive their rewards.
+		/// - `validator_stash` is the stash account of the validator.
 		/// - `era` may be any era between `[current_era - history_depth; current_era]`.
 		///
 		/// The origin of this call must be _Signed_. Any account can call this function, even if
 		/// it is not one of the stakers.
 		///
-		/// ## Complexity
-		/// - At most O(MaxNominatorRewardedPerValidator).
+		/// The reward payout could be paged in case there are too many nominators backing the
+		/// `validator_stash`. This call will payout unpaid pages in an ascending order. To claim a
+		/// specific page, use `payout_stakers_by_page`.`
+		///
+		/// If all pages are claimed, it returns an error `InvalidPage`.
 		#[pallet::call_index(18)]
-		#[pallet::weight(T::WeightInfo::payout_stakers_alive_staked(
-			T::MaxNominatorRewardedPerValidator::get()
-		))]
+		#[pallet::weight(T::WeightInfo::payout_stakers_alive_staked(T::MaxExposurePageSize::get()))]
 		pub fn payout_stakers(
 			origin: OriginFor<T>,
 			validator_stash: T::AccountId,
@@ -1776,6 +1842,35 @@ pub mod pallet {
 			MinCommission::<T>::put(new);
 			Ok(())
 		}
+
+		/// Pay out a page of the stakers behind a validator for the given era and page.
+		///
+		/// - `validator_stash` is the stash account of the validator.
+		/// - `era` may be any era between `[current_era - history_depth; current_era]`.
+		/// - `page` is the page index of nominators to pay out with value between 0 and
+		///   `num_nominators / T::MaxExposurePageSize`.
+		///
+		/// The origin of this call must be _Signed_. Any account can call this function, even if
+		/// it is not one of the stakers.
+		///
+		/// If a validator has more than [`Config::MaxExposurePageSize`] nominators backing
+		/// them, then the list of nominators is paged, with each page being capped at
+		/// [`Config::MaxExposurePageSize`.] If a validator has more than one page of nominators,
+		/// the call needs to be made for each page separately in order for all the nominators
+		/// backing a validator to receive the reward. The nominators are not sorted across pages
+		/// and so it should not be assumed the highest staker would be on the topmost page and vice
+		/// versa. If rewards are not claimed in [`Config::HistoryDepth`] eras, they are lost.
+		#[pallet::call_index(26)]
+		#[pallet::weight(T::WeightInfo::payout_stakers_alive_staked(T::MaxExposurePageSize::get()))]
+		pub fn payout_stakers_by_page(
+			origin: OriginFor<T>,
+			validator_stash: T::AccountId,
+			era: EraIndex,
+			page: PageIndex,
+		) -> DispatchResultWithPostInfo {
+			ensure_signed(origin)?;
+			Self::do_payout_stakers_by_page(validator_stash, era, page)
+		}
 	}
 }
 
diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs
index 29539cbb84cf7..8efb31b9512fc 100644
--- a/frame/staking/src/tests.rs
+++ b/frame/staking/src/tests.rs
@@ -28,6 +28,7 @@ use frame_support::{
 	pallet_prelude::*,
 	traits::{Currency, Get, ReservableCurrency},
 };
+
 use mock::*;
 use pallet_balances::Error as BalancesError;
 use sp_runtime::{
@@ -157,7 +158,7 @@ fn basic_setup_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			}
 		);
 		// Account 21 controls its own stash, which is 200 * balance_factor units
@@ -168,7 +169,7 @@ fn basic_setup_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		// Account 1 does not control any stash
@@ -191,13 +192,13 @@ fn basic_setup_works() {
 				total: 500,
 				active: 500,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		assert_eq!(Staking::nominators(101).unwrap().targets, vec![11, 21]);
 
 		assert_eq!(
-			Staking::eras_stakers(active_era(), 11),
+			Staking::eras_stakers(active_era(), &11),
 			Exposure {
 				total: 1125,
 				own: 1000,
@@ -205,7 +206,7 @@ fn basic_setup_works() {
 			},
 		);
 		assert_eq!(
-			Staking::eras_stakers(active_era(), 21),
+			Staking::eras_stakers(active_era(), &21),
 			Exposure {
 				total: 1375,
 				own: 1000,
@@ -466,7 +467,7 @@ fn staking_should_work() {
 				total: 1500,
 				active: 1500,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![0],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		// e.g. it cannot reserve more than 500 that it has free from the total 2000
@@ -521,7 +522,7 @@ fn less_than_needed_candidates_works() {
 
 			// But the exposure is updated in a simple way. No external votes exists.
 			// This is purely self-vote.
-			assert!(ErasStakers::<Test>::iter_prefix_values(active_era())
+			assert!(ErasStakersPaged::<Test>::iter_prefix_values((active_era(),))
 				.all(|exposure| exposure.others.is_empty()));
 		});
 }
@@ -639,9 +640,9 @@ fn nominating_and_rewards_should_work() {
 			assert_eq!(Balances::total_balance(&21), initial_balance_21 + total_payout_0 / 2);
 			initial_balance_21 = Balances::total_balance(&21);
 
-			assert_eq!(ErasStakers::<Test>::iter_prefix_values(active_era()).count(), 2);
+			assert_eq!(ErasStakersPaged::<Test>::iter_prefix_values((active_era(),)).count(), 2);
 			assert_eq!(
-				Staking::eras_stakers(active_era(), 11),
+				Staking::eras_stakers(active_era(), &11),
 				Exposure {
 					total: 1000 + 800,
 					own: 1000,
@@ -652,7 +653,7 @@ fn nominating_and_rewards_should_work() {
 				},
 			);
 			assert_eq!(
-				Staking::eras_stakers(active_era(), 21),
+				Staking::eras_stakers(active_era(), &21),
 				Exposure {
 					total: 1000 + 1200,
 					own: 1000,
@@ -712,7 +713,7 @@ fn nominators_also_get_slashed_pro_rata() {
 	ExtBuilder::default().build_and_execute(|| {
 		mock::start_active_era(1);
 		let slash_percent = Perbill::from_percent(5);
-		let initial_exposure = Staking::eras_stakers(active_era(), 11);
+		let initial_exposure = Staking::eras_stakers(active_era(), &11);
 		// 101 is a nominator for 11
 		assert_eq!(initial_exposure.others.first().unwrap().who, 101);
 
@@ -980,7 +981,7 @@ fn cannot_transfer_staked_balance() {
 		// Confirm account 11 has some free balance
 		assert_eq!(Balances::free_balance(11), 1000);
 		// Confirm account 11 (via controller) is totally staked
-		assert_eq!(Staking::eras_stakers(active_era(), 11).total, 1000);
+		assert_eq!(Staking::eras_stakers(active_era(), &11).total, 1000);
 		// Confirm account 11 cannot transfer as a result
 		assert_noop!(
 			Balances::transfer_allow_death(RuntimeOrigin::signed(11), 21, 1),
@@ -1005,7 +1006,7 @@ fn cannot_transfer_staked_balance_2() {
 		// Confirm account 21 has some free balance
 		assert_eq!(Balances::free_balance(21), 2000);
 		// Confirm account 21 (via controller) is totally staked
-		assert_eq!(Staking::eras_stakers(active_era(), 21).total, 1000);
+		assert_eq!(Staking::eras_stakers(active_era(), &21).total, 1000);
 		// Confirm account 21 can transfer at most 1000
 		assert_noop!(
 			Balances::transfer_allow_death(RuntimeOrigin::signed(21), 21, 1001),
@@ -1024,7 +1025,7 @@ fn cannot_reserve_staked_balance() {
 		// Confirm account 11 has some free balance
 		assert_eq!(Balances::free_balance(11), 1000);
 		// Confirm account 11 (via controller 10) is totally staked
-		assert_eq!(Staking::eras_stakers(active_era(), 11).own, 1000);
+		assert_eq!(Staking::eras_stakers(active_era(), &11).own, 1000);
 		// Confirm account 11 cannot reserve as a result
 		assert_noop!(Balances::reserve(&11, 1), BalancesError::<Test, _>::LiquidityRestrictions);
 
@@ -1053,7 +1054,7 @@ fn reward_destination_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1076,10 +1077,13 @@ fn reward_destination_works() {
 				total: 1000 + total_payout_0,
 				active: 1000 + total_payout_0,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![0],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
+		// (era 0, page 0) is claimed
+		assert_eq!(Staking::claimed_rewards(0, &11), vec![0]);
+
 		// Change RewardDestination to Stash
 		<Payee<Test>>::insert(&11, RewardDestination::Stash);
 
@@ -1094,6 +1098,8 @@ fn reward_destination_works() {
 		assert_eq!(Staking::payee(&11), RewardDestination::Stash);
 		// Check that reward went to the stash account
 		assert_eq!(Balances::free_balance(11), 1000 + total_payout_0 + total_payout_1);
+		// Record this value
+		let recorded_stash_balance = 1000 + total_payout_0 + total_payout_1;
 		// Check that amount at stake is NOT increased
 		assert_eq!(
 			Staking::ledger(&11),
@@ -1102,10 +1108,13 @@ fn reward_destination_works() {
 				total: 1000 + total_payout_0,
 				active: 1000 + total_payout_0,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![0, 1],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
+		// (era 1, page 0) is claimed
+		assert_eq!(Staking::claimed_rewards(1, &11), vec![0]);
+
 		// Change RewardDestination to Controller
 		<Payee<Test>>::insert(&11, RewardDestination::Controller);
 
@@ -1122,7 +1131,7 @@ fn reward_destination_works() {
 		// Check that RewardDestination is Controller
 		assert_eq!(Staking::payee(&11), RewardDestination::Controller);
 		// Check that reward went to the controller account
-		assert_eq!(Balances::free_balance(11), 23150 + total_payout_2);
+		assert_eq!(Balances::free_balance(11), recorded_stash_balance + total_payout_2);
 		// Check that amount at stake is NOT increased
 		assert_eq!(
 			Staking::ledger(&11),
@@ -1131,9 +1140,12 @@ fn reward_destination_works() {
 				total: 1000 + total_payout_0,
 				active: 1000 + total_payout_0,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![0, 1, 2],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
+
+		// (era 2, page 0) is claimed
+		assert_eq!(Staking::claimed_rewards(2, &11), vec![0]);
 	});
 }
 
@@ -1158,7 +1170,7 @@ fn validator_payment_prefs_work() {
 
 		// Compute total payout now for whole duration as other parameter won't change
 		let total_payout_1 = current_total_payout_for_duration(reward_time_per_era());
-		let exposure_1 = Staking::eras_stakers(active_era(), 11);
+		let exposure_1 = Staking::eras_stakers(active_era(), &11);
 		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
 
 		mock::start_active_era(2);
@@ -1191,7 +1203,7 @@ fn bond_extra_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1208,7 +1220,7 @@ fn bond_extra_works() {
 				total: 1000 + 100,
 				active: 1000 + 100,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1222,7 +1234,7 @@ fn bond_extra_works() {
 				total: 1000000,
 				active: 1000000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 	});
@@ -1260,11 +1272,11 @@ fn bond_extra_and_withdraw_unbonded_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		assert_eq!(
-			Staking::eras_stakers(active_era(), 11),
+			Staking::eras_stakers(active_era(), &11),
 			Exposure { total: 1000, own: 1000, others: vec![] }
 		);
 
@@ -1278,12 +1290,12 @@ fn bond_extra_and_withdraw_unbonded_works() {
 				total: 1000 + 100,
 				active: 1000 + 100,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		// Exposure is a snapshot! only updated after the next era update.
 		assert_ne!(
-			Staking::eras_stakers(active_era(), 11),
+			Staking::eras_stakers(active_era(), &11),
 			Exposure { total: 1000 + 100, own: 1000 + 100, others: vec![] }
 		);
 
@@ -1299,12 +1311,12 @@ fn bond_extra_and_withdraw_unbonded_works() {
 				total: 1000 + 100,
 				active: 1000 + 100,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		// Exposure is now updated.
 		assert_eq!(
-			Staking::eras_stakers(active_era(), 11),
+			Staking::eras_stakers(active_era(), &11),
 			Exposure { total: 1000 + 100, own: 1000 + 100, others: vec![] }
 		);
 
@@ -1317,7 +1329,7 @@ fn bond_extra_and_withdraw_unbonded_works() {
 				total: 1000 + 100,
 				active: 100,
 				unlocking: bounded_vec![UnlockChunk { value: 1000, era: 2 + 3 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			}),
 		);
 
@@ -1330,7 +1342,7 @@ fn bond_extra_and_withdraw_unbonded_works() {
 				total: 1000 + 100,
 				active: 100,
 				unlocking: bounded_vec![UnlockChunk { value: 1000, era: 2 + 3 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			}),
 		);
 
@@ -1346,7 +1358,7 @@ fn bond_extra_and_withdraw_unbonded_works() {
 				total: 1000 + 100,
 				active: 100,
 				unlocking: bounded_vec![UnlockChunk { value: 1000, era: 2 + 3 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			}),
 		);
 
@@ -1362,7 +1374,7 @@ fn bond_extra_and_withdraw_unbonded_works() {
 				total: 100,
 				active: 100,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			}),
 		);
 	})
@@ -1465,7 +1477,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1484,7 +1496,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 100,
 				unlocking: bounded_vec![UnlockChunk { value: 900, era: 2 + 3 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1497,7 +1509,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1510,7 +1522,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 100,
 				unlocking: bounded_vec![UnlockChunk { value: 900, era: 5 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1523,7 +1535,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 600,
 				unlocking: bounded_vec![UnlockChunk { value: 400, era: 5 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1536,7 +1548,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1551,7 +1563,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 100,
 				unlocking: bounded_vec![UnlockChunk { value: 900, era: 5 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1564,7 +1576,7 @@ fn rebond_works() {
 				total: 1000,
 				active: 600,
 				unlocking: bounded_vec![UnlockChunk { value: 400, era: 5 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 	})
@@ -1591,7 +1603,7 @@ fn rebond_is_fifo() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1606,7 +1618,7 @@ fn rebond_is_fifo() {
 				total: 1000,
 				active: 600,
 				unlocking: bounded_vec![UnlockChunk { value: 400, era: 2 + 3 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1624,7 +1636,7 @@ fn rebond_is_fifo() {
 					UnlockChunk { value: 400, era: 2 + 3 },
 					UnlockChunk { value: 300, era: 3 + 3 },
 				],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1643,7 +1655,7 @@ fn rebond_is_fifo() {
 					UnlockChunk { value: 300, era: 3 + 3 },
 					UnlockChunk { value: 200, era: 4 + 3 },
 				],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1659,7 +1671,7 @@ fn rebond_is_fifo() {
 					UnlockChunk { value: 400, era: 2 + 3 },
 					UnlockChunk { value: 100, era: 3 + 3 },
 				],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 	})
@@ -1688,7 +1700,7 @@ fn rebond_emits_right_value_in_event() {
 				total: 1000,
 				active: 100,
 				unlocking: bounded_vec![UnlockChunk { value: 900, era: 1 + 3 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
@@ -1701,7 +1713,7 @@ fn rebond_emits_right_value_in_event() {
 				total: 1000,
 				active: 200,
 				unlocking: bounded_vec![UnlockChunk { value: 800, era: 1 + 3 }],
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		// Event emitted should be correct
@@ -1716,7 +1728,7 @@ fn rebond_emits_right_value_in_event() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		// Event emitted should be correct, only 800
@@ -1736,15 +1748,15 @@ fn reward_to_stake_works() {
 			// Confirm account 10 and 20 are validators
 			assert!(<Validators<Test>>::contains_key(&11) && <Validators<Test>>::contains_key(&21));
 
-			assert_eq!(Staking::eras_stakers(active_era(), 11).total, 1000);
-			assert_eq!(Staking::eras_stakers(active_era(), 21).total, 2000);
+			assert_eq!(Staking::eras_stakers(active_era(), &11).total, 1000);
+			assert_eq!(Staking::eras_stakers(active_era(), &21).total, 2000);
 
 			// Give the man some money.
 			let _ = Balances::make_free_balance_be(&10, 1000);
 			let _ = Balances::make_free_balance_be(&20, 1000);
 
 			// Bypass logic and change current exposure
-			ErasStakers::<Test>::insert(0, 21, Exposure { total: 69, own: 69, others: vec![] });
+			EraInfo::<Test>::set_exposure(0, &21, Exposure { total: 69, own: 69, others: vec![] });
 			<Ledger<Test>>::insert(
 				&20,
 				StakingLedger {
@@ -1752,7 +1764,7 @@ fn reward_to_stake_works() {
 					total: 69,
 					active: 69,
 					unlocking: Default::default(),
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				},
 			);
 
@@ -1765,21 +1777,18 @@ fn reward_to_stake_works() {
 			mock::start_active_era(1);
 			mock::make_all_reward_payment(0);
 
-			assert_eq!(Staking::eras_stakers(active_era(), 11).total, 1000);
-			assert_eq!(Staking::eras_stakers(active_era(), 21).total, 2000);
+			assert_eq!(Staking::eras_stakers(active_era(), &11).total, 1000);
+			assert_eq!(Staking::eras_stakers(active_era(), &21).total, 2000);
 
 			let _11_balance = Balances::free_balance(&11);
-			let _21_balance = Balances::free_balance(&21);
-
 			assert_eq!(_11_balance, 1000 + total_payout_0 / 2);
-			assert_eq!(_21_balance, 2000 + total_payout_0 / 2);
 
 			// Trigger another new era as the info are frozen before the era start.
 			mock::start_active_era(2);
 
 			// -- new infos
-			assert_eq!(Staking::eras_stakers(active_era(), 11).total, _11_balance);
-			assert_eq!(Staking::eras_stakers(active_era(), 21).total, _21_balance);
+			assert_eq!(Staking::eras_stakers(active_era(), &11).total, 1000 + total_payout_0 / 2);
+			assert_eq!(Staking::eras_stakers(active_era(), &21).total, 2000 + total_payout_0 / 2);
 		});
 }
 
@@ -1813,7 +1822,7 @@ fn reap_stash_works() {
 					total: 5,
 					active: 5,
 					unlocking: Default::default(),
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				},
 			);
 
@@ -1909,8 +1918,8 @@ fn wrong_vote_is_moot() {
 			assert_eq_uvec!(validator_controllers(), vec![21, 11]);
 
 			// our new voter is taken into account
-			assert!(Staking::eras_stakers(active_era(), 11).others.iter().any(|i| i.who == 61));
-			assert!(Staking::eras_stakers(active_era(), 21).others.iter().any(|i| i.who == 61));
+			assert!(Staking::eras_stakers(active_era(), &11).others.iter().any(|i| i.who == 61));
+			assert!(Staking::eras_stakers(active_era(), &21).others.iter().any(|i| i.who == 61));
 		});
 }
 
@@ -1943,7 +1952,7 @@ fn bond_with_no_staked_value() {
 					active: 0,
 					total: 5,
 					unlocking: bounded_vec![UnlockChunk { value: 5, era: 3 }],
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				})
 			);
 
@@ -1997,9 +2006,9 @@ fn bond_with_little_staked_value_bounded() {
 			mock::start_active_era(1);
 			mock::make_all_reward_payment(0);
 
-			// 2 is elected.
+			// 1 is elected.
 			assert_eq_uvec!(validator_controllers(), vec![21, 11, 1]);
-			assert_eq!(Staking::eras_stakers(active_era(), 2).total, 0);
+			assert_eq!(Staking::eras_stakers(active_era(), &2).total, 0);
 
 			// Old ones are rewarded.
 			assert_eq_error_rate!(
@@ -2017,7 +2026,7 @@ fn bond_with_little_staked_value_bounded() {
 			mock::make_all_reward_payment(1);
 
 			assert_eq_uvec!(validator_controllers(), vec![21, 11, 1]);
-			assert_eq!(Staking::eras_stakers(active_era(), 2).total, 0);
+			assert_eq!(Staking::eras_stakers(active_era(), &2).total, 0);
 
 			// 2 is now rewarded.
 			assert_eq_error_rate!(
@@ -2168,8 +2177,8 @@ fn phragmen_should_not_overflow() {
 		assert_eq_uvec!(validator_controllers(), vec![3, 5]);
 
 		// We can safely convert back to values within [u64, u128].
-		assert!(Staking::eras_stakers(active_era(), 3).total > Votes::max_value() as Balance);
-		assert!(Staking::eras_stakers(active_era(), 5).total > Votes::max_value() as Balance);
+		assert!(Staking::eras_stakers(active_era(), &3).total > Votes::max_value() as Balance);
+		assert!(Staking::eras_stakers(active_era(), &5).total > Votes::max_value() as Balance);
 	})
 }
 
@@ -2193,10 +2202,9 @@ fn reward_validator_slashing_validator_does_not_overflow() {
 
 		// Check reward
 		ErasRewardPoints::<Test>::insert(0, reward);
-		ErasStakers::<Test>::insert(0, 11, &exposure);
-		ErasStakersClipped::<Test>::insert(0, 11, exposure);
+		EraInfo::<Test>::set_exposure(0, &11, exposure);
 		ErasValidatorReward::<Test>::insert(0, stake);
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 0));
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 0, 0));
 		assert_eq!(Balances::total_balance(&11), stake * 2);
 
 		// Set staker
@@ -2206,9 +2214,9 @@ fn reward_validator_slashing_validator_does_not_overflow() {
 		// only slashes out of bonded stake are applied. without this line, it is 0.
 		Staking::bond(RuntimeOrigin::signed(2), stake - 1, RewardDestination::default()).unwrap();
 		// Override exposure of 11
-		ErasStakers::<Test>::insert(
+		EraInfo::<Test>::set_exposure(
 			0,
-			11,
+			&11,
 			Exposure {
 				total: stake,
 				own: 1,
@@ -2219,7 +2227,7 @@ fn reward_validator_slashing_validator_does_not_overflow() {
 		// Check slashing
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(100)],
@@ -2318,7 +2326,7 @@ fn offence_forces_new_era() {
 	ExtBuilder::default().build_and_execute(|| {
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(5)],
@@ -2336,7 +2344,7 @@ fn offence_ensures_new_era_without_clobbering() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(5)],
@@ -2354,7 +2362,7 @@ fn offence_deselects_validator_even_when_slash_is_zero() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(0)],
@@ -2375,7 +2383,7 @@ fn slashing_performed_according_exposure() {
 	// This test checks that slashing is performed according the exposure (or more precisely,
 	// historical exposure), not the current balance.
 	ExtBuilder::default().build_and_execute(|| {
-		assert_eq!(Staking::eras_stakers(active_era(), 11).own, 1000);
+		assert_eq!(Staking::eras_stakers(active_era(), &11).own, 1000);
 
 		// Handle an offence with a historical exposure.
 		on_offence_now(
@@ -2401,7 +2409,7 @@ fn slash_in_old_span_does_not_deselect() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(0)],
@@ -2424,7 +2432,7 @@ fn slash_in_old_span_does_not_deselect() {
 
 		on_offence_in_era(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(0)],
@@ -2440,7 +2448,7 @@ fn slash_in_old_span_does_not_deselect() {
 
 		on_offence_in_era(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			// NOTE: A 100% slash here would clean up the account, causing de-registration.
@@ -2467,11 +2475,11 @@ fn reporters_receive_their_slice() {
 		// The reporters' reward is calculated from the total exposure.
 		let initial_balance = 1125;
 
-		assert_eq!(Staking::eras_stakers(active_era(), 11).total, initial_balance);
+		assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance);
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![1, 2],
 			}],
 			&[Perbill::from_percent(50)],
@@ -2494,11 +2502,11 @@ fn subsequent_reports_in_same_span_pay_out_less() {
 		// The reporters' reward is calculated from the total exposure.
 		let initial_balance = 1125;
 
-		assert_eq!(Staking::eras_stakers(active_era(), 11).total, initial_balance);
+		assert_eq!(Staking::eras_stakers(active_era(), &11).total, initial_balance);
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![1],
 			}],
 			&[Perbill::from_percent(20)],
@@ -2511,7 +2519,7 @@ fn subsequent_reports_in_same_span_pay_out_less() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![1],
 			}],
 			&[Perbill::from_percent(50)],
@@ -2533,7 +2541,7 @@ fn invulnerables_are_not_slashed() {
 		assert_eq!(Balances::free_balance(11), 1000);
 		assert_eq!(Balances::free_balance(21), 2000);
 
-		let exposure = Staking::eras_stakers(active_era(), 21);
+		let exposure = Staking::eras_stakers(active_era(), &21);
 		let initial_balance = Staking::slashable_balance_of(&21);
 
 		let nominator_balances: Vec<_> =
@@ -2542,11 +2550,11 @@ fn invulnerables_are_not_slashed() {
 		on_offence_now(
 			&[
 				OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), 11)),
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
 					reporters: vec![],
 				},
 				OffenceDetails {
-					offender: (21, Staking::eras_stakers(active_era(), 21)),
+					offender: (21, Staking::eras_stakers(active_era(), &21)),
 					reporters: vec![],
 				},
 			],
@@ -2576,7 +2584,7 @@ fn dont_slash_if_fraction_is_zero() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(0)],
@@ -2597,7 +2605,7 @@ fn only_slash_for_max_in_era() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(50)],
@@ -2609,7 +2617,7 @@ fn only_slash_for_max_in_era() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(25)],
@@ -2620,7 +2628,7 @@ fn only_slash_for_max_in_era() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(60)],
@@ -2642,7 +2650,7 @@ fn garbage_collection_after_slashing() {
 
 			on_offence_now(
 				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), 11)),
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
 					reporters: vec![],
 				}],
 				&[Perbill::from_percent(10)],
@@ -2654,7 +2662,7 @@ fn garbage_collection_after_slashing() {
 
 			on_offence_now(
 				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), 11)),
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
 					reporters: vec![],
 				}],
 				&[Perbill::from_percent(100)],
@@ -2691,12 +2699,15 @@ fn garbage_collection_on_window_pruning() {
 		assert_eq!(Balances::free_balance(11), 1000);
 		let now = active_era();
 
-		let exposure = Staking::eras_stakers(now, 11);
+		let exposure = Staking::eras_stakers(now, &11);
 		assert_eq!(Balances::free_balance(101), 2000);
 		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
 
 		on_offence_now(
-			&[OffenceDetails { offender: (11, Staking::eras_stakers(now, 11)), reporters: vec![] }],
+			&[OffenceDetails {
+				offender: (11, Staking::eras_stakers(now, &11)),
+				reporters: vec![],
+			}],
 			&[Perbill::from_percent(10)],
 		);
 
@@ -2731,14 +2742,14 @@ fn slashing_nominators_by_span_max() {
 		assert_eq!(Balances::free_balance(101), 2000);
 		assert_eq!(Staking::slashable_balance_of(&21), 1000);
 
-		let exposure_11 = Staking::eras_stakers(active_era(), 11);
-		let exposure_21 = Staking::eras_stakers(active_era(), 21);
+		let exposure_11 = Staking::eras_stakers(active_era(), &11);
+		let exposure_21 = Staking::eras_stakers(active_era(), &21);
 		let nominated_value_11 = exposure_11.others.iter().find(|o| o.who == 101).unwrap().value;
 		let nominated_value_21 = exposure_21.others.iter().find(|o| o.who == 101).unwrap().value;
 
 		on_offence_in_era(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(10)],
@@ -2765,7 +2776,7 @@ fn slashing_nominators_by_span_max() {
 		// second slash: higher era, higher value, same span.
 		on_offence_in_era(
 			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), 21)),
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(30)],
@@ -2787,7 +2798,7 @@ fn slashing_nominators_by_span_max() {
 		// in-era value, but lower slash value than slash 2.
 		on_offence_in_era(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(20)],
@@ -2822,7 +2833,7 @@ fn slashes_are_summed_across_spans() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), 21)),
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(10)],
@@ -2845,7 +2856,7 @@ fn slashes_are_summed_across_spans() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), 21)),
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(10)],
@@ -2869,7 +2880,7 @@ fn deferred_slashes_are_deferred() {
 
 		assert_eq!(Balances::free_balance(11), 1000);
 
-		let exposure = Staking::eras_stakers(active_era(), 11);
+		let exposure = Staking::eras_stakers(active_era(), &11);
 		assert_eq!(Balances::free_balance(101), 2000);
 		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
 
@@ -2877,7 +2888,7 @@ fn deferred_slashes_are_deferred() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(10)],
@@ -2928,7 +2939,7 @@ fn retroactive_deferred_slashes_two_eras_before() {
 		assert_eq!(BondingDuration::get(), 3);
 
 		mock::start_active_era(1);
-		let exposure_11_at_era1 = Staking::eras_stakers(active_era(), 11);
+		let exposure_11_at_era1 = Staking::eras_stakers(active_era(), &11);
 
 		mock::start_active_era(3);
 
@@ -2964,7 +2975,7 @@ fn retroactive_deferred_slashes_one_before() {
 		assert_eq!(BondingDuration::get(), 3);
 
 		mock::start_active_era(1);
-		let exposure_11_at_era1 = Staking::eras_stakers(active_era(), 11);
+		let exposure_11_at_era1 = Staking::eras_stakers(active_era(), &11);
 
 		// unbond at slash era.
 		mock::start_active_era(2);
@@ -3012,12 +3023,12 @@ fn staker_cannot_bail_deferred_slash() {
 		assert_eq!(Balances::free_balance(11), 1000);
 		assert_eq!(Balances::free_balance(101), 2000);
 
-		let exposure = Staking::eras_stakers(active_era(), 11);
+		let exposure = Staking::eras_stakers(active_era(), &11);
 		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (11, Staking::eras_stakers(active_era(), 11)),
+				offender: (11, Staking::eras_stakers(active_era(), &11)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(10)],
@@ -3036,7 +3047,7 @@ fn staker_cannot_bail_deferred_slash() {
 				active: 0,
 				total: 500,
 				stash: 101,
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 				unlocking: bounded_vec![UnlockChunk { era: 4u32, value: 500 }],
 			}
 		);
@@ -3086,7 +3097,7 @@ fn remove_deferred() {
 
 		assert_eq!(Balances::free_balance(11), 1000);
 
-		let exposure = Staking::eras_stakers(active_era(), 11);
+		let exposure = Staking::eras_stakers(active_era(), &11);
 		assert_eq!(Balances::free_balance(101), 2000);
 		let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value;
 
@@ -3162,7 +3173,7 @@ fn remove_multi_deferred() {
 
 		assert_eq!(Balances::free_balance(11), 1000);
 
-		let exposure = Staking::eras_stakers(active_era(), 11);
+		let exposure = Staking::eras_stakers(active_era(), &11);
 		assert_eq!(Balances::free_balance(101), 2000);
 
 		on_offence_now(
@@ -3172,7 +3183,7 @@ fn remove_multi_deferred() {
 
 		on_offence_now(
 			&[OffenceDetails {
-				offender: (21, Staking::eras_stakers(active_era(), 21)),
+				offender: (21, Staking::eras_stakers(active_era(), &21)),
 				reporters: vec![],
 			}],
 			&[Perbill::from_percent(10)],
@@ -3537,8 +3548,9 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() {
 		mock::start_active_era(1);
 
 		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
-		// Change total issuance in order to modify total payout
+		// Increase total token issuance to affect the total payout.
 		let _ = Balances::deposit_creating(&999, 1_000_000_000);
+
 		// Compute total payout now for whole duration as other parameter won't change
 		let total_payout_1 = current_total_payout_for_duration(reward_time_per_era());
 		assert!(total_payout_1 != total_payout_0);
@@ -3546,7 +3558,7 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() {
 		mock::start_active_era(2);
 
 		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
-		// Change total issuance in order to modify total payout
+		// Increase total token issuance to affect the total payout.
 		let _ = Balances::deposit_creating(&999, 1_000_000_000);
 		// Compute total payout now for whole duration as other parameter won't change
 		let total_payout_2 = current_total_payout_for_duration(reward_time_per_era());
@@ -3563,19 +3575,19 @@ fn claim_reward_at_the_last_era_and_no_double_claim_and_invalid_claim() {
 		// Last kept is 1:
 		assert!(current_era - HistoryDepth::get() == 1);
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 0),
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 0, 0),
 			// Fail: Era out of history
 			Error::<Test>::InvalidEraToReward.with_weight(err_weight)
 		);
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 1));
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 2));
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0));
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0));
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 2),
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0),
 			// Fail: Double claim
 			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
 		);
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, active_era),
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, active_era, 0),
 			// Fail: Era not finished yet
 			Error::<Test>::InvalidEraToReward.with_weight(err_weight)
 		);
@@ -3601,7 +3613,7 @@ fn zero_slash_keeps_nominators() {
 
 		assert_eq!(Balances::free_balance(11), 1000);
 
-		let exposure = Staking::eras_stakers(active_era(), 11);
+		let exposure = Staking::eras_stakers(active_era(), &11);
 		assert_eq!(Balances::free_balance(101), 2000);
 
 		on_offence_now(
@@ -3680,9 +3692,10 @@ fn six_session_delay() {
 }
 
 #[test]
-fn test_max_nominator_rewarded_per_validator_and_cant_steal_someone_else_reward() {
+fn test_nominators_over_max_exposure_page_size_are_rewarded() {
 	ExtBuilder::default().build_and_execute(|| {
-		for i in 0..=<<Test as Config>::MaxNominatorRewardedPerValidator as Get<_>>::get() {
+		// bond one nominator more than the max exposure page size to validator 11.
+		for i in 0..=MaxExposurePageSize::get() {
 			let stash = 10_000 + i as AccountId;
 			let balance = 10_000 + i as Balance;
 			Balances::make_free_balance_be(&stash, balance);
@@ -3702,29 +3715,73 @@ fn test_max_nominator_rewarded_per_validator_and_cant_steal_someone_else_reward(
 		mock::start_active_era(2);
 		mock::make_all_reward_payment(1);
 
-		// Assert only nominators from 1 to Max are rewarded
-		for i in 0..=<<Test as Config>::MaxNominatorRewardedPerValidator as Get<_>>::get() {
+		// Assert nominators from 1 to Max are rewarded
+		let mut i: u32 = 0;
+		while i < MaxExposurePageSize::get() {
 			let stash = 10_000 + i as AccountId;
 			let balance = 10_000 + i as Balance;
-			if stash == 10_000 {
-				assert!(Balances::free_balance(&stash) == balance);
-			} else {
-				assert!(Balances::free_balance(&stash) > balance);
-			}
+			assert!(Balances::free_balance(&stash) > balance);
+			i += 1;
 		}
+
+		// Assert overflowing nominators from page 1 are also rewarded
+		let stash = 10_000 + i as AccountId;
+		assert!(Balances::free_balance(&stash) > (10_000 + i) as Balance);
 	});
 }
 
 #[test]
-fn test_payout_stakers() {
-	// Test that payout_stakers work in general, including that only the top
-	// `T::MaxNominatorRewardedPerValidator` nominators are rewarded.
+fn test_nominators_are_rewarded_for_all_exposure_page() {
+	ExtBuilder::default().build_and_execute(|| {
+		// 3 pages of exposure
+		let nominator_count = 2 * MaxExposurePageSize::get() + 1;
+
+		for i in 0..nominator_count {
+			let stash = 10_000 + i as AccountId;
+			let balance = 10_000 + i as Balance;
+			Balances::make_free_balance_be(&stash, balance);
+			assert_ok!(Staking::bond(
+				RuntimeOrigin::signed(stash),
+				balance,
+				RewardDestination::Stash
+			));
+			assert_ok!(Staking::nominate(RuntimeOrigin::signed(stash), vec![11]));
+		}
+		mock::start_active_era(1);
+
+		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
+		// compute and ensure the reward amount is greater than zero.
+		let _ = current_total_payout_for_duration(reward_time_per_era());
+
+		mock::start_active_era(2);
+		mock::make_all_reward_payment(1);
+
+		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 3);
+
+		// Assert all nominators are rewarded according to their stake
+		for i in 0..nominator_count {
+			// balance of the nominator after the reward payout.
+			let current_balance = Balances::free_balance(&((10000 + i) as AccountId));
+			// balance of the nominator in the previous iteration.
+			let previous_balance = Balances::free_balance(&((10000 + i - 1) as AccountId));
+			// balance before the reward.
+			let original_balance = 10_000 + i as Balance;
+
+			assert!(current_balance > original_balance);
+			// since the stake of the nominator is increasing for each iteration, the final balance
+			// after the reward should also be higher than the previous iteration.
+			assert!(current_balance > previous_balance);
+		}
+	});
+}
+
+#[test]
+fn test_multi_page_payout_stakers_by_page() {
+	// Test that payout_stakers work in general and that it pays the correct amount of reward.
 	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
 		let balance = 1000;
 		// Track the exposure of the validator and all nominators.
 		let mut total_exposure = balance;
-		// Track the exposure of the validator and the nominators that will get paid out.
-		let mut payout_exposure = balance;
 		// Create a validator:
 		bond_validator(11, balance); // Default(64)
 		assert_eq!(Validators::<Test>::count(), 1);
@@ -3733,44 +3790,66 @@ fn test_payout_stakers() {
 		for i in 0..100 {
 			let bond_amount = balance + i as Balance;
 			bond_nominator(1000 + i, bond_amount, vec![11]);
+			// with multi page reward payout, payout exposure is same as total exposure.
 			total_exposure += bond_amount;
-			if i >= 36 {
-				payout_exposure += bond_amount;
-			};
 		}
-		let payout_exposure_part = Perbill::from_rational(payout_exposure, total_exposure);
 
 		mock::start_active_era(1);
 		Staking::reward_by_ids(vec![(11, 1)]);
 
+		// Since `MaxExposurePageSize = 64`, there are two pages of validator exposure.
+		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 2);
+
 		// compute and ensure the reward amount is greater than zero.
 		let payout = current_total_payout_for_duration(reward_time_per_era());
-		let actual_paid_out = payout_exposure_part * payout;
-
 		mock::start_active_era(2);
 
+		// verify the exposures are calculated correctly.
+		let actual_exposure_0 = EraInfo::<Test>::get_paged_exposure(1, &11, 0).unwrap();
+		assert_eq!(actual_exposure_0.total(), total_exposure);
+		assert_eq!(actual_exposure_0.own(), 1000);
+		assert_eq!(actual_exposure_0.others().len(), 64);
+		let actual_exposure_1 = EraInfo::<Test>::get_paged_exposure(1, &11, 1).unwrap();
+		assert_eq!(actual_exposure_1.total(), total_exposure);
+		// own stake is only included once in the first page
+		assert_eq!(actual_exposure_1.own(), 0);
+		assert_eq!(actual_exposure_1.others().len(), 100 - 64);
+
 		let pre_payout_total_issuance = Balances::total_issuance();
 		RewardOnUnbalanceWasCalled::set(false);
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 1));
-		assert_eq_error_rate!(
-			Balances::total_issuance(),
-			pre_payout_total_issuance + actual_paid_out,
-			1
-		);
+
+		let controller_balance_before_p0_payout = Balances::free_balance(&11);
+		// Payout rewards for first exposure page
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0));
+
+		let controller_balance_after_p0_payout = Balances::free_balance(&11);
+
+		// verify rewards have been paid out but still some left
+		assert!(Balances::total_issuance() > pre_payout_total_issuance);
+		assert!(Balances::total_issuance() < pre_payout_total_issuance + payout);
+
+		// verify the validator has been rewarded
+		assert!(controller_balance_after_p0_payout > controller_balance_before_p0_payout);
+
+		// Payout the second and last page of nominators
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 1));
+
+		// verify the validator was not rewarded the second time
+		assert_eq!(Balances::free_balance(&11), controller_balance_after_p0_payout);
+
+		// verify all rewards have been paid out
+		assert_eq_error_rate!(Balances::total_issuance(), pre_payout_total_issuance + payout, 2);
 		assert!(RewardOnUnbalanceWasCalled::get());
 
-		// Top 64 nominators of validator 11 automatically paid out, including the validator
+		// verify all nominators of validator 11 are paid out, including the validator
 		// Validator payout goes to controller.
 		assert!(Balances::free_balance(&11) > balance);
-		for i in 36..100 {
+		for i in 0..100 {
 			assert!(Balances::free_balance(&(1000 + i)) > balance + i as Balance);
 		}
-		// The bottom 36 do not
-		for i in 0..36 {
-			assert_eq!(Balances::free_balance(&(1000 + i)), balance + i as Balance);
-		}
 
-		// We track rewards in `claimed_rewards` vec
+		// verify we no longer track rewards in `legacy_claimed_rewards` vec
+		let ledger = Staking::ledger(&11);
 		assert_eq!(
 			Staking::ledger(&11),
 			Some(StakingLedger {
@@ -3778,30 +3857,206 @@ fn test_payout_stakers() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![1]
+				legacy_claimed_rewards: bounded_vec![]
 			})
 		);
 
+		// verify rewards are tracked to prevent double claims
+		for page in 0..EraInfo::<Test>::get_page_count(1, &11) {
+			assert_eq!(
+				EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+					1,
+					ledger.as_ref().unwrap(),
+					&11,
+					page
+				),
+				true
+			);
+		}
+
 		for i in 3..16 {
 			Staking::reward_by_ids(vec![(11, 1)]);
 
 			// compute and ensure the reward amount is greater than zero.
 			let payout = current_total_payout_for_duration(reward_time_per_era());
-			let actual_paid_out = payout_exposure_part * payout;
 			let pre_payout_total_issuance = Balances::total_issuance();
 
 			mock::start_active_era(i);
 			RewardOnUnbalanceWasCalled::set(false);
-			assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, i - 1));
+			mock::make_all_reward_payment(i - 1);
 			assert_eq_error_rate!(
 				Balances::total_issuance(),
-				pre_payout_total_issuance + actual_paid_out,
-				1
+				pre_payout_total_issuance + payout,
+				2
 			);
 			assert!(RewardOnUnbalanceWasCalled::get());
+
+			// verify we track rewards for each era and page
+			for page in 0..EraInfo::<Test>::get_page_count(i - 1, &11) {
+				assert_eq!(
+					EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+						i - 1,
+						Staking::ledger(&11).as_ref().unwrap(),
+						&11,
+						page
+					),
+					true
+				);
+			}
+		}
+
+		assert_eq!(Staking::claimed_rewards(14, &11), vec![0, 1]);
+
+		let last_era = 99;
+		let history_depth = HistoryDepth::get();
+		let last_reward_era = last_era - 1;
+		let first_claimable_reward_era = last_era - history_depth;
+		for i in 16..=last_era {
+			Staking::reward_by_ids(vec![(11, 1)]);
+			// compute and ensure the reward amount is greater than zero.
+			let _ = current_total_payout_for_duration(reward_time_per_era());
+			mock::start_active_era(i);
+		}
+
+		// verify we clean up history as we go
+		for era in 0..15 {
+			assert_eq!(Staking::claimed_rewards(era, &11), Vec::<sp_staking::PageIndex>::new());
+		}
+
+		// verify only page 0 is marked as claimed
+		assert_ok!(Staking::payout_stakers_by_page(
+			RuntimeOrigin::signed(1337),
+			11,
+			first_claimable_reward_era,
+			0
+		));
+		assert_eq!(Staking::claimed_rewards(first_claimable_reward_era, &11), vec![0]);
+
+		// verify page 0 and 1 are marked as claimed
+		assert_ok!(Staking::payout_stakers_by_page(
+			RuntimeOrigin::signed(1337),
+			11,
+			first_claimable_reward_era,
+			1
+		));
+		assert_eq!(Staking::claimed_rewards(first_claimable_reward_era, &11), vec![0, 1]);
+
+		// verify only page 0 is marked as claimed
+		assert_ok!(Staking::payout_stakers_by_page(
+			RuntimeOrigin::signed(1337),
+			11,
+			last_reward_era,
+			0
+		));
+		assert_eq!(Staking::claimed_rewards(last_reward_era, &11), vec![0]);
+
+		// verify page 0 and 1 are marked as claimed
+		assert_ok!(Staking::payout_stakers_by_page(
+			RuntimeOrigin::signed(1337),
+			11,
+			last_reward_era,
+			1
+		));
+		assert_eq!(Staking::claimed_rewards(last_reward_era, &11), vec![0, 1]);
+
+		// Out of order claims works.
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 69, 0));
+		assert_eq!(Staking::claimed_rewards(69, &11), vec![0]);
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 23, 1));
+		assert_eq!(Staking::claimed_rewards(23, &11), vec![1]);
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 42, 0));
+		assert_eq!(Staking::claimed_rewards(42, &11), vec![0]);
+	});
+}
+
+#[test]
+fn test_multi_page_payout_stakers_backward_compatible() {
+	// Test that payout_stakers work in general and that it pays the correct amount of reward.
+	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
+		let balance = 1000;
+		// Track the exposure of the validator and all nominators.
+		let mut total_exposure = balance;
+		// Create a validator:
+		bond_validator(11, balance); // Default(64)
+		assert_eq!(Validators::<Test>::count(), 1);
+
+		let err_weight = <Test as Config>::WeightInfo::payout_stakers_alive_staked(0);
+
+		// Create nominators, targeting stash of validators
+		for i in 0..100 {
+			let bond_amount = balance + i as Balance;
+			bond_nominator(1000 + i, bond_amount, vec![11]);
+			// with multi page reward payout, payout exposure is same as total exposure.
+			total_exposure += bond_amount;
 		}
 
-		// We track rewards in `claimed_rewards` vec
+		mock::start_active_era(1);
+		Staking::reward_by_ids(vec![(11, 1)]);
+
+		// Since `MaxExposurePageSize = 64`, there are two pages of validator exposure.
+		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 2);
+
+		// compute and ensure the reward amount is greater than zero.
+		let payout = current_total_payout_for_duration(reward_time_per_era());
+		mock::start_active_era(2);
+
+		// verify the exposures are calculated correctly.
+		let actual_exposure_0 = EraInfo::<Test>::get_paged_exposure(1, &11, 0).unwrap();
+		assert_eq!(actual_exposure_0.total(), total_exposure);
+		assert_eq!(actual_exposure_0.own(), 1000);
+		assert_eq!(actual_exposure_0.others().len(), 64);
+		let actual_exposure_1 = EraInfo::<Test>::get_paged_exposure(1, &11, 1).unwrap();
+		assert_eq!(actual_exposure_1.total(), total_exposure);
+		// own stake is only included once in the first page
+		assert_eq!(actual_exposure_1.own(), 0);
+		assert_eq!(actual_exposure_1.others().len(), 100 - 64);
+
+		let pre_payout_total_issuance = Balances::total_issuance();
+		RewardOnUnbalanceWasCalled::set(false);
+
+		let controller_balance_before_p0_payout = Balances::free_balance(&11);
+		// Payout rewards for first exposure page
+		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 1));
+		// page 0 is claimed
+		assert_noop!(
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0),
+			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
+		);
+
+		let controller_balance_after_p0_payout = Balances::free_balance(&11);
+
+		// verify rewards have been paid out but still some left
+		assert!(Balances::total_issuance() > pre_payout_total_issuance);
+		assert!(Balances::total_issuance() < pre_payout_total_issuance + payout);
+
+		// verify the validator has been rewarded
+		assert!(controller_balance_after_p0_payout > controller_balance_before_p0_payout);
+
+		// This should payout the second and last page of nominators
+		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 1));
+
+		// cannot claim any more pages
+		assert_noop!(
+			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 1),
+			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
+		);
+
+		// verify the validator was not rewarded the second time
+		assert_eq!(Balances::free_balance(&11), controller_balance_after_p0_payout);
+
+		// verify all rewards have been paid out
+		assert_eq_error_rate!(Balances::total_issuance(), pre_payout_total_issuance + payout, 2);
+		assert!(RewardOnUnbalanceWasCalled::get());
+
+		// verify all nominators of validator 11 are paid out, including the validator
+		// Validator payout goes to controller.
+		assert!(Balances::free_balance(&11) > balance);
+		for i in 0..100 {
+			assert!(Balances::free_balance(&(1000 + i)) > balance + i as Balance);
+		}
+
+		// verify we no longer track rewards in `legacy_claimed_rewards` vec
+		let ledger = Staking::ledger(&11);
 		assert_eq!(
 			Staking::ledger(&11),
 			Some(StakingLedger {
@@ -3809,14 +4064,60 @@ fn test_payout_stakers() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: (1..=14).collect::<Vec<_>>().try_into().unwrap()
+				legacy_claimed_rewards: bounded_vec![]
 			})
 		);
 
+		// verify rewards are tracked to prevent double claims
+		for page in 0..EraInfo::<Test>::get_page_count(1, &11) {
+			assert_eq!(
+				EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+					1,
+					ledger.as_ref().unwrap(),
+					&11,
+					page
+				),
+				true
+			);
+		}
+
+		for i in 3..16 {
+			Staking::reward_by_ids(vec![(11, 1)]);
+
+			// compute and ensure the reward amount is greater than zero.
+			let payout = current_total_payout_for_duration(reward_time_per_era());
+			let pre_payout_total_issuance = Balances::total_issuance();
+
+			mock::start_active_era(i);
+			RewardOnUnbalanceWasCalled::set(false);
+			mock::make_all_reward_payment(i - 1);
+			assert_eq_error_rate!(
+				Balances::total_issuance(),
+				pre_payout_total_issuance + payout,
+				2
+			);
+			assert!(RewardOnUnbalanceWasCalled::get());
+
+			// verify we track rewards for each era and page
+			for page in 0..EraInfo::<Test>::get_page_count(i - 1, &11) {
+				assert_eq!(
+					EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+						i - 1,
+						Staking::ledger(&11).as_ref().unwrap(),
+						&11,
+						page
+					),
+					true
+				);
+			}
+		}
+
+		assert_eq!(Staking::claimed_rewards(14, &11), vec![0, 1]);
+
 		let last_era = 99;
 		let history_depth = HistoryDepth::get();
-		let expected_last_reward_era = last_era - 1;
-		let expected_start_reward_era = last_era - history_depth;
+		let last_reward_era = last_era - 1;
+		let first_claimable_reward_era = last_era - history_depth;
 		for i in 16..=last_era {
 			Staking::reward_by_ids(vec![(11, 1)]);
 			// compute and ensure the reward amount is greater than zero.
@@ -3824,48 +4125,125 @@ fn test_payout_stakers() {
 			mock::start_active_era(i);
 		}
 
-		// We clean it up as history passes
+		// verify we clean up history as we go
+		for era in 0..15 {
+			assert_eq!(Staking::claimed_rewards(era, &11), Vec::<sp_staking::PageIndex>::new());
+		}
+
+		// verify only page 0 is marked as claimed
 		assert_ok!(Staking::payout_stakers(
 			RuntimeOrigin::signed(1337),
 			11,
-			expected_start_reward_era
+			first_claimable_reward_era
 		));
+		assert_eq!(Staking::claimed_rewards(first_claimable_reward_era, &11), vec![0]);
+
+		// verify page 0 and 1 are marked as claimed
 		assert_ok!(Staking::payout_stakers(
 			RuntimeOrigin::signed(1337),
 			11,
-			expected_last_reward_era
+			first_claimable_reward_era,
 		));
-		assert_eq!(
-			Staking::ledger(&11),
-			Some(StakingLedger {
-				stash: 11,
-				total: 1000,
-				active: 1000,
-				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![expected_start_reward_era, expected_last_reward_era]
-			})
+		assert_eq!(Staking::claimed_rewards(first_claimable_reward_era, &11), vec![0, 1]);
+
+		// change order and verify only page 1 is marked as claimed
+		assert_ok!(Staking::payout_stakers_by_page(
+			RuntimeOrigin::signed(1337),
+			11,
+			last_reward_era,
+			1
+		));
+		assert_eq!(Staking::claimed_rewards(last_reward_era, &11), vec![1]);
+
+		// verify page 0 is claimed even when explicit page is not passed
+		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, last_reward_era,));
+
+		assert_eq!(Staking::claimed_rewards(last_reward_era, &11), vec![1, 0]);
+
+		// cannot claim any more pages
+		assert_noop!(
+			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, last_reward_era),
+			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
 		);
 
+		// Create 4 nominator pages
+		for i in 100..200 {
+			let bond_amount = balance + i as Balance;
+			bond_nominator(1000 + i, bond_amount, vec![11]);
+		}
+
+		let test_era = last_era + 1;
+		mock::start_active_era(test_era);
+
+		Staking::reward_by_ids(vec![(11, 1)]);
+		// compute and ensure the reward amount is greater than zero.
+		let _ = current_total_payout_for_duration(reward_time_per_era());
+		mock::start_active_era(test_era + 1);
+
 		// Out of order claims works.
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 69));
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 23));
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 42));
-		assert_eq!(
-			Staking::ledger(&11),
-			Some(StakingLedger {
-				stash: 11,
-				total: 1000,
-				active: 1000,
-				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![
-					expected_start_reward_era,
-					23,
-					42,
-					69,
-					expected_last_reward_era
-				]
-			})
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, test_era, 2));
+		assert_eq!(Staking::claimed_rewards(test_era, &11), vec![2]);
+
+		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, test_era));
+		assert_eq!(Staking::claimed_rewards(test_era, &11), vec![2, 0]);
+
+		// cannot claim page 2 again
+		assert_noop!(
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, test_era, 2),
+			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
 		);
+
+		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, test_era));
+		assert_eq!(Staking::claimed_rewards(test_era, &11), vec![2, 0, 1]);
+
+		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, test_era));
+		assert_eq!(Staking::claimed_rewards(test_era, &11), vec![2, 0, 1, 3]);
+	});
+}
+
+#[test]
+fn test_page_count_and_size() {
+	// Test that payout_stakers work in general and that it pays the correct amount of reward.
+	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
+		let balance = 1000;
+		// Track the exposure of the validator and all nominators.
+		// Create a validator:
+		bond_validator(11, balance); // Default(64)
+		assert_eq!(Validators::<Test>::count(), 1);
+
+		// Create nominators, targeting stash of validators
+		for i in 0..100 {
+			let bond_amount = balance + i as Balance;
+			bond_nominator(1000 + i, bond_amount, vec![11]);
+		}
+
+		mock::start_active_era(1);
+
+		// Since max exposure page size is 64, 2 pages of nominators are created.
+		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 2);
+
+		// first page has 64 nominators
+		assert_eq!(EraInfo::<Test>::get_paged_exposure(1, &11, 0).unwrap().others().len(), 64);
+		// second page has 36 nominators
+		assert_eq!(EraInfo::<Test>::get_paged_exposure(1, &11, 1).unwrap().others().len(), 36);
+
+		// now lets decrease page size
+		MaxExposurePageSize::set(32);
+		mock::start_active_era(2);
+		// now we expect 4 pages.
+		assert_eq!(EraInfo::<Test>::get_page_count(2, &11), 4);
+		// first 3 pages have 32 nominators each
+		assert_eq!(EraInfo::<Test>::get_paged_exposure(2, &11, 0).unwrap().others().len(), 32);
+		assert_eq!(EraInfo::<Test>::get_paged_exposure(2, &11, 1).unwrap().others().len(), 32);
+		assert_eq!(EraInfo::<Test>::get_paged_exposure(2, &11, 2).unwrap().others().len(), 32);
+		assert_eq!(EraInfo::<Test>::get_paged_exposure(2, &11, 3).unwrap().others().len(), 4);
+
+		// now lets decrease page size even more
+		MaxExposurePageSize::set(5);
+		mock::start_active_era(3);
+
+		// now we expect the max 20 pages (100/5).
+		assert_eq!(EraInfo::<Test>::get_page_count(3, &11), 20);
 	});
 }
 
@@ -3895,12 +4273,12 @@ fn payout_stakers_handles_basic_errors() {
 
 		// Wrong Era, too big
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 2),
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0),
 			Error::<Test>::InvalidEraToReward.with_weight(err_weight)
 		);
 		// Wrong Staker
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 10, 1),
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 10, 1, 0),
 			Error::<Test>::NotStash.with_weight(err_weight)
 		);
 
@@ -3920,33 +4298,137 @@ fn payout_stakers_handles_basic_errors() {
 		// to payout era starting from expected_start_reward_era=19 through
 		// expected_last_reward_era=98 (80 total eras), but not 18 or 99.
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, expected_start_reward_era - 1),
+			Staking::payout_stakers_by_page(
+				RuntimeOrigin::signed(1337),
+				11,
+				expected_start_reward_era - 1,
+				0
+			),
 			Error::<Test>::InvalidEraToReward.with_weight(err_weight)
 		);
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, expected_last_reward_era + 1),
+			Staking::payout_stakers_by_page(
+				RuntimeOrigin::signed(1337),
+				11,
+				expected_last_reward_era + 1,
+				0
+			),
 			Error::<Test>::InvalidEraToReward.with_weight(err_weight)
 		);
-		assert_ok!(Staking::payout_stakers(
+		assert_ok!(Staking::payout_stakers_by_page(
 			RuntimeOrigin::signed(1337),
 			11,
-			expected_start_reward_era
+			expected_start_reward_era,
+			0
 		));
-		assert_ok!(Staking::payout_stakers(
+		assert_ok!(Staking::payout_stakers_by_page(
+			RuntimeOrigin::signed(1337),
+			11,
+			expected_last_reward_era,
+			0
+		));
+
+		// can call page 1
+		assert_ok!(Staking::payout_stakers_by_page(
 			RuntimeOrigin::signed(1337),
 			11,
-			expected_last_reward_era
+			expected_last_reward_era,
+			1
 		));
 
 		// Can't claim again
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, expected_start_reward_era),
+			Staking::payout_stakers_by_page(
+				RuntimeOrigin::signed(1337),
+				11,
+				expected_start_reward_era,
+				0
+			),
+			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
+		);
+
+		assert_noop!(
+			Staking::payout_stakers_by_page(
+				RuntimeOrigin::signed(1337),
+				11,
+				expected_last_reward_era,
+				0
+			),
 			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
 		);
+
 		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, expected_last_reward_era),
+			Staking::payout_stakers_by_page(
+				RuntimeOrigin::signed(1337),
+				11,
+				expected_last_reward_era,
+				1
+			),
 			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
 		);
+
+		// invalid page
+		assert_noop!(
+			Staking::payout_stakers_by_page(
+				RuntimeOrigin::signed(1337),
+				11,
+				expected_last_reward_era,
+				2
+			),
+			Error::<Test>::InvalidPage.with_weight(err_weight)
+		);
+	});
+}
+
+#[test]
+fn test_commission_paid_across_pages() {
+	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
+		let balance = 1;
+		let commission = 50;
+		// Create a validator:
+		bond_validator(11, balance);
+		assert_ok!(Staking::validate(
+			RuntimeOrigin::signed(11),
+			ValidatorPrefs { commission: Perbill::from_percent(commission), blocked: false }
+		));
+		assert_eq!(Validators::<Test>::count(), 1);
+
+		// Create nominators, targeting stash of validators
+		for i in 0..200 {
+			let bond_amount = balance + i as Balance;
+			bond_nominator(1000 + i, bond_amount, vec![11]);
+		}
+
+		mock::start_active_era(1);
+		Staking::reward_by_ids(vec![(11, 1)]);
+
+		// Since `MaxExposurePageSize = 64`, there are four pages of validator
+		// exposure.
+		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 4);
+
+		// compute and ensure the reward amount is greater than zero.
+		let payout = current_total_payout_for_duration(reward_time_per_era());
+		mock::start_active_era(2);
+
+		let initial_balance = Balances::free_balance(&11);
+		// Payout rewards for first exposure page
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0));
+
+		let controller_balance_after_p0_payout = Balances::free_balance(&11);
+
+		// some commission is paid
+		assert!(initial_balance < controller_balance_after_p0_payout);
+
+		// payout all pages
+		for i in 1..4 {
+			let before_balance = Balances::free_balance(&11);
+			assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, i));
+			let after_balance = Balances::free_balance(&11);
+			// some commission is paid for every page
+			assert!(before_balance < after_balance);
+		}
+
+		assert_eq_error_rate!(Balances::free_balance(&11), initial_balance + payout / 2, 1,);
 	});
 }
 
@@ -3955,8 +4437,7 @@ fn payout_stakers_handles_weight_refund() {
 	// Note: this test relies on the assumption that `payout_stakers_alive_staked` is solely used by
 	// `payout_stakers` to calculate the weight of each payout op.
 	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
-		let max_nom_rewarded =
-			<<Test as Config>::MaxNominatorRewardedPerValidator as Get<_>>::get();
+		let max_nom_rewarded = MaxExposurePageSize::get();
 		// Make sure the configured value is meaningful for our use.
 		assert!(max_nom_rewarded >= 4);
 		let half_max_nom_rewarded = max_nom_rewarded / 2;
@@ -3992,7 +4473,11 @@ fn payout_stakers_handles_weight_refund() {
 		start_active_era(2);
 
 		// Collect payouts when there are no nominators
-		let call = TestCall::Staking(StakingCall::payout_stakers { validator_stash: 11, era: 1 });
+		let call = TestCall::Staking(StakingCall::payout_stakers_by_page {
+			validator_stash: 11,
+			era: 1,
+			page: 0,
+		});
 		let info = call.get_dispatch_info();
 		let result = call.dispatch(RuntimeOrigin::signed(20));
 		assert_ok!(result);
@@ -4005,7 +4490,11 @@ fn payout_stakers_handles_weight_refund() {
 		start_active_era(3);
 
 		// Collect payouts for an era where the validator did not receive any points.
-		let call = TestCall::Staking(StakingCall::payout_stakers { validator_stash: 11, era: 2 });
+		let call = TestCall::Staking(StakingCall::payout_stakers_by_page {
+			validator_stash: 11,
+			era: 2,
+			page: 0,
+		});
 		let info = call.get_dispatch_info();
 		let result = call.dispatch(RuntimeOrigin::signed(20));
 		assert_ok!(result);
@@ -4018,7 +4507,11 @@ fn payout_stakers_handles_weight_refund() {
 		start_active_era(4);
 
 		// Collect payouts when the validator has `half_max_nom_rewarded` nominators.
-		let call = TestCall::Staking(StakingCall::payout_stakers { validator_stash: 11, era: 3 });
+		let call = TestCall::Staking(StakingCall::payout_stakers_by_page {
+			validator_stash: 11,
+			era: 3,
+			page: 0,
+		});
 		let info = call.get_dispatch_info();
 		let result = call.dispatch(RuntimeOrigin::signed(20));
 		assert_ok!(result);
@@ -4041,14 +4534,22 @@ fn payout_stakers_handles_weight_refund() {
 		start_active_era(6);
 
 		// Collect payouts when the validator had `half_max_nom_rewarded` nominators.
-		let call = TestCall::Staking(StakingCall::payout_stakers { validator_stash: 11, era: 5 });
+		let call = TestCall::Staking(StakingCall::payout_stakers_by_page {
+			validator_stash: 11,
+			era: 5,
+			page: 0,
+		});
 		let info = call.get_dispatch_info();
 		let result = call.dispatch(RuntimeOrigin::signed(20));
 		assert_ok!(result);
 		assert_eq!(extract_actual_weight(&result, &info), max_nom_rewarded_weight);
 
 		// Try and collect payouts for an era that has already been collected.
-		let call = TestCall::Staking(StakingCall::payout_stakers { validator_stash: 11, era: 5 });
+		let call = TestCall::Staking(StakingCall::payout_stakers_by_page {
+			validator_stash: 11,
+			era: 5,
+			page: 0,
+		});
 		let info = call.get_dispatch_info();
 		let result = call.dispatch(RuntimeOrigin::signed(20));
 		assert!(result.is_err());
@@ -4058,7 +4559,7 @@ fn payout_stakers_handles_weight_refund() {
 }
 
 #[test]
-fn bond_during_era_correctly_populates_claimed_rewards() {
+fn bond_during_era_does_not_populate_legacy_claimed_rewards() {
 	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
 		// Era = None
 		bond_validator(9, 1000);
@@ -4069,7 +4570,7 @@ fn bond_during_era_correctly_populates_claimed_rewards() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: bounded_vec![],
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 		mock::start_active_era(5);
@@ -4081,13 +4582,12 @@ fn bond_during_era_correctly_populates_claimed_rewards() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: (0..5).collect::<Vec<_>>().try_into().unwrap(),
+				legacy_claimed_rewards: bounded_vec![],
 			})
 		);
 
 		// make sure only era upto history depth is stored
 		let current_era = 99;
-		let last_reward_era = 99 - HistoryDepth::get();
 		mock::start_active_era(current_era);
 		bond_validator(13, 1000);
 		assert_eq!(
@@ -4097,10 +4597,7 @@ fn bond_during_era_correctly_populates_claimed_rewards() {
 				total: 1000,
 				active: 1000,
 				unlocking: Default::default(),
-				claimed_rewards: (last_reward_era..current_era)
-					.collect::<Vec<_>>()
-					.try_into()
-					.unwrap(),
+				legacy_claimed_rewards: Default::default(),
 			})
 		);
 	});
@@ -4129,7 +4626,7 @@ fn offences_weight_calculated_correctly() {
 			>,
 		> = (1..10)
 			.map(|i| OffenceDetails {
-				offender: (i, Staking::eras_stakers(active_era(), i)),
+				offender: (i, Staking::eras_stakers(active_era(), &i)),
 				reporters: vec![],
 			})
 			.collect();
@@ -4145,7 +4642,7 @@ fn offences_weight_calculated_correctly() {
 
 		// On Offence with one offenders, Applied
 		let one_offender = [OffenceDetails {
-			offender: (11, Staking::eras_stakers(active_era(), 11)),
+			offender: (11, Staking::eras_stakers(active_era(), &11)),
 			reporters: vec![1],
 		}];
 
@@ -4201,7 +4698,7 @@ fn payout_creates_controller() {
 		// compute and ensure the reward amount is greater than zero.
 		let _ = current_total_payout_for_duration(reward_time_per_era());
 		mock::start_active_era(2);
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(controller), 11, 1));
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0));
 
 		// Controller is created
 		assert!(Balances::free_balance(controller) > 0);
@@ -4229,7 +4726,7 @@ fn payout_to_any_account_works() {
 		// compute and ensure the reward amount is greater than zero.
 		let _ = current_total_payout_for_duration(reward_time_per_era());
 		mock::start_active_era(2);
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(1337), 11, 1));
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0));
 
 		// Payment is successful
 		assert!(Balances::free_balance(42) > 0);
@@ -4352,7 +4849,7 @@ fn cannot_rebond_to_lower_than_ed() {
 					total: 11 * 1000,
 					active: 11 * 1000,
 					unlocking: Default::default(),
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				}
 			);
 
@@ -4366,7 +4863,7 @@ fn cannot_rebond_to_lower_than_ed() {
 					total: 11 * 1000,
 					active: 0,
 					unlocking: bounded_vec![UnlockChunk { value: 11 * 1000, era: 3 }],
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				}
 			);
 
@@ -4392,7 +4889,7 @@ fn cannot_bond_extra_to_lower_than_ed() {
 					total: 11 * 1000,
 					active: 11 * 1000,
 					unlocking: Default::default(),
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				}
 			);
 
@@ -4406,7 +4903,7 @@ fn cannot_bond_extra_to_lower_than_ed() {
 					total: 11 * 1000,
 					active: 0,
 					unlocking: bounded_vec![UnlockChunk { value: 11 * 1000, era: 3 }],
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				}
 			);
 
@@ -4433,7 +4930,7 @@ fn do_not_die_when_active_is_ed() {
 					total: 1000 * ed,
 					active: 1000 * ed,
 					unlocking: Default::default(),
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				}
 			);
 
@@ -4450,7 +4947,7 @@ fn do_not_die_when_active_is_ed() {
 					total: ed,
 					active: ed,
 					unlocking: Default::default(),
-					claimed_rewards: bounded_vec![],
+					legacy_claimed_rewards: bounded_vec![],
 				}
 			);
 		})
@@ -5527,7 +6024,7 @@ fn proportional_slash_stop_slashing_if_remaining_zero() {
 		active: 20,
 		// we have some chunks, but they are not affected.
 		unlocking: bounded_vec![c(1, 10), c(2, 10)],
-		claimed_rewards: bounded_vec![],
+		legacy_claimed_rewards: bounded_vec![],
 	};
 
 	assert_eq!(BondingDuration::get(), 3);
@@ -5545,7 +6042,7 @@ fn proportional_ledger_slash_works() {
 		total: 10,
 		active: 10,
 		unlocking: bounded_vec![],
-		claimed_rewards: bounded_vec![],
+		legacy_claimed_rewards: bounded_vec![],
 	};
 	assert_eq!(BondingDuration::get(), 3);
 
@@ -5762,149 +6259,6 @@ fn proportional_ledger_slash_works() {
 	);
 }
 
-#[test]
-fn pre_bonding_era_cannot_be_claimed() {
-	// Verifies initial conditions of mock
-	ExtBuilder::default().nominate(false).build_and_execute(|| {
-		let history_depth = HistoryDepth::get();
-		// jump to some era above history_depth
-		let mut current_era = history_depth + 10;
-		let last_reward_era = current_era - 1;
-		let start_reward_era = current_era - history_depth;
-
-		// put some money in stash=3 and controller=4.
-		for i in 3..5 {
-			let _ = Balances::make_free_balance_be(&i, 2000);
-		}
-
-		mock::start_active_era(current_era);
-
-		// add a new candidate for being a validator. account 3 controlled by 4.
-		assert_ok!(Staking::bond(RuntimeOrigin::signed(3), 1500, RewardDestination::Controller));
-
-		let claimed_rewards: BoundedVec<_, _> =
-			(start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap();
-		assert_eq!(
-			Staking::ledger(&3).unwrap(),
-			StakingLedger {
-				stash: 3,
-				total: 1500,
-				active: 1500,
-				unlocking: Default::default(),
-				claimed_rewards,
-			}
-		);
-
-		// start next era
-		current_era = current_era + 1;
-		mock::start_active_era(current_era);
-
-		// claiming reward for last era in which validator was active works
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(3), 3, current_era - 1));
-
-		// consumed weight for all payout_stakers dispatches that fail
-		let err_weight = <Test as Config>::WeightInfo::payout_stakers_alive_staked(0);
-		// cannot claim rewards for an era before bonding occured as it is
-		// already marked as claimed.
-		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(3), 3, current_era - 2),
-			Error::<Test>::AlreadyClaimed.with_weight(err_weight)
-		);
-
-		// decoding will fail now since Staking Ledger is in corrupt state
-		HistoryDepth::set(history_depth - 1);
-		assert_eq!(Staking::ledger(&4), None);
-
-		// make sure stakers still cannot claim rewards that they are not meant to
-		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(3), 3, current_era - 2),
-			Error::<Test>::NotController
-		);
-
-		// fix the corrupted state for post conditions check
-		HistoryDepth::set(history_depth);
-	});
-}
-
-#[test]
-fn reducing_history_depth_abrupt() {
-	// Verifies initial conditions of mock
-	ExtBuilder::default().nominate(false).build_and_execute(|| {
-		let original_history_depth = HistoryDepth::get();
-		let mut current_era = original_history_depth + 10;
-		let last_reward_era = current_era - 1;
-		let start_reward_era = current_era - original_history_depth;
-
-		// put some money in (stash, controller)=(3,3),(5,5).
-		for i in 3..7 {
-			let _ = Balances::make_free_balance_be(&i, 2000);
-		}
-
-		// start current era
-		mock::start_active_era(current_era);
-
-		// add a new candidate for being a staker. account 3 controlled by 3.
-		assert_ok!(Staking::bond(RuntimeOrigin::signed(3), 1500, RewardDestination::Controller));
-
-		// all previous era before the bonding action should be marked as
-		// claimed.
-		let claimed_rewards: BoundedVec<_, _> =
-			(start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap();
-		assert_eq!(
-			Staking::ledger(&3).unwrap(),
-			StakingLedger {
-				stash: 3,
-				total: 1500,
-				active: 1500,
-				unlocking: Default::default(),
-				claimed_rewards,
-			}
-		);
-
-		// next era
-		current_era = current_era + 1;
-		mock::start_active_era(current_era);
-
-		// claiming reward for last era in which validator was active works
-		assert_ok!(Staking::payout_stakers(RuntimeOrigin::signed(3), 3, current_era - 1));
-
-		// next era
-		current_era = current_era + 1;
-		mock::start_active_era(current_era);
-
-		// history_depth reduced without migration
-		let history_depth = original_history_depth - 1;
-		HistoryDepth::set(history_depth);
-		// claiming reward does not work anymore
-		assert_noop!(
-			Staking::payout_stakers(RuntimeOrigin::signed(3), 3, current_era - 1),
-			Error::<Test>::NotController
-		);
-
-		// new stakers can still bond
-		assert_ok!(Staking::bond(RuntimeOrigin::signed(5), 1200, RewardDestination::Controller));
-
-		// new staking ledgers created will be bounded by the current history depth
-		let last_reward_era = current_era - 1;
-		let start_reward_era = current_era - history_depth;
-		let claimed_rewards: BoundedVec<_, _> =
-			(start_reward_era..=last_reward_era).collect::<Vec<_>>().try_into().unwrap();
-		assert_eq!(
-			Staking::ledger(&5).unwrap(),
-			StakingLedger {
-				stash: 5,
-				total: 1200,
-				active: 1200,
-				unlocking: Default::default(),
-				claimed_rewards,
-			}
-		);
-
-		// fix the corrupted state for post conditions check
-		HistoryDepth::set(original_history_depth);
-	});
-}
-
 #[test]
 fn reducing_max_unlocking_chunks_abrupt() {
 	// Concern is on validators only
@@ -6051,6 +6405,282 @@ fn set_min_commission_works_with_admin_origin() {
 	})
 }
 
+#[test]
+fn can_page_exposure() {
+	let mut others: Vec<IndividualExposure<AccountId, Balance>> = vec![];
+	let mut total_stake: Balance = 0;
+	// 19 nominators
+	for i in 1..20 {
+		let individual_stake: Balance = 100 * i as Balance;
+		others.push(IndividualExposure { who: i, value: individual_stake });
+		total_stake += individual_stake;
+	}
+	let own_stake: Balance = 500;
+	total_stake += own_stake;
+	assert_eq!(total_stake, 19_500);
+	// build full exposure set
+	let exposure: Exposure<AccountId, Balance> =
+		Exposure { total: total_stake, own: own_stake, others };
+
+	// when
+	let (exposure_metadata, exposure_page): (
+		PagedExposureMetadata<Balance>,
+		Vec<ExposurePage<AccountId, Balance>>,
+	) = exposure.clone().into_pages(3);
+
+	// then
+	// 7 pages of nominators.
+	assert_eq!(exposure_page.len(), 7);
+	assert_eq!(exposure_metadata.page_count, 7);
+	// first page stake = 100 + 200 + 300
+	assert!(matches!(exposure_page[0], ExposurePage { page_total: 600, .. }));
+	// second page stake = 0 + 400 + 500 + 600
+	assert!(matches!(exposure_page[1], ExposurePage { page_total: 1500, .. }));
+	// verify overview has the total
+	assert_eq!(exposure_metadata.total, 19_500);
+	// verify total stake is same as in the original exposure.
+	assert_eq!(
+		exposure_page.iter().map(|a| a.page_total).reduce(|a, b| a + b).unwrap(),
+		19_500 - exposure_metadata.own
+	);
+	// verify own stake is correct
+	assert_eq!(exposure_metadata.own, 500);
+	// verify number of nominators are same as in the original exposure.
+	assert_eq!(exposure_page.iter().map(|a| a.others.len()).reduce(|a, b| a + b).unwrap(), 19);
+	assert_eq!(exposure_metadata.nominator_count, 19);
+}
+
+#[test]
+fn should_retain_era_info_only_upto_history_depth() {
+	ExtBuilder::default().build_and_execute(|| {
+		// remove existing exposure
+		Pallet::<Test>::clear_era_information(0);
+		let validator_stash = 10;
+
+		for era in 0..4 {
+			ClaimedRewards::<Test>::insert(era, &validator_stash, vec![0, 1, 2]);
+			for page in 0..3 {
+				ErasStakersPaged::<Test>::insert(
+					(era, &validator_stash, page),
+					ExposurePage { page_total: 100, others: vec![] },
+				);
+			}
+		}
+
+		for i in 0..4 {
+			// Count of entries remaining in ClaimedRewards = total - cleared_count
+			assert_eq!(ClaimedRewards::<Test>::iter().count(), (4 - i));
+			// 1 claimed_rewards entry for each era
+			assert_eq!(ClaimedRewards::<Test>::iter_prefix(i as EraIndex).count(), 1);
+			// 3 entries (pages) for each era
+			assert_eq!(ErasStakersPaged::<Test>::iter_prefix((i as EraIndex,)).count(), 3);
+
+			// when clear era info
+			Pallet::<Test>::clear_era_information(i as EraIndex);
+
+			// then all era entries are cleared
+			assert_eq!(ClaimedRewards::<Test>::iter_prefix(i as EraIndex).count(), 0);
+			assert_eq!(ErasStakersPaged::<Test>::iter_prefix((i as EraIndex,)).count(), 0);
+		}
+	});
+}
+
+#[test]
+fn test_legacy_claimed_rewards_is_checked_at_reward_payout() {
+	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
+		// Create a validator:
+		bond_validator(11, 1000);
+
+		// reward validator for next 2 eras
+		mock::start_active_era(1);
+		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
+		mock::start_active_era(2);
+		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
+		mock::start_active_era(3);
+
+		//verify rewards are not claimed
+		assert_eq!(
+			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+				1,
+				Staking::ledger(11).as_ref().unwrap(),
+				&11,
+				0
+			),
+			false
+		);
+		assert_eq!(
+			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+				2,
+				Staking::ledger(11).as_ref().unwrap(),
+				&11,
+				0
+			),
+			false
+		);
+
+		// assume reward claim for era 1 was stored in legacy storage
+		Ledger::<Test>::insert(
+			11,
+			StakingLedger {
+				stash: 11,
+				total: 1000,
+				active: 1000,
+				unlocking: Default::default(),
+				legacy_claimed_rewards: bounded_vec![1],
+			},
+		);
+
+		// verify rewards for era 1 cannot be claimed
+		assert_noop!(
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 1, 0),
+			Error::<Test>::AlreadyClaimed
+				.with_weight(<Test as Config>::WeightInfo::payout_stakers_alive_staked(0)),
+		);
+		assert_eq!(
+			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+				1,
+				Staking::ledger(11).as_ref().unwrap(),
+				&11,
+				0
+			),
+			true
+		);
+
+		// verify rewards for era 2 can be claimed
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 2, 0));
+		assert_eq!(
+			EraInfo::<Test>::is_rewards_claimed_with_legacy_fallback(
+				2,
+				Staking::ledger(11).as_ref().unwrap(),
+				&11,
+				0
+			),
+			true
+		);
+		// but the new claimed rewards for era 2 is not stored in legacy storage
+		assert_eq!(
+			Ledger::<Test>::get(11).unwrap(),
+			StakingLedger {
+				stash: 11,
+				total: 1000,
+				active: 1000,
+				unlocking: Default::default(),
+				legacy_claimed_rewards: bounded_vec![1],
+			},
+		);
+		// instead it is kept in `ClaimedRewards`
+		assert_eq!(ClaimedRewards::<Test>::get(2, 11), vec![0]);
+	});
+}
+
+#[test]
+fn test_validator_exposure_is_backward_compatible_with_non_paged_rewards_payout() {
+	ExtBuilder::default().has_stakers(false).build_and_execute(|| {
+		// case 1: exposure exist in clipped.
+		// set page cap to 10
+		MaxExposurePageSize::set(10);
+		bond_validator(11, 1000);
+		let mut expected_individual_exposures: Vec<IndividualExposure<AccountId, Balance>> = vec![];
+		let mut total_exposure: Balance = 0;
+		// 1st exposure page
+		for i in 0..10 {
+			let who = 1000 + i;
+			let value = 1000 + i as Balance;
+			bond_nominator(who, value, vec![11]);
+			expected_individual_exposures.push(IndividualExposure { who, value });
+			total_exposure += value;
+		}
+
+		for i in 10..15 {
+			let who = 1000 + i;
+			let value = 1000 + i as Balance;
+			bond_nominator(who, value, vec![11]);
+			expected_individual_exposures.push(IndividualExposure { who, value });
+			total_exposure += value;
+		}
+
+		mock::start_active_era(1);
+		// reward validator for current era
+		Pallet::<Test>::reward_by_ids(vec![(11, 1)]);
+
+		// start new era
+		mock::start_active_era(2);
+		// verify exposure for era 1 is stored in paged storage, that each exposure is stored in
+		// one and only one page, and no exposure is repeated.
+		let actual_exposure_page_0 = ErasStakersPaged::<Test>::get((1, 11, 0)).unwrap();
+		let actual_exposure_page_1 = ErasStakersPaged::<Test>::get((1, 11, 1)).unwrap();
+		expected_individual_exposures.iter().for_each(|exposure| {
+			assert!(
+				actual_exposure_page_0.others.contains(exposure) ||
+					actual_exposure_page_1.others.contains(exposure)
+			);
+		});
+		assert_eq!(
+			expected_individual_exposures.len(),
+			actual_exposure_page_0.others.len() + actual_exposure_page_1.others.len()
+		);
+		// verify `EraInfo` returns page from paged storage
+		assert_eq!(
+			EraInfo::<Test>::get_paged_exposure(1, &11, 0).unwrap().others(),
+			&actual_exposure_page_0.others
+		);
+		assert_eq!(
+			EraInfo::<Test>::get_paged_exposure(1, &11, 1).unwrap().others(),
+			&actual_exposure_page_1.others
+		);
+		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 2);
+
+		// case 2: exposure exist in ErasStakers and ErasStakersClipped (legacy).
+		// delete paged storage and add exposure to clipped storage
+		<ErasStakersPaged<Test>>::remove((1, 11, 0));
+		<ErasStakersPaged<Test>>::remove((1, 11, 1));
+		<ErasStakersOverview<Test>>::remove(1, 11);
+
+		<ErasStakers<Test>>::insert(
+			1,
+			11,
+			Exposure {
+				total: total_exposure,
+				own: 1000,
+				others: expected_individual_exposures.clone(),
+			},
+		);
+		let mut clipped_exposure = expected_individual_exposures.clone();
+		clipped_exposure.sort_by(|a, b| b.who.cmp(&a.who));
+		clipped_exposure.truncate(10);
+		<ErasStakersClipped<Test>>::insert(
+			1,
+			11,
+			Exposure { total: total_exposure, own: 1000, others: clipped_exposure.clone() },
+		);
+
+		// verify `EraInfo` returns exposure from clipped storage
+		let actual_exposure_paged = EraInfo::<Test>::get_paged_exposure(1, &11, 0).unwrap();
+		assert_eq!(actual_exposure_paged.others(), &clipped_exposure);
+		assert_eq!(actual_exposure_paged.own(), 1000);
+		assert_eq!(actual_exposure_paged.exposure_metadata.page_count, 1);
+
+		let actual_exposure_full = EraInfo::<Test>::get_full_exposure(1, &11);
+		assert_eq!(actual_exposure_full.others, expected_individual_exposures);
+		assert_eq!(actual_exposure_full.own, 1000);
+		assert_eq!(actual_exposure_full.total, total_exposure);
+
+		// for pages other than 0, clipped storage returns empty exposure
+		assert_eq!(EraInfo::<Test>::get_paged_exposure(1, &11, 1), None);
+		// page size is 1 for clipped storage
+		assert_eq!(EraInfo::<Test>::get_page_count(1, &11), 1);
+
+		// payout for page 0 works
+		assert_ok!(Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 0, 0));
+		// payout for page 1 fails
+		assert_noop!(
+			Staking::payout_stakers_by_page(RuntimeOrigin::signed(1337), 11, 0, 1),
+			Error::<Test>::InvalidPage
+				.with_weight(<Test as Config>::WeightInfo::payout_stakers_alive_staked(0))
+		);
+	});
+}
+
 mod staking_interface {
 	use frame_support::storage::with_storage_layer;
 	use sp_staking::StakingInterface;
@@ -6080,7 +6710,7 @@ mod staking_interface {
 		ExtBuilder::default().build_and_execute(|| {
 			on_offence_now(
 				&[OffenceDetails {
-					offender: (11, Staking::eras_stakers(active_era(), 11)),
+					offender: (11, Staking::eras_stakers(active_era(), &11)),
 					reporters: vec![],
 				}],
 				&[Perbill::from_percent(100)],
diff --git a/primitives/staking/src/lib.rs b/primitives/staking/src/lib.rs
index 1621af164b375..4ec37a29cea58 100644
--- a/primitives/staking/src/lib.rs
+++ b/primitives/staking/src/lib.rs
@@ -21,11 +21,13 @@
 //! approaches in general. Definitions related to sessions, slashing, etc go here.
 
 use crate::currency_to_vote::CurrencyToVote;
-use codec::{FullCodec, MaxEncodedLen};
+use codec::{Decode, Encode, FullCodec, HasCompact, MaxEncodedLen};
 use scale_info::TypeInfo;
-use sp_core::RuntimeDebug;
-use sp_runtime::{DispatchError, DispatchResult, Saturating};
-use sp_std::{collections::btree_map::BTreeMap, ops::Sub, vec::Vec};
+use sp_runtime::{
+	traits::{AtLeast32BitUnsigned, Zero},
+	DispatchError, DispatchResult, RuntimeDebug, Saturating,
+};
+use sp_std::{collections::btree_map::BTreeMap, ops::Sub, vec, vec::Vec};
 
 pub mod offence;
 
@@ -37,6 +39,9 @@ pub type SessionIndex = u32;
 /// Counter for the number of eras that have passed.
 pub type EraIndex = u32;
 
+/// Counter for paged storage items.
+pub type PageIndex = u32;
+
 /// Representation of the status of a staker.
 #[derive(RuntimeDebug, TypeInfo)]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone))]
@@ -261,6 +266,9 @@ pub trait StakingInterface {
 		}
 	}
 
+	#[cfg(feature = "runtime-benchmarks")]
+	fn max_exposure_page_size() -> PageIndex;
+
 	#[cfg(feature = "runtime-benchmarks")]
 	fn add_era_stakers(
 		current_era: &EraIndex,
@@ -272,4 +280,122 @@ pub trait StakingInterface {
 	fn set_current_era(era: EraIndex);
 }
 
+/// The amount of exposure for an era that an individual nominator has (susceptible to slashing).
+#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
+pub struct IndividualExposure<AccountId, Balance: HasCompact> {
+	/// The stash account of the nominator in question.
+	pub who: AccountId,
+	/// Amount of funds exposed.
+	#[codec(compact)]
+	pub value: Balance,
+}
+
+/// A snapshot of the stake backing a single validator in the system.
+#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
+pub struct Exposure<AccountId, Balance: HasCompact> {
+	/// The total balance backing this validator.
+	#[codec(compact)]
+	pub total: Balance,
+	/// The validator's own stash that is exposed.
+	#[codec(compact)]
+	pub own: Balance,
+	/// The portions of nominators stashes that are exposed.
+	pub others: Vec<IndividualExposure<AccountId, Balance>>,
+}
+
+impl<AccountId, Balance: Default + HasCompact> Default for Exposure<AccountId, Balance> {
+	fn default() -> Self {
+		Self { total: Default::default(), own: Default::default(), others: vec![] }
+	}
+}
+
+impl<
+		AccountId: Clone,
+		Balance: HasCompact + AtLeast32BitUnsigned + Copy + codec::MaxEncodedLen,
+	> Exposure<AccountId, Balance>
+{
+	/// Splits an `Exposure` into `PagedExposureMetadata` and multiple chunks of
+	/// `IndividualExposure` with each chunk having maximum of `page_size` elements.
+	pub fn into_pages(
+		self,
+		page_size: PageIndex,
+	) -> (PagedExposureMetadata<Balance>, Vec<ExposurePage<AccountId, Balance>>) {
+		let individual_chunks = self.others.chunks(page_size as usize);
+		let mut exposure_pages: Vec<ExposurePage<AccountId, Balance>> =
+			Vec::with_capacity(individual_chunks.len());
+
+		for chunk in individual_chunks {
+			let mut page_total: Balance = Zero::zero();
+			let mut others: Vec<IndividualExposure<AccountId, Balance>> =
+				Vec::with_capacity(chunk.len());
+			for individual in chunk.iter() {
+				page_total.saturating_accrue(individual.value);
+				others.push(IndividualExposure {
+					who: individual.who.clone(),
+					value: individual.value,
+				})
+			}
+
+			exposure_pages.push(ExposurePage { page_total, others });
+		}
+
+		(
+			PagedExposureMetadata {
+				total: self.total,
+				own: self.own,
+				nominator_count: self.others.len() as u32,
+				page_count: exposure_pages.len() as PageIndex,
+			},
+			exposure_pages,
+		)
+	}
+}
+
+/// A snapshot of the stake backing a single validator in the system.
+#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
+pub struct ExposurePage<AccountId, Balance: HasCompact> {
+	/// The total balance of this chunk/page.
+	#[codec(compact)]
+	pub page_total: Balance,
+	/// The portions of nominators stashes that are exposed.
+	pub others: Vec<IndividualExposure<AccountId, Balance>>,
+}
+
+impl<A, B: Default + HasCompact> Default for ExposurePage<A, B> {
+	fn default() -> Self {
+		ExposurePage { page_total: Default::default(), others: vec![] }
+	}
+}
+
+/// Metadata for Paged Exposure of a validator such as total stake across pages and page count.
+///
+/// In combination with the associated `ExposurePage`s, it can be used to reconstruct a full
+/// `Exposure` set of a validator. This is useful for cases where we want to query full set of
+/// `Exposure` as one page (for backward compatibility).
+#[derive(
+	PartialEq,
+	Eq,
+	PartialOrd,
+	Ord,
+	Clone,
+	Encode,
+	Decode,
+	RuntimeDebug,
+	TypeInfo,
+	Default,
+	MaxEncodedLen,
+)]
+pub struct PagedExposureMetadata<Balance: HasCompact + codec::MaxEncodedLen> {
+	/// The total balance backing this validator.
+	#[codec(compact)]
+	pub total: Balance,
+	/// The validator's own stash that is exposed.
+	#[codec(compact)]
+	pub own: Balance,
+	/// Number of nominators backing this validator.
+	pub nominator_count: u32,
+	/// Number of pages of nominators.
+	pub page_count: PageIndex,
+}
+
 sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $);