mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 17:28:23 +00:00
Add rewards cutover migration and checks
This commit is contained in:
parent
03bd305e37
commit
89e2db7ab7
3 changed files with 246 additions and 10 deletions
|
|
@ -53,7 +53,7 @@ fn push_unsent_entry<T: Config>(window_start: u32, window_index: u32, duration:
|
|||
}
|
||||
|
||||
#[allow(clippy::multiple_bound_locations)]
|
||||
#[benchmarks(where T: pallet_balances::Config)]
|
||||
#[benchmarks(where T: pallet_balances::Config + pallet_timestamp::Config<Moment = u64>)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
|
|
@ -72,6 +72,14 @@ mod benchmarks {
|
|||
|
||||
T::BenchmarkHelper::setup();
|
||||
<RewardPointsForEra<T>>::insert(1u32, era_reward_points);
|
||||
pallet_timestamp::Now::<T>::put(35_000u64);
|
||||
#[cfg(test)]
|
||||
crate::mock::Mock::mutate(|mock| {
|
||||
mock.active_era = Some(pallet_external_validators::traits::ActiveEraInfo {
|
||||
index: 1,
|
||||
start: Some(30_000),
|
||||
});
|
||||
});
|
||||
|
||||
#[block]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ pub mod pallet {
|
|||
};
|
||||
|
||||
/// The current storage version.
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
|
||||
|
||||
pub type RewardPoints = u32;
|
||||
pub type EraIndex = u32;
|
||||
|
|
@ -194,6 +194,86 @@ pub mod pallet {
|
|||
fn on_initialize(_n: frame_system::pallet_prelude::BlockNumberFor<T>) -> Weight {
|
||||
Self::process_unsent_reward_eras()
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let on_chain = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
if on_chain >= STORAGE_VERSION {
|
||||
return T::DbWeight::get().reads(1);
|
||||
}
|
||||
|
||||
let cutover_era = T::EraIndexProvider::active_era().index.saturating_add(1);
|
||||
|
||||
// This upgrade intentionally drops any pre-window retry state and
|
||||
// defers window accounting until the next full era so the current
|
||||
// in-flight era is not partially accounted under mixed semantics.
|
||||
WindowModeStartsAtEra::<T>::put(cutover_era);
|
||||
NextWindowToSubmit::<T>::kill();
|
||||
for slot in 0..UNSENT_QUEUE_CAPACITY {
|
||||
UnsentRewardWindow::<T>::remove(slot);
|
||||
}
|
||||
UnsentRewardHead::<T>::put(0);
|
||||
UnsentRewardTail::<T>::put(0);
|
||||
STORAGE_VERSION.put::<Pallet<T>>();
|
||||
|
||||
log::info!(
|
||||
target: "ext_validators_rewards",
|
||||
"Migrated rewards pallet to storage version 2. Window mode will start at era {}.",
|
||||
cutover_era,
|
||||
);
|
||||
|
||||
T::DbWeight::get().reads_writes(2, 69)
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
|
||||
let on_chain = Pallet::<T>::on_chain_storage_version();
|
||||
let active_era = T::EraIndexProvider::active_era().index;
|
||||
let needs_upgrade = on_chain < STORAGE_VERSION;
|
||||
|
||||
Ok((needs_upgrade, active_era).encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
|
||||
let (needs_upgrade, active_era): (bool, EraIndex) = Decode::decode(&mut &state[..])
|
||||
.map_err(|_| "Failed to decode pre-upgrade state")?;
|
||||
|
||||
if !needs_upgrade {
|
||||
frame_support::ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() >= STORAGE_VERSION,
|
||||
"Storage version regressed on a no-op upgrade",
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
frame_support::ensure!(
|
||||
Pallet::<T>::on_chain_storage_version() == STORAGE_VERSION,
|
||||
"Rewards pallet storage version was not upgraded",
|
||||
);
|
||||
frame_support::ensure!(
|
||||
WindowModeStartsAtEra::<T>::get() == active_era.saturating_add(1),
|
||||
"Window cutover era was not initialized correctly",
|
||||
);
|
||||
frame_support::ensure!(
|
||||
NextWindowToSubmit::<T>::get() == 0,
|
||||
"NextWindowToSubmit was not reset",
|
||||
);
|
||||
frame_support::ensure!(
|
||||
UnsentRewardHead::<T>::get() == 0,
|
||||
"UnsentRewardHead was not reset",
|
||||
);
|
||||
frame_support::ensure!(
|
||||
UnsentRewardTail::<T>::get() == 0,
|
||||
"UnsentRewardTail was not reset",
|
||||
);
|
||||
frame_support::ensure!(
|
||||
UnsentRewardWindow::<T>::iter().next().is_none(),
|
||||
"UnsentRewardWindow entries were not cleared",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
|
|
@ -350,6 +430,11 @@ pub mod pallet {
|
|||
#[pallet::storage]
|
||||
pub type NextWindowToSubmit<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
/// Era at which window mode becomes active after a live upgrade.
|
||||
/// `0` means window mode is active immediately (fresh chains/tests).
|
||||
#[pallet::storage]
|
||||
pub type WindowModeStartsAtEra<T: Config> = StorageValue<_, EraIndex, ValueQuery>;
|
||||
|
||||
/// Maximum number of unsent reward entries in the ring buffer.
|
||||
pub const UNSENT_QUEUE_CAPACITY: u32 = 64;
|
||||
|
||||
|
|
@ -380,6 +465,11 @@ pub mod pallet {
|
|||
T::UnixTime::now().as_secs().saturated_into::<u32>()
|
||||
}
|
||||
|
||||
fn window_mode_is_active(era_index: EraIndex) -> bool {
|
||||
let start_era = WindowModeStartsAtEra::<T>::get();
|
||||
start_era == 0 || era_index >= start_era
|
||||
}
|
||||
|
||||
fn rewards_window_config() -> (u32, u32) {
|
||||
(
|
||||
T::RewardsWindowGenesisTimestamp::get(),
|
||||
|
|
@ -575,6 +665,7 @@ pub mod pallet {
|
|||
let now = Self::now_seconds();
|
||||
let (genesis, interval) = Self::rewards_window_config();
|
||||
let window_start = Self::window_start_for(now, genesis, interval);
|
||||
let window_mode_active = Self::window_mode_is_active(active_era.index);
|
||||
|
||||
RewardPointsForEra::<T>::mutate(active_era.index, |era_rewards| {
|
||||
for (validator, points) in points.into_iter() {
|
||||
|
|
@ -582,13 +673,15 @@ pub mod pallet {
|
|||
.saturating_accrue(points);
|
||||
era_rewards.total.saturating_accrue(points);
|
||||
|
||||
let operator = Self::account_to_h160(&validator);
|
||||
WindowOperatorPoints::<T>::mutate(window_start, |operators| {
|
||||
operators
|
||||
.entry(operator)
|
||||
.and_modify(|existing| *existing = existing.saturating_add(points))
|
||||
.or_insert(points);
|
||||
});
|
||||
if window_mode_active {
|
||||
let operator = Self::account_to_h160(&validator);
|
||||
WindowOperatorPoints::<T>::mutate(window_start, |operators| {
|
||||
operators
|
||||
.entry(operator)
|
||||
.and_modify(|existing| *existing = existing.saturating_add(points))
|
||||
.or_insert(points);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1120,6 +1213,15 @@ pub mod pallet {
|
|||
|
||||
impl<T: Config> OnEraEnd for Pallet<T> {
|
||||
fn on_era_end(era_index: EraIndex) {
|
||||
if !Self::window_mode_is_active(era_index) {
|
||||
log::info!(
|
||||
target: "ext_validators_rewards",
|
||||
"Skipping transition-era rewards for era {} until window mode cutover",
|
||||
era_index,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate performance-scaled inflation based on blocks produced.
|
||||
let base_inflation = T::EraInflationProvider::get();
|
||||
let scaled_inflation = Self::calculate_scaled_inflation(era_index, base_inflation);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
use {
|
||||
crate::{self as pallet_external_validators_rewards, mock::*},
|
||||
frame_support::{assert_noop, assert_ok, traits::fungible::Mutate},
|
||||
frame_support::{
|
||||
assert_noop, assert_ok,
|
||||
traits::{fungible::Mutate, GetStorageVersion, Hooks, StorageVersion},
|
||||
},
|
||||
pallet_external_validators::traits::{ActiveEraInfo, OnEraEnd, OnEraStart},
|
||||
sp_core::H160,
|
||||
sp_std::collections::btree_map::BTreeMap,
|
||||
|
|
@ -66,6 +69,129 @@ fn can_reward_validators() {
|
|||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_upgrade_schedules_window_mode_for_next_era_and_resets_retry_state() {
|
||||
new_test_ext().execute_with(|| {
|
||||
Mock::mutate(|mock| {
|
||||
mock.active_era = Some(ActiveEraInfo {
|
||||
index: 7,
|
||||
start: Some(70_000),
|
||||
})
|
||||
});
|
||||
|
||||
StorageVersion::new(1).put::<crate::Pallet<Test>>();
|
||||
crate::WindowModeStartsAtEra::<Test>::put(0);
|
||||
crate::NextWindowToSubmit::<Test>::put(123);
|
||||
crate::UnsentRewardWindow::<Test>::insert(
|
||||
5,
|
||||
crate::QueuedRewardsWindow {
|
||||
window_start: 100,
|
||||
window_index: 10,
|
||||
duration: 10,
|
||||
},
|
||||
);
|
||||
crate::UnsentRewardHead::<Test>::put(5);
|
||||
crate::UnsentRewardTail::<Test>::put(9);
|
||||
|
||||
let _ = <crate::Pallet<Test> as Hooks<u64>>::on_runtime_upgrade();
|
||||
|
||||
assert_eq!(crate::WindowModeStartsAtEra::<Test>::get(), 8);
|
||||
assert_eq!(crate::NextWindowToSubmit::<Test>::get(), 0);
|
||||
assert_eq!(crate::UnsentRewardWindow::<Test>::iter().count(), 0);
|
||||
assert_eq!(crate::UnsentRewardHead::<Test>::get(), 0);
|
||||
assert_eq!(crate::UnsentRewardTail::<Test>::get(), 0);
|
||||
assert_eq!(
|
||||
<crate::Pallet<Test>>::on_chain_storage_version(),
|
||||
StorageVersion::new(2)
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_era_does_not_write_window_points_before_cutover() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(5); // now = 35s
|
||||
|
||||
RewardsWindowGenesisTimestamp::set(20);
|
||||
RewardsWindowDuration::set(10);
|
||||
crate::WindowModeStartsAtEra::<Test>::put(2);
|
||||
|
||||
Mock::mutate(|mock| {
|
||||
mock.active_era = Some(ActiveEraInfo {
|
||||
index: 1,
|
||||
start: Some(20_000),
|
||||
})
|
||||
});
|
||||
|
||||
ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 10)]);
|
||||
|
||||
assert_eq!(
|
||||
crate::RewardPointsForEra::<Test>::get(1).total,
|
||||
10,
|
||||
"era accounting should still accumulate during the discarded transition era"
|
||||
);
|
||||
assert!(
|
||||
crate::WindowOperatorPoints::<Test>::get(30).is_empty(),
|
||||
"window accounting must remain off until the cutover era"
|
||||
);
|
||||
|
||||
Mock::mutate(|mock| {
|
||||
mock.active_era = Some(ActiveEraInfo {
|
||||
index: 2,
|
||||
start: Some(40_000),
|
||||
})
|
||||
});
|
||||
|
||||
ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 10)]);
|
||||
|
||||
assert_eq!(
|
||||
crate::WindowOperatorPoints::<Test>::get(30).get(&H160::from_low_u64_be(1)),
|
||||
Some(&10),
|
||||
"window accounting should start once the cutover era is reached"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_era_on_era_end_is_skipped_before_cutover() {
|
||||
new_test_ext().execute_with(|| {
|
||||
run_to_block(5); // now = 35s
|
||||
|
||||
RewardsWindowGenesisTimestamp::set(20);
|
||||
RewardsWindowDuration::set(10);
|
||||
crate::WindowModeStartsAtEra::<Test>::put(2);
|
||||
|
||||
Mock::mutate(|mock| {
|
||||
mock.active_era = Some(ActiveEraInfo {
|
||||
index: 1,
|
||||
start: Some(20_000),
|
||||
})
|
||||
});
|
||||
|
||||
ExternalValidatorsRewards::reward_by_ids([(H160::from_low_u64_be(1), 10)]);
|
||||
for _ in 0..600 {
|
||||
ExternalValidatorsRewards::note_block_author(H160::from_low_u64_be(1));
|
||||
}
|
||||
|
||||
System::reset_events();
|
||||
ExternalValidatorsRewards::on_era_end(1);
|
||||
|
||||
assert!(
|
||||
!System::events().iter().any(|record| matches!(
|
||||
&record.event,
|
||||
RuntimeEvent::ExternalValidatorsRewards(
|
||||
crate::Event::RewardsWindowSubmitted { .. }
|
||||
| crate::Event::RewardsWindowSubmissionFailed { .. }
|
||||
| crate::Event::RewardsWindowSkipped { .. }
|
||||
)
|
||||
)),
|
||||
"transition era should not emit window-processing events before cutover"
|
||||
);
|
||||
assert_eq!(crate::WindowInflationAmount::<Test>::iter().count(), 0);
|
||||
assert_eq!(crate::NextWindowToSubmit::<Test>::get(), 0);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_mode_attributes_points_to_aligned_window() {
|
||||
new_test_ext().execute_with(|| {
|
||||
|
|
|
|||
Loading…
Reference in a new issue