diff --git a/Cargo.lock b/Cargo.lock index 82dcef6699..1a19b73ece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5633,7 +5633,7 @@ dependencies = [ [[package]] name = "hydradx" -version = "15.0.2" +version = "15.1.0" dependencies = [ "async-trait", "clap", @@ -5758,7 +5758,7 @@ dependencies = [ "pallet-referrals", "pallet-route-executor", "pallet-stableswap", - "pallet-staking 5.1.0", + "pallet-staking 5.2.0", "pallet-timestamp", "pallet-transaction-multi-payment", "pallet-uniques", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "426.0.0" +version = "427.0.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", @@ -5889,7 +5889,7 @@ dependencies = [ "pallet-session", "pallet-signet", "pallet-stableswap", - "pallet-staking 5.1.0", + "pallet-staking 5.2.0", "pallet-state-trie-migration", "pallet-timestamp", "pallet-tips", @@ -9143,7 +9143,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.18.1" +version = "1.19.0" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", @@ -10667,7 +10667,7 @@ dependencies = [ [[package]] name = "pallet-staking" -version = "5.1.0" +version = "5.2.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -12929,7 +12929,7 @@ dependencies = [ [[package]] name = "primitives" -version = "6.4.0" +version = "6.4.1" dependencies = [ "frame-support", "hex-literal", @@ -13975,7 +13975,7 @@ dependencies = [ "pallet-scheduler", "pallet-session", "pallet-stableswap", - "pallet-staking 5.1.0", + "pallet-staking 5.2.0", "pallet-timestamp", "pallet-transaction-multi-payment", "pallet-transaction-pause", @@ -14041,7 +14041,7 @@ dependencies = [ "pallet-asset-registry", "pallet-omnipool", "pallet-stableswap", - "pallet-staking 5.1.0", + "pallet-staking 5.2.0", "primitives", "scraper", "serde", diff --git a/Makefile b/Makefile index ec0240c3fe..3edced4428 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ format: .PHONY: try-runtime try-runtime: $(cargo) build --release --features try-runtime - try-runtime --runtime ./target/release/wbuild/hydradx-runtime/hydradx_runtime.wasm on-runtime-upgrade --blocktime 6000 --checks all live --uri wss://archive.rpc.hydration.cloud + try-runtime --runtime ./target/release/wbuild/hydradx-runtime/hydradx_runtime.wasm on-runtime-upgrade --blocktime 2000 --checks all live --uri wss://archive.rpc.hydration.cloud .PHONY: build-docs build-docs: diff --git a/README.md b/README.md index 83791f0c72..78f858f313 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,12 @@ Grab `zombienet` utility used to start network from [releases](https://github.co Start local testnet with 4 relay chain validators and HydraDX as a parachain with 2 collators. -``` +```bash cd launch-configs/zombienet/ zombienet spawn local.json + +# Enable 2s block time +npm exec --yes --package=@polkadot/api --package=@polkadot/util-crypto -- node scripts/assign_cores.js ``` ## Interaction with the node diff --git a/launch-configs/zombienet/local.json b/launch-configs/zombienet/local.json index ca9d056662..fde828e233 100644 --- a/launch-configs/zombienet/local.json +++ b/launch-configs/zombienet/local.json @@ -14,8 +14,11 @@ "patch": { "configuration": { "config": { + "scheduler_params": { + "num_cores": 3 + }, "async_backing_params": { - "max_candidate_depth": 3, + "max_candidate_depth": 6, "allowed_ancestry_len": 2 } } @@ -31,6 +34,7 @@ "--pruning=archive" ], "ws_port": 9944, + "rpc_port": 9945, "invulnerable": true }, { diff --git a/math/src/staking/math.rs b/math/src/staking/math.rs index 7b9d63435c..c04490fa12 100644 --- a/math/src/staking/math.rs +++ b/math/src/staking/math.rs @@ -50,22 +50,40 @@ pub fn calculate_slashed_points( /// Function calculates period number from block number and period size. /// /// Parameters: -/// - `period_length`: length of the one period in blocks +/// - `period_length`: length of one period based on 12s blocks (this was the initial default) /// - `block_number`: block number to calculate period for -/// - `six_sec_block_since`: block number when staking switched to 6 sec. blocks and period -/// - `period_length` should be doubled +/// - `six_sec_blocks_since`: block number when staking switched to 6s blocks +/// - `two_sec_blocks_since`: block number when staking switched to 2s blocks pub fn calculate_period_number( period_length: NonZeroU128, block_number: u128, - six_sec_block_since: NonZeroU128, + six_sec_blocks_since: NonZeroU128, + two_sec_blocks_since: NonZeroU128, ) -> Period { - if block_number.le(&Into::::into(six_sec_block_since)) { - return block_number.saturating_div(period_length.get()); + let period_length = period_length.get(); + let six_sec_blocks_since = six_sec_blocks_since.get(); + let two_sec_blocks_since = two_sec_blocks_since.get(); + + if block_number.le(&six_sec_blocks_since) { + return block_number.saturating_div(period_length); + } + + if block_number.le(&two_sec_blocks_since) || two_sec_blocks_since <= six_sec_blocks_since { + return six_sec_blocks_since + .saturating_add(block_number) + .saturating_div(period_length.saturating_mul(2)); } - Into::::into(six_sec_block_since) - .saturating_add(block_number) - .saturating_div(period_length.get().saturating_mul(2)) + let normalized_blocks = six_sec_blocks_since + .saturating_mul(6) + .saturating_add( + two_sec_blocks_since + .saturating_sub(six_sec_blocks_since) + .saturating_mul(3), + ) + .saturating_add(block_number.saturating_sub(two_sec_blocks_since)); + + normalized_blocks.saturating_div(period_length.saturating_mul(6)) } /// Function calculates total amount of `Points` user have accumulated until now. diff --git a/math/src/staking/tests.rs b/math/src/staking/tests.rs index 79b7df2a7a..37d88567bc 100644 --- a/math/src/staking/tests.rs +++ b/math/src/staking/tests.rs @@ -88,7 +88,8 @@ fn calculate_period_number_should_work_when_period_length_is_not_zero() { calculate_period_number( NonZeroU128::try_from(1_u128).unwrap(), 12_341_u128, - NonZeroU128::try_from(12_341_u128).unwrap() + NonZeroU128::try_from(12_341_u128).unwrap(), + NonZeroU128::try_from(u32::MAX as u128).unwrap() ), 12_341_u128 ); @@ -97,7 +98,8 @@ fn calculate_period_number_should_work_when_period_length_is_not_zero() { calculate_period_number( NonZeroU128::try_from(1_000_u128).unwrap(), 12_341_u128, - NonZeroU128::try_from(12_342_u128).unwrap() + NonZeroU128::try_from(12_342_u128).unwrap(), + NonZeroU128::try_from(u32::MAX as u128).unwrap() ), 12_u128 ); @@ -106,7 +108,8 @@ fn calculate_period_number_should_work_when_period_length_is_not_zero() { calculate_period_number( NonZeroU128::try_from(1_000_u128).unwrap(), 1_u128, - NonZeroU128::try_from(1).unwrap() + NonZeroU128::try_from(1).unwrap(), + NonZeroU128::try_from(u32::MAX as u128).unwrap() ), 0_u128 ); @@ -115,7 +118,8 @@ fn calculate_period_number_should_work_when_period_length_is_not_zero() { calculate_period_number( NonZeroU128::try_from(82_u128).unwrap(), 12_341_u128, - NonZeroU128::try_from(12_341_u128).unwrap() + NonZeroU128::try_from(12_341_u128).unwrap(), + NonZeroU128::try_from(u32::MAX as u128).unwrap() ), 150_u128 ); @@ -126,7 +130,8 @@ fn calculate_period_number_should_work_when_period_length_is_not_zero() { calculate_period_number( NonZeroU128::try_from(41_u128).unwrap(), 12_341_u128, - NonZeroU128::try_from(5_001_u128).unwrap() + NonZeroU128::try_from(5_001_u128).unwrap(), + NonZeroU128::try_from(u32::MAX as u128).unwrap() ), 211_u128 ); @@ -138,12 +143,69 @@ fn calculate_period_number_should_work_when_period_length_is_not_zero() { calculate_period_number( NonZeroU128::try_from(2_617_u128).unwrap(), 678_789_789_u128, - NonZeroU128::try_from(89_789_124_u128).unwrap() + NonZeroU128::try_from(89_789_124_u128).unwrap(), + NonZeroU128::try_from(u32::MAX as u128).unwrap() ), 146_843_u128 ); } +#[test] +fn calculate_period_number_should_work_after_two_sec_transition() { + assert_eq!( + calculate_period_number( + NonZeroU128::try_from(10_u128).unwrap(), + 100_u128, + NonZeroU128::try_from(100_u128).unwrap(), + NonZeroU128::try_from(200_u128).unwrap() + ), + 10_u128 + ); + + assert_eq!( + calculate_period_number( + NonZeroU128::try_from(10_u128).unwrap(), + 200_u128, + NonZeroU128::try_from(100_u128).unwrap(), + NonZeroU128::try_from(200_u128).unwrap() + ), + 15_u128 + ); + + assert_eq!( + calculate_period_number( + NonZeroU128::try_from(10_u128).unwrap(), + 259_u128, + NonZeroU128::try_from(100_u128).unwrap(), + NonZeroU128::try_from(200_u128).unwrap() + ), + 15_u128 + ); + + assert_eq!( + calculate_period_number( + NonZeroU128::try_from(10_u128).unwrap(), + 260_u128, + NonZeroU128::try_from(100_u128).unwrap(), + NonZeroU128::try_from(200_u128).unwrap() + ), + 16_u128 + ); +} + +#[test] +fn calculate_period_number_should_fall_back_when_two_sec_transition_is_invalid() { + assert_eq!( + calculate_period_number( + NonZeroU128::try_from(10_u128).unwrap(), + 200_u128, + NonZeroU128::try_from(100_u128).unwrap(), + NonZeroU128::try_from(50_u128).unwrap() + ), + 15_u128 + ); +} + #[test] fn calculate_points_should_work() { let time_points_per_period = 2_u8; diff --git a/node/Cargo.toml b/node/Cargo.toml index 9845b0ca9e..4781a74988 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx" -version = "15.0.2" +version = "15.1.0" description = "Hydration node" authors = ["GalacticCouncil"] edition = "2021" diff --git a/node/src/service.rs b/node/src/service.rs index aafe0ecfb9..0d56def251 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -618,7 +618,7 @@ fn start_consensus( para_id, proposer, collator_service, - authoring_duration: Duration::from_millis(1500), + authoring_duration: Duration::from_millis(2000), reinitialize: false, slot_offset: Duration::from_secs(1), block_import_handle, diff --git a/node/src/service/evm.rs b/node/src/service/evm.rs index 1934a0145c..5c6fdefc24 100644 --- a/node/src/service/evm.rs +++ b/node/src/service/evm.rs @@ -49,7 +49,7 @@ pub struct EthereumConfig { pub max_past_logs: u32, /// Maximum fee history cache size. - #[clap(long, default_value = "2048")] + #[clap(long, default_value = "6144")] pub fee_history_limit: u64, #[clap(long)] @@ -161,7 +161,7 @@ pub fn spawn_frontier_tasks( None, MappingSyncWorker::new( client.import_notification_stream(), - Duration::new(6, 0), + Duration::new(2, 0), client.clone(), backend, overrides.clone(), @@ -176,8 +176,8 @@ pub fn spawn_frontier_tasks( ); // Spawn Frontier EthFilterApi maintenance task. - // Each filter is allowed to stay in the pool for 100 blocks. - const FILTER_RETAIN_THRESHOLD: u64 = 100; + // Each filter is allowed to stay in the pool for about 10 minutes with 2s blocks. + const FILTER_RETAIN_THRESHOLD: u64 = 300; task_manager.spawn_essential_handle().spawn( "frontier-filter-pool", None, diff --git a/pallets/dca/Cargo.toml b/pallets/dca/Cargo.toml index b65f45a524..45aedea4e8 100644 --- a/pallets/dca/Cargo.toml +++ b/pallets/dca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-dca' -version = "1.18.1" +version = "1.19.0" description = 'A pallet to manage DCA scheduling' authors = ['GalacticCouncil'] edition = '2021' diff --git a/pallets/dca/src/lib.rs b/pallets/dca/src/lib.rs index cc7b6d441c..8025727844 100644 --- a/pallets/dca/src/lib.rs +++ b/pallets/dca/src/lib.rs @@ -130,7 +130,7 @@ pub mod pallet { use super::*; - const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(3); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] diff --git a/pallets/dca/src/migrations.rs b/pallets/dca/src/migrations.rs index a4a4b969a7..6dd9dc735e 100644 --- a/pallets/dca/src/migrations.rs +++ b/pallets/dca/src/migrations.rs @@ -2,12 +2,21 @@ use crate::pallet; use frame_support::traits::{Get, GetStorageVersion, OnRuntimeUpgrade, StorageVersion}; use sp_runtime::Saturating; -// This migration multiplies the periods of schedules by 2 to account for 2x faster block times -// -// The migration does not use a StorageVersion, make sure it is removed from the Runtime Executive -// after it has been run. -pub struct MultiplySchedulesPeriodBy2(sp_std::marker::PhantomData); -impl OnRuntimeUpgrade for MultiplySchedulesPeriodBy2 { +// This migration multiplies the periods of schedules by 3 to account for 3x faster block times +// when moving from 6s to 2s blocks. +pub struct MultiplySchedulesPeriodBy3(sp_std::marker::PhantomData); +impl OnRuntimeUpgrade for MultiplySchedulesPeriodBy3 { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + assert_eq!( + StorageVersion::get::>(), + StorageVersion::new(2), + "DCA storage version must be v2 before multiplying schedule periods" + ); + + Ok(sp_std::vec::Vec::new()) + } + fn on_runtime_upgrade() -> frame_support::weights::Weight { let mut reads = 0u64; let mut writes = 0u64; @@ -16,15 +25,27 @@ impl OnRuntimeUpgrade for MultiplySchedulesPeriodBy2 { let in_code_version = crate::Pallet::::in_code_storage_version(); reads.saturating_inc(); - if on_chain_version == in_code_version { + if on_chain_version >= in_code_version { // Already migrated return T::DbWeight::get().reads(reads); } - for (key, mut schedule) in crate::Schedules::::iter() { - schedule.period = schedule.period.saturating_mul(2u32.into()); + if on_chain_version != StorageVersion::new(2) { + log::warn!("DCA schedule period migration skipped: expected storage version 2, got {on_chain_version:?}"); + return T::DbWeight::get().reads(reads); + } + + let schedules: sp_std::vec::Vec<_> = crate::Schedules::::iter().collect(); + reads.saturating_accrue(schedules.len() as u64); + + log::info!( + "MultiplySchedulesPeriodBy3 found schedules: {:?}, processing cap: 150", + schedules.len() + ); + + for (key, mut schedule) in schedules { + schedule.period = schedule.period.saturating_mul(3u32.into()); crate::Schedules::::insert(key, schedule); - reads.saturating_inc(); writes.saturating_inc(); // At the time before the migration there are ~60 schedules. @@ -36,11 +57,23 @@ impl OnRuntimeUpgrade for MultiplySchedulesPeriodBy2 { } // Increase on-chain StorageVersion - StorageVersion::new(2).put::>(); + StorageVersion::new(3).put::>(); + writes.saturating_inc(); - log::info!("MultiplySchedulesPeriodBy2 processed schedules: {writes:?}"); + log::info!("MultiplySchedulesPeriodBy3 processed schedules: {writes:?}"); T::DbWeight::get().reads_writes(reads, writes) } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: sp_std::vec::Vec) -> Result<(), sp_runtime::TryRuntimeError> { + assert_eq!( + StorageVersion::get::>(), + StorageVersion::new(3), + "DCA storage version must be v3 after multiplying schedule periods" + ); + + Ok(()) + } } #[cfg(test)] @@ -55,7 +88,7 @@ mod test { use frame_support::assert_ok; #[test] - fn multiply_schedules_period_by_2_works() { + fn multiply_schedules_period_by_3_works() { ExtBuilder::default() .with_endowed_accounts(vec![(ALICE, HDX, 10000 * ONE)]) .build() @@ -71,17 +104,18 @@ mod test { let stored_schedule = DCA::schedules(0).unwrap(); assert_eq!(stored_schedule.period, 100); + StorageVersion::new(2).put::(); // Act - MultiplySchedulesPeriodBy2::::on_runtime_upgrade(); + MultiplySchedulesPeriodBy3::::on_runtime_upgrade(); let updated_schedule = DCA::schedules(0).unwrap(); // Assert - assert_eq!(updated_schedule.period, 200); + assert_eq!(updated_schedule.period, 300); // Storage version has been updated let on_chain_version = StorageVersion::get::(); - assert_eq!(on_chain_version, StorageVersion::new(2)); + assert_eq!(on_chain_version, StorageVersion::new(3)); }); } } diff --git a/pallets/dca/src/tests/on_initialize.rs b/pallets/dca/src/tests/on_initialize.rs index f1417fdb72..48ce9ec20a 100644 --- a/pallets/dca/src/tests/on_initialize.rs +++ b/pallets/dca/src/tests/on_initialize.rs @@ -26,6 +26,7 @@ use frame_support::assert_ok; use frame_support::traits::OnInitialize; use hydradx_traits::router::PoolType; use hydradx_traits::router::PoolType::Omnipool; +use hydradx_traits::OraclePeriod; use orml_traits::MultiCurrency; use orml_traits::MultiReservableCurrency; use orml_traits::NamedMultiReservableCurrency; @@ -34,6 +35,10 @@ use sp_runtime::DispatchError; use std::borrow::Borrow; use std::ops::RangeInclusive; +fn retry_block(current_block: u64, retries_before: u32) -> u64 { + current_block + OraclePeriod::Short.as_period() * 2_u64.pow(retries_before) +} + #[test] fn successful_sell_dca_execution_should_emit_trade_executed_event() { ExtBuilder::default() @@ -1170,7 +1175,7 @@ fn buy_dca_schedule_should_be_retried_when_trade_limit_error_happens() { let schedule_id = 0; - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); let retries = DCA::retries_on_error(schedule_id); assert_eq!(1, retries); expect_dca_events(vec![ @@ -1183,7 +1188,7 @@ fn buy_dca_schedule_should_be_retried_when_trade_limit_error_happens() { DcaEvent::ExecutionPlanned { id: schedule_id, who: ALICE, - block: 522, + block: retry_block(502, 0), } .into(), ]); @@ -1223,7 +1228,7 @@ fn sell_dca_schedule_should_be_retried_when_trade_limit_error_happens() { assert_number_of_executed_sell_trades!(0); let schedule_id = 0; - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); let retries = DCA::retries_on_error(schedule_id); assert_eq!(1, retries); expect_dca_events(vec![ @@ -1236,7 +1241,7 @@ fn sell_dca_schedule_should_be_retried_when_trade_limit_error_happens() { DcaEvent::ExecutionPlanned { id: schedule_id, who: ALICE, - block: 522, + block: retry_block(502, 0), } .into(), ]); @@ -1282,7 +1287,7 @@ fn dca_trade_unallocation_should_be_rolled_back_when_trade_fails() { set_to_blocknumber(502); assert_number_of_executed_buy_trades!(0); - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); let buy_fee_in_native = DCA::get_transaction_fee(&schedule.order, None).unwrap(); assert_eq!( @@ -1362,16 +1367,20 @@ fn dca_schedule_should_continue_on_multiple_failures_then_terminated() { //Act and assert let schedule_id = 0; + let retry_1 = retry_block(502, 0); + let retry_2 = retry_block(retry_1, 1); + let retry_3 = retry_block(retry_2, 2); + set_to_blocknumber(502); - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_1, vec![schedule_id]); - set_to_blocknumber(522); - assert_scheduled_ids!(562, vec![schedule_id]); + set_to_blocknumber(retry_1); + assert_scheduled_ids!(retry_2, vec![schedule_id]); - set_to_blocknumber(562); - assert_scheduled_ids!(642, vec![schedule_id]); + set_to_blocknumber(retry_2); + assert_scheduled_ids!(retry_3, vec![schedule_id]); - set_to_blocknumber(642); + set_to_blocknumber(retry_3); assert!(DCA::schedules(schedule_id).is_none()); assert_number_of_executed_buy_trades!(0); }); @@ -1407,26 +1416,32 @@ fn dca_schedule_should_use_specified_max_retry_count() { //Act and assert let schedule_id = 0; + let retry_1 = retry_block(502, 0); + let retry_2 = retry_block(retry_1, 1); + let retry_3 = retry_block(retry_2, 2); + let retry_4 = retry_block(retry_3, 3); + let retry_5 = retry_block(retry_4, 4); + set_to_blocknumber(502); - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_1, vec![schedule_id]); - set_to_blocknumber(522); - assert_scheduled_ids!(562, vec![schedule_id]); + set_to_blocknumber(retry_1); + assert_scheduled_ids!(retry_2, vec![schedule_id]); - set_to_blocknumber(562); - assert_scheduled_ids!(642, vec![schedule_id]); + set_to_blocknumber(retry_2); + assert_scheduled_ids!(retry_3, vec![schedule_id]); - set_to_blocknumber(642); - assert_scheduled_ids!(802, vec![schedule_id]); + set_to_blocknumber(retry_3); + assert_scheduled_ids!(retry_4, vec![schedule_id]); let retries = DCA::retries_on_error(schedule_id); assert_eq!(4, retries); - set_to_blocknumber(802); - assert_scheduled_ids!(1122, vec![schedule_id]); + set_to_blocknumber(retry_4); + assert_scheduled_ids!(retry_5, vec![schedule_id]); let retries = DCA::retries_on_error(schedule_id); assert_eq!(5, retries); - set_to_blocknumber(1122); + set_to_blocknumber(retry_5); assert!(DCA::schedules(schedule_id).is_none()); assert_number_of_executed_buy_trades!(0); }); @@ -1461,7 +1476,7 @@ fn buy_dca_schedule_should_continue_on_slippage_error() { //Act and assert let schedule_id = 0; set_to_blocknumber(502); - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); let retries = DCA::retries_on_error(schedule_id); assert_eq!(1, retries); }); @@ -1501,7 +1516,7 @@ fn sell_dca_schedule_continue_on_slippage_error() { //Act and assert let schedule_id = 0; set_to_blocknumber(502); - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); let retries = DCA::retries_on_error(schedule_id); assert_eq!(1, retries); }); @@ -1540,16 +1555,19 @@ fn dca_schedule_retry_should_be_reset_when_successful_trade_after_failed_ones() //Act and assert let schedule_id = 0; + let retry_1 = retry_block(502, 0); + let retry_2 = retry_block(retry_1, 1); + set_to_blocknumber(502); - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_1, vec![schedule_id]); - set_to_blocknumber(522); - assert_scheduled_ids!(562, vec![schedule_id]); + set_to_blocknumber(retry_1); + assert_scheduled_ids!(retry_2, vec![schedule_id]); set_max_price_diff(Permill::from_percent(10)); - set_to_blocknumber(562); - assert_scheduled_ids!(562 + ONE_HUNDRED_BLOCKS, vec![schedule_id]); + set_to_blocknumber(retry_2); + assert_scheduled_ids!(retry_2 + ONE_HUNDRED_BLOCKS, vec![schedule_id]); assert_number_of_executed_sell_trades!(1); let retries = DCA::retries_on_error(schedule_id); @@ -1930,7 +1948,7 @@ fn one_sell_dca_execution_should_be_rescheduled_when_price_diff_is_more_than_max assert_eq!(total_amount - fee_in_native, Currencies::reserved_balance(HDX, &ALICE)); let schedule_id = 0; - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); expect_dca_events(vec![ DcaEvent::TradeFailed { id: schedule_id, @@ -1941,7 +1959,7 @@ fn one_sell_dca_execution_should_be_rescheduled_when_price_diff_is_more_than_max DcaEvent::ExecutionPlanned { id: schedule_id, who: ALICE, - block: 522, + block: retry_block(502, 0), } .into(), ]); @@ -1993,7 +2011,7 @@ fn one_sell_dca_execution_should_be_rescheduled_when_price_diff_is_more_than_use ); let schedule_id = 0; - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); expect_dca_events(vec![ DcaEvent::TradeFailed { id: schedule_id, @@ -2004,7 +2022,7 @@ fn one_sell_dca_execution_should_be_rescheduled_when_price_diff_is_more_than_use DcaEvent::ExecutionPlanned { id: schedule_id, who: ALICE, - block: 522, + block: retry_block(502, 0), } .into(), ]); @@ -2060,7 +2078,7 @@ fn one_buy_dca_execution_should_be_rescheduled_when_price_diff_is_more_than_max_ ); let schedule_id = 0; - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); }); } @@ -2113,7 +2131,7 @@ fn specified_slippage_should_be_used_in_circuit_breaker_price_check() { ); let schedule_id = 0; - assert_scheduled_ids!(522, vec![schedule_id]); + assert_scheduled_ids!(retry_block(502, 0), vec![schedule_id]); let retries = DCA::retries_on_error(schedule_id); assert_eq!(1, retries); @@ -2214,8 +2232,8 @@ fn dca_should_be_terminated_when_price_change_is_big_but_no_free_blocks_to_repla assert_ok!(DCA::schedule( RuntimeOrigin::signed(ALICE), schedule, - Option::Some(1015) - )); //995 + 20 because 20 is the retry delay + Option::Some(retry_block(995, 0)) + )); } //Act diff --git a/pallets/ema-oracle/src/lib.rs b/pallets/ema-oracle/src/lib.rs index 08a5abe05f..ca1d69db03 100644 --- a/pallets/ema-oracle/src/lib.rs +++ b/pallets/ema-oracle/src/lib.rs @@ -42,7 +42,7 @@ //! - *Smoothing Factor*: A factor applied to each value aggregated into the averaging oracle. //! Implicitly determines the oracle period. //! - *Period*: The window over which an oracle is averaged. Certain smoothing factors correspond to -//! an oracle period. E.g. ten minutes oracle period ≈ 0.0198 +//! an oracle period. E.g. ten minutes oracle period ≈ 0.0066 //! - *Source*: The source of the data. E.g. xyk pallet. //! //! ### Implementation diff --git a/pallets/ema-oracle/src/tests/mod.rs b/pallets/ema-oracle/src/tests/mod.rs index 34fc8c220d..b01dc35131 100644 --- a/pallets/ema-oracle/src/tests/mod.rs +++ b/pallets/ema-oracle/src/tests/mod.rs @@ -658,13 +658,11 @@ fn calculate_new_by_integrating_incoming_with_works() { let next_oracle = start_oracle .calculate_new_by_integrating_incoming(TenMinutes, &next_value) .unwrap(); - // ten minutes corresponds to 100 blocks which corresponds to a smoothing factor of - // `2 / 101 ≈ 1 / 50` which means that for an update from 50 to 151 we expect an update of - // about 2 + // Ten minutes corresponds to 300 blocks, so the smoothing factor is `2 / 301`. let expected_oracle = OracleEntry { - price: Price::new(52, 1), - volume: Volume::from_a_in_b_out(1, 52), - liquidity: Liquidity::new(52, 1), + price: Price::new(15_252, 301), + volume: Volume::from_a_in_b_out(1, 51), + liquidity: Liquidity::new(51, 1), shares_issuance: Some(10_u128), updated_at: 6, }; @@ -712,7 +710,7 @@ fn calculate_current_from_outdated_should_incorporate_longer_time_deltas() { volume: Volume::from_a_in_b_out(1, 8_000), liquidity: Liquidity::new(8_000, 1), shares_issuance: Some(10_u128), - updated_at: 1_000, + updated_at: 3_000, }; let next_oracle = start_oracle .calculate_current_from_outdated(period, &next_value) @@ -794,7 +792,7 @@ fn get_entry_works() { let expected_ten_min = AggregatedEntry { price: Price::new(2_000, 1_000), - volume: Volume::from_a_in_b_out(141, 70), // volume oracle gets updated towards zero + volume: Volume::from_a_in_b_out(520, 260), // volume oracle gets updated towards zero liquidity: Liquidity::new(2_000, 1_000), shares_issuance: Some(1_000_u128), oracle_age: 98, @@ -803,7 +801,7 @@ fn get_entry_works() { let expected_day = AggregatedEntry { price: Price::new(2_000, 1_000), - volume: Volume::from_a_in_b_out(986, 493), + volume: Volume::from_a_in_b_out(995, 498), liquidity: Liquidity::new(2_000, 1_000), shares_issuance: Some(1_000_u128), oracle_age: 98, @@ -812,7 +810,7 @@ fn get_entry_works() { let expected_week = AggregatedEntry { price: Price::new(2_000, 1_000), - volume: Volume::from_a_in_b_out(998, 499), + volume: Volume::from_a_in_b_out(999, 500), liquidity: Liquidity::new(2_000, 1_000), shares_issuance: Some(1_000_u128), oracle_age: 98, @@ -872,13 +870,13 @@ fn get_price_returns_updated_price() { ); assert_price_approx_eq!( EmaOracle::get_price(HDX, DOT, Day, SOURCE).unwrap().0, - Price::new(6_246_761_041_102_896_u128, 10_000_000_000), + Price::new(8_147_079_718_630_222_u128, 10_000_000_000), tolerance, "Day Oracle should converge somewhat." ); assert_price_approx_eq!( EmaOracle::get_price(HDX, DOT, Week, SOURCE).unwrap().0, - Price::new(9_100_156_788_246_781_u128, 10_000_000_000), + Price::new(9_680_010_466_208_035_u128, 10_000_000_000), tolerance, "Week Oracle should converge a little." ); @@ -936,8 +934,8 @@ fn ema_update_should_return_none_if_new_entry_is_older() { fn check_period_smoothing_factors() { use hydra_dx_math::ema::smoothing_from_period; - // We assume a 6 second block time. - let secs_per_block = 6; + // We assume a 2 second block time. + let secs_per_block = 2; let minutes = 60 / secs_per_block; let hours = 60 * minutes; let days = 24 * hours; @@ -946,7 +944,7 @@ fn check_period_smoothing_factors() { println!("Last Block: {} (bits: {})", last_block, last_block.to_bits()); assert_eq!(into_smoothing(LastBlock), last_block); - let short = smoothing_from_period(20); + let short = smoothing_from_period(2 * minutes); println!("Short: {} (bits: {})", short, short.to_bits()); assert_eq!(into_smoothing(Short), short); diff --git a/pallets/ema-oracle/src/types.rs b/pallets/ema-oracle/src/types.rs index d0fe176a99..9eaa6301de 100644 --- a/pallets/ema-oracle/src/types.rs +++ b/pallets/ema-oracle/src/types.rs @@ -235,11 +235,11 @@ where pub fn into_smoothing(period: OraclePeriod) -> Fraction { match period { OraclePeriod::LastBlock => Fraction::from_bits(170141183460469231731687303715884105728), - OraclePeriod::Short => Fraction::from_bits(16203922234330403022065457496750867212), - OraclePeriod::TenMinutes => Fraction::from_bits(3369132345751865974884897103284833777), - OraclePeriod::Hour => Fraction::from_bits(566193622164623067326746434994622648), - OraclePeriod::Day => Fraction::from_bits(23629079016800115510268356880200556), - OraclePeriod::Week => Fraction::from_bits(3375783642235081630771268215908257), + OraclePeriod::Short => Fraction::from_bits(5578399457720302679727452580848659204), + OraclePeriod::TenMinutes => Fraction::from_bits(1130506202395144396888287732331455852), + OraclePeriod::Hour => Fraction::from_bits(188940792293691539957453974143125048), + OraclePeriod::Day => Fraction::from_bits(7876724310107137877905016259618254), + OraclePeriod::Week => Fraction::from_bits(1125268656257546977236763791891456), } } diff --git a/pallets/gigahdx/src/lib.rs b/pallets/gigahdx/src/lib.rs index 79a7d67525..c6cb5d0afa 100644 --- a/pallets/gigahdx/src/lib.rs +++ b/pallets/gigahdx/src/lib.rs @@ -38,6 +38,8 @@ pub use pallet::*; pub mod traits; +pub mod migrations; + #[cfg(test)] mod tests; @@ -80,7 +82,7 @@ pub mod pallet { use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use frame_support::pallet_prelude::*; use frame_support::sp_runtime::helpers_128bit::multiply_by_rational_with_rounding; - use frame_support::sp_runtime::traits::{AccountIdConversion, CheckedAdd}; + use frame_support::sp_runtime::traits::{AccountIdConversion, CheckedAdd, SaturatedConversion}; use frame_support::sp_runtime::{ArithmeticError, Rounding}; use frame_support::traits::fungibles::Mutate as FungiblesMutate; use frame_support::traits::tokens::{Fortitude, Precision, Preservation}; @@ -130,7 +132,12 @@ pub mod pallet { pub amount: Balance, } - pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); + + #[pallet::type_value] + pub fn DefaultTwoSecSince() -> BlockNumberFor { + u32::MAX.into() + } /// Defensive tripwire bound for `realize_yield`. Aggregate solvency /// guarantees the gigapot covers all accrued yield; a *per-account* @@ -240,6 +247,12 @@ pub mod pallet { OptionQuery, >; + #[pallet::storage] + /// Block number when the runtime switched to 2 second blocks. + #[pallet::getter(fn two_sec_blocks_since)] + pub(super) type TwoSecBlocksSince = + StorageValue<_, BlockNumberFor, ValueQuery, DefaultTwoSecSince>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -470,9 +483,7 @@ pub mod pallet { let who = ensure_signed(origin)?; let entry = PendingUnstakes::::get(&who, position_id).ok_or(Error::::PendingUnstakeNotFound)?; - let expires_at = position_id - .checked_add(&T::CooldownPeriod::get()) - .ok_or(Error::::Overflow)?; + let expires_at = Self::cooldown_expires_at(position_id)?; ensure!( frame_system::Pallet::::block_number() >= expires_at, Error::::CooldownNotElapsed, @@ -737,7 +748,7 @@ pub mod pallet { }; let principal_consumed = stake.hdx.saturating_sub(new_hdx); - let expires_at = now.checked_add(&T::CooldownPeriod::get()).ok_or(Error::::Overflow)?; + let expires_at = Self::cooldown_expires_at(now)?; PendingUnstakes::::mutate(who, now, |maybe| { let entry = maybe.get_or_insert(PendingUnstake { amount: 0 }); @@ -769,6 +780,37 @@ pub mod pallet { } impl Pallet { + /// Expiry block for a pending unstake position keyed by its originating block. + /// Positions created before the 2s switch preserve their remaining wall-clock cooldown. + pub fn cooldown_expires_at(unstaked_at: BlockNumberFor) -> Result, Error> { + let switch_block = Self::two_sec_blocks_since(); + let cooldown = T::CooldownPeriod::get(); + + if switch_block == u32::MAX.into() || unstaked_at >= switch_block { + return unstaked_at.checked_add(&cooldown).ok_or(Error::::Overflow); + } + + let created_at: u128 = unstaked_at.saturated_into(); + let switch: u128 = switch_block.saturated_into(); + // Runtime cooldown is now expressed in 2s blocks; pre-switch positions were created with 6s blocks. + let old_cooldown: u128 = cooldown.saturated_into::() / 3; + let old_expires_at = created_at.checked_add(old_cooldown).ok_or(Error::::Overflow)?; + + let expires_at = if old_expires_at <= switch { + old_expires_at + } else { + let remaining_at_switch = old_expires_at.checked_sub(switch).ok_or(Error::::Overflow)?; + switch + .checked_add(remaining_at_switch.checked_mul(3).ok_or(Error::::Overflow)?) + .ok_or(Error::::Overflow)? + }; + + let converted: BlockNumberFor = expires_at.saturated_into(); + let roundtrip: u128 = converted.saturated_into(); + ensure!(roundtrip == expires_at, Error::::Overflow); + Ok(converted) + } + /// Admission gate shared by `giga_stake` and `migrate`. /// /// Enforces the `MinStake` floor, the strict no-overlapping-lock policy diff --git a/pallets/gigahdx/src/migrations.rs b/pallets/gigahdx/src/migrations.rs new file mode 100644 index 0000000000..ffb4ae3b60 --- /dev/null +++ b/pallets/gigahdx/src/migrations.rs @@ -0,0 +1,71 @@ +use crate::pallet; +use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; +use sp_core::Get; + +pub struct SetTwoSecBlocksSince(sp_std::marker::PhantomData); +impl OnRuntimeUpgrade for SetTwoSecBlocksSince { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + Ok(sp_std::vec::Vec::new()) + } + + fn on_runtime_upgrade() -> Weight { + let current_block_height = frame_system::Pallet::::block_number(); + let mut writes = 0u64; + + let two_sec_blocks_since = crate::TwoSecBlocksSince::::get(); + if two_sec_blocks_since == u32::MAX.into() { + crate::TwoSecBlocksSince::::put(current_block_height); + writes += 1; + + log::info!("GigaHdx TwoSecBlocksSince set to: {current_block_height:?}"); + } else { + log::info!("GigaHdx TwoSecBlocksSince already set to: {two_sec_blocks_since:?}"); + } + + T::DbWeight::get().reads_writes(1, writes) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: sp_std::vec::Vec) -> Result<(), sp_runtime::TryRuntimeError> { + assert!( + crate::TwoSecBlocksSince::::get() != u32::MAX.into(), + "GigaHdx TwoSecBlocksSince must be initialized" + ); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + tests::mock::{ExtBuilder, GigaHdx, System, Test}, + TwoSecBlocksSince, + }; + use frame_support::traits::OnRuntimeUpgrade; + + #[test] + fn set_two_sec_blocks_since_executes_when_storage_not_set() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(500); + + SetTwoSecBlocksSince::::on_runtime_upgrade(); + + assert_eq!(GigaHdx::two_sec_blocks_since(), 500); + }); + } + + #[test] + fn set_two_sec_blocks_since_does_not_overwrite_existing_value() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(500); + TwoSecBlocksSince::::put(123); + + SetTwoSecBlocksSince::::on_runtime_upgrade(); + + assert_eq!(GigaHdx::two_sec_blocks_since(), 123); + }); + } +} diff --git a/pallets/gigahdx/src/tests/mock.rs b/pallets/gigahdx/src/tests/mock.rs index 6359c3bb8f..3ec980dc1f 100644 --- a/pallets/gigahdx/src/tests/mock.rs +++ b/pallets/gigahdx/src/tests/mock.rs @@ -317,7 +317,7 @@ pub fn only_pending(who: AccountId) -> PendingView { PendingView { id, amount: p.amount, - expires_at: id + GigaHdxCooldownPeriod::get(), + expires_at: GigaHdx::cooldown_expires_at(id).expect("cooldown expiry should fit"), } } diff --git a/pallets/gigahdx/src/tests/mod.rs b/pallets/gigahdx/src/tests/mod.rs index 50348bd3d0..b05a028632 100644 --- a/pallets/gigahdx/src/tests/mod.rs +++ b/pallets/gigahdx/src/tests/mod.rs @@ -5,7 +5,7 @@ mod do_stake; mod freeze; mod invariants; mod migrate; -mod mock; +pub(crate) mod mock; mod multi_positions; mod realize_yield; mod seize; diff --git a/pallets/gigahdx/src/tests/unlock.rs b/pallets/gigahdx/src/tests/unlock.rs index da660a8869..ea71f11f85 100644 --- a/pallets/gigahdx/src/tests/unlock.rs +++ b/pallets/gigahdx/src/tests/unlock.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 use super::mock::*; -use crate::{Error, Stakes}; +use crate::{Error, Stakes, TwoSecBlocksSince}; use frame_support::sp_runtime::traits::AccountIdConversion; use frame_support::traits::tokens::{Fortitude, Preservation}; use frame_support::traits::{fungible::Inspect, LockIdentifier}; @@ -125,6 +125,71 @@ fn unlock_should_fail_when_cooldown_not_elapsed() { }); } +#[test] +fn unlock_should_preserve_remaining_cooldown_when_position_was_created_before_2s_switch() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(10); + stake_alice_100(); + assert_ok!(GigaHdx::giga_unstake(RawOrigin::Signed(ALICE).into(), 100 * ONE)); + TwoSecBlocksSince::::put(20); + + // Mock cooldown is 100 new blocks, so the old 6s cooldown is 33 blocks. + // At the switch block 23 old blocks remain, which become 69 blocks at 2s. + assert_eq!(GigaHdx::cooldown_expires_at(10).unwrap(), 89); + + System::set_block_number(88); + assert_noop!( + GigaHdx::unlock(RawOrigin::Signed(ALICE).into(), 10), + Error::::CooldownNotElapsed + ); + + System::set_block_number(89); + assert_ok!(GigaHdx::unlock(RawOrigin::Signed(ALICE).into(), 10)); + assert_eq!(lock_amount(ALICE, GIGAHDX_LOCK_ID), 0); + }); +} + +#[test] +fn unlock_should_use_old_deadline_when_pre_switch_position_already_matured_at_switch() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(10); + stake_alice_100(); + assert_ok!(GigaHdx::giga_unstake(RawOrigin::Signed(ALICE).into(), 100 * ONE)); + TwoSecBlocksSince::::put(50); + + assert_eq!(GigaHdx::cooldown_expires_at(10).unwrap(), 43); + + System::set_block_number(49); + assert_ok!(GigaHdx::unlock(RawOrigin::Signed(ALICE).into(), 10)); + assert_eq!(lock_amount(ALICE, GIGAHDX_LOCK_ID), 0); + }); +} + +#[test] +fn unlock_should_use_new_cooldown_when_position_was_created_after_2s_switch() { + ExtBuilder::default().build().execute_with(|| { + TwoSecBlocksSince::::put(20); + System::set_block_number(30); + stake_alice_100(); + assert_ok!(GigaHdx::giga_unstake(RawOrigin::Signed(ALICE).into(), 100 * ONE)); + + assert_eq!( + GigaHdx::cooldown_expires_at(30).unwrap(), + 30 + GigaHdxCooldownPeriod::get() + ); + + System::set_block_number(30 + GigaHdxCooldownPeriod::get() - 1); + assert_noop!( + GigaHdx::unlock(RawOrigin::Signed(ALICE).into(), 30), + Error::::CooldownNotElapsed + ); + + System::set_block_number(30 + GigaHdxCooldownPeriod::get()); + assert_ok!(GigaHdx::unlock(RawOrigin::Signed(ALICE).into(), 30)); + assert_eq!(lock_amount(ALICE, GIGAHDX_LOCK_ID), 0); + }); +} + #[test] fn unlock_should_release_lock_when_cooldown_elapsed() { ExtBuilder::default().build().execute_with(|| { diff --git a/pallets/lbp/src/lib.rs b/pallets/lbp/src/lib.rs index 5dc5a340e2..697bff68f0 100644 --- a/pallets/lbp/src/lib.rs +++ b/pallets/lbp/src/lib.rs @@ -86,7 +86,7 @@ pub enum WeightCurveType { /// Max weight corresponds to 100% pub const MAX_WEIGHT: LBPWeight = 100_000_000; -/// Max sale duration is 14 days, assuming 6 sec blocks +/// Max sale duration is 14 days, assuming 6 sec blocks (relay chain) pub const MAX_SALE_DURATION: u32 = (60 * 60 * 24 / 6) * 14; /// Lock Identifier for the collected fees diff --git a/pallets/staking/Cargo.toml b/pallets/staking/Cargo.toml index 4cbca65a17..c7729f81d1 100644 --- a/pallets/staking/Cargo.toml +++ b/pallets/staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-staking" -version = "5.1.0" +version = "5.2.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index 4499a1601e..0cffa5941a 100644 --- a/pallets/staking/src/lib.rs +++ b/pallets/staking/src/lib.rs @@ -71,7 +71,7 @@ pub mod pallet { use sp_runtime::traits::AtLeast32BitUnsigned; /// Current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(3); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -194,6 +194,11 @@ pub mod pallet { u32::MAX.into() } + #[pallet::type_value] + pub fn DefaultTwoSecSince() -> BlockNumberFor { + u32::MAX.into() + } + #[pallet::storage] /// Global staking state. #[pallet::getter(fn staking)] @@ -256,6 +261,12 @@ pub mod pallet { pub(super) type SixSecBlocksSince = StorageValue<_, BlockNumberFor, ValueQuery, DefaultSixSecSince>; + #[pallet::storage] + /// Block number when we switched to 2 sec. blocks. + #[pallet::getter(fn two_sec_blocks_since)] + pub(super) type TwoSecBlocksSince = + StorageValue<_, BlockNumberFor, ValueQuery, DefaultTwoSecSince>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -1085,6 +1096,7 @@ impl Pallet { NonZeroU128::try_from(T::PeriodLength::get().saturated_into::()).ok()?, block.saturated_into(), NonZeroU128::try_from(Self::six_sec_blocks_since().saturated_into::()).ok()?, + NonZeroU128::try_from(Self::two_sec_blocks_since().saturated_into::()).ok()?, )) } diff --git a/pallets/staking/src/migrations.rs b/pallets/staking/src/migrations.rs index 086064a49a..3402680187 100644 --- a/pallets/staking/src/migrations.rs +++ b/pallets/staking/src/migrations.rs @@ -1,5 +1,8 @@ use crate::pallet; -use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; +use frame_support::{ + traits::{OnRuntimeUpgrade, StorageVersion}, + weights::Weight, +}; use sp_core::Get; use sp_runtime::traits::BlockNumberProvider; @@ -22,11 +25,66 @@ impl OnRuntimeUpgrade for SetSixSecBlocksSince { } } -#[cfg(all(feature = "try-runtime", test))] +// This migration sets TwoSecBlocksSince which is used to correctly calculate the periods in staking +// after the migration to 2s block time. +pub struct SetTwoSecBlocksSince(sp_std::marker::PhantomData); +impl OnRuntimeUpgrade for SetTwoSecBlocksSince { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + assert!( + StorageVersion::get::>() < StorageVersion::new(3), + "Staking storage version must be below v3 before setting TwoSecBlocksSince" + ); + + Ok(sp_std::vec::Vec::new()) + } + + fn on_runtime_upgrade() -> Weight { + let on_chain_version = StorageVersion::get::>(); + if on_chain_version >= StorageVersion::new(3) { + return T::DbWeight::get().reads(1); + } + + let current_block_height = T::BlockNumberProvider::current_block_number(); + let mut writes = 0u64; + + let two_sec_blocks_since = crate::TwoSecBlocksSince::::get(); + if two_sec_blocks_since == u32::MAX.into() { + crate::TwoSecBlocksSince::::put(current_block_height); + writes += 1; + + log::info!("TwoSecBlocksSince set to: {current_block_height:?}"); + } else { + log::info!("TwoSecBlocksSince already set to: {two_sec_blocks_since:?}"); + } + + StorageVersion::new(3).put::>(); + + T::DbWeight::get().reads_writes(2, writes + 1) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: sp_std::vec::Vec) -> Result<(), sp_runtime::TryRuntimeError> { + assert_eq!( + StorageVersion::get::>(), + StorageVersion::new(3), + "Staking storage version must be v3 after setting TwoSecBlocksSince" + ); + assert!( + crate::TwoSecBlocksSince::::get() != u32::MAX.into(), + "TwoSecBlocksSince must be initialized" + ); + + Ok(()) + } +} + +#[cfg(test)] mod test { use super::*; - use crate::migrations::SetSixSecBlocksSince; + use crate::migrations::{SetSixSecBlocksSince, SetTwoSecBlocksSince}; use crate::tests::mock::{set_block_number, ExtBuilder, Staking, Test}; + use crate::SixSecBlocksSince; use frame_system::pallet_prelude::BlockNumberFor; #[test] @@ -34,6 +92,7 @@ mod test { ExtBuilder::default().build().execute_with(|| { // Arrange set_block_number(500); + SixSecBlocksSince::::put(u32::MAX as BlockNumberFor); // Act SetSixSecBlocksSince::::on_runtime_upgrade(); @@ -48,6 +107,7 @@ mod test { ExtBuilder::default().build().execute_with(|| { // Arrange set_block_number(500); + SixSecBlocksSince::::put(u32::MAX as BlockNumberFor); SetSixSecBlocksSince::::on_runtime_upgrade(); // Act @@ -58,4 +118,54 @@ mod test { assert_eq!(Staking::six_sec_blocks_since(), 500u32 as BlockNumberFor); }); } + + #[test] + fn set_two_blocks_since_executes_when_storage_not_set() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + set_block_number(500); + StorageVersion::new(2).put::(); + + // Act + SetTwoSecBlocksSince::::on_runtime_upgrade(); + + // Assert + assert_eq!(Staking::two_sec_blocks_since(), 500u32 as BlockNumberFor); + assert_eq!(StorageVersion::get::(), StorageVersion::new(3)); + }); + } + + #[test] + fn set_two_blocks_since_does_not_execute_when_storage_is_set() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + set_block_number(500); + StorageVersion::new(2).put::(); + SetTwoSecBlocksSince::::on_runtime_upgrade(); + + // Act + set_block_number(1000); + StorageVersion::new(2).put::(); + SetTwoSecBlocksSince::::on_runtime_upgrade(); + + // Assert + assert_eq!(Staking::two_sec_blocks_since(), 500u32 as BlockNumberFor); + assert_eq!(StorageVersion::get::(), StorageVersion::new(3)); + }); + } + + #[test] + fn set_two_blocks_since_does_not_execute_when_storage_version_is_current() { + ExtBuilder::default().build().execute_with(|| { + // Arrange + set_block_number(500); + StorageVersion::new(3).put::(); + + // Act + SetTwoSecBlocksSince::::on_runtime_upgrade(); + + // Assert + assert_eq!(Staking::two_sec_blocks_since(), u32::MAX as BlockNumberFor); + }); + } } diff --git a/pallets/staking/src/tests/tests.rs b/pallets/staking/src/tests/tests.rs index 46c478d8e1..32b8bf2102 100644 --- a/pallets/staking/src/tests/tests.rs +++ b/pallets/staking/src/tests/tests.rs @@ -13,6 +13,20 @@ use pallet_conviction_voting::VotingHooks; use pretty_assertions::assert_eq; //NOTE: Referendums with even indexes are finished. +#[test] +fn staking_period_number_should_account_for_two_sec_transition() { + ExtBuilder::default().build().execute_with(|| { + SixSecBlocksSince::::put(100); + TwoSecBlocksSince::::put(200); + + assert_eq!(Staking::get_period_number(100), Some(0)); + assert_eq!(Staking::get_period_number(200), Some(0)); + assert_eq!(Staking::get_period_number(59_299), Some(0)); + assert_eq!(Staking::get_period_number(59_300), Some(1)); + assert_eq!(Staking::get_period_number(119_300), Some(2)); + }); +} + #[test] fn process_votes_should_work_when_referendum_is_finished() { ExtBuilder::default() diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 3cbd796b03..4b5aa3d67f 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "primitives" -version = "6.4.0" +version = "6.4.1" authors = ["GalacticCouncil"] edition = "2021" repository = "https://github.com/galacticcouncil/HydraDX-node" diff --git a/primitives/src/constants.rs b/primitives/src/constants.rs index 6fea78d802..cf67a36dbf 100644 --- a/primitives/src/constants.rs +++ b/primitives/src/constants.rs @@ -44,8 +44,12 @@ pub mod time { /// `SLOT_DURATION` is picked up by `pallet_timestamp` which is in turn picked /// up by `pallet_aura` to implement `fn slot_duration()`. /// Change this to adjust the block time. - pub const MILLISECS_PER_BLOCK: u64 = 6_000; - pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; + pub const MILLISECS_PER_BLOCK: u64 = 2_000; + + // The slot duration determines the length of each author's turn and is decoupled from the block + // production interval. During their slot, authors are allowed to produce multiple blocks. The slot + // duration is required to be at least 6s, the same as on the relay chain. + pub const SLOT_DURATION: u64 = 6_000; // Time is measured by number of blocks. pub const MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); @@ -73,9 +77,9 @@ pub mod chain { pub const CORE_ASSET_ID: AssetId = 0; pub const HOLLAR_ASSET_ID: AssetId = 222; - /// We allow for 2 seconds of compute with a 6 seconds average block. + /// We allow for 1.5 seconds of compute with a 2 seconds average block. pub const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( - WEIGHT_REF_TIME_PER_SECOND.saturating_mul(2), + WEIGHT_REF_TIME_PER_SECOND.saturating_mul(3).saturating_div(2), polkadot_primitives::v8::MAX_POV_SIZE as u64, ); @@ -91,7 +95,7 @@ pub mod chain { pub const UNINCLUDED_SEGMENT_CAPACITY: u32 = (3 + DEFAULT_RELAY_PARENT_OFFSET) * BLOCK_PROCESSING_VELOCITY; /// How many parachain blocks are processed by the relay chain per parent. Limits the number of /// blocks authored per slot. - pub const BLOCK_PROCESSING_VELOCITY: u32 = 1; + pub const BLOCK_PROCESSING_VELOCITY: u32 = 3; /// Relay chain slot duration, in milliseconds. pub const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; } @@ -107,10 +111,10 @@ mod tests { assert_eq!(DAYS / 24, HOURS); // 60 minuts in an hour assert_eq!(HOURS / 60, MINUTES); - // 1 minute = 60s = 10 blocks 6s each - assert_eq!(MINUTES, 10); - // 6s per block - assert_eq!(SECS_PER_BLOCK, 6); + // 1 minute = 60s = 30 blocks 2s each + assert_eq!(MINUTES, 30); + // 2s per block + assert_eq!(SECS_PER_BLOCK, 2); // 1s = 1000ms assert_eq!(MILLISECS_PER_BLOCK / 1000, SECS_PER_BLOCK); // Extra check for epoch time because changing it bricks the block production and requires regenesis diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 42cb4ede0d..bc8a98828c 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "426.0.0" +version = "427.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index c3ff7fb00c..c5e24e0e99 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -610,8 +610,8 @@ impl Contains for DepositLockWhitelist { } parameter_types! { - pub const DefaultMaxNetTradeVolumeLimitPerBlock: (u32, u32) = (5_000, 10_000); // 50% - pub const DefaultMaxLiquidityLimitPerBlock: Option<(u32, u32)> = Some((500, 10_000)); // 5% + pub const DefaultMaxNetTradeVolumeLimitPerBlock: (u32, u32) = (1_670, 10_000); // 17% + pub const DefaultMaxLiquidityLimitPerBlock: Option<(u32, u32)> = Some((167, 10_000)); // 1.7% } impl pallet_circuit_breaker::Config for Runtime { @@ -661,7 +661,7 @@ where impl pallet_ema_oracle::Config for Runtime { type AuthorityOrigin = EitherOf, EconomicParameters>; - /// The definition of the oracle time periods currently assumes a 6 second block time. + /// The definition of the oracle time periods currently assumes a 2 second block time. /// We use the parachain blocks anyway, because we want certain guarantees over how many blocks correspond /// to which smoothing factor. type BlockNumberProvider = System; @@ -908,10 +908,10 @@ impl frame_system::offchain::SigningTypes for Runtime { parameter_types! { pub MinBudgetInNativeCurrency: Balance = 1000 * UNITS; - pub MaxSchedulesPerBlock: u32 = 6; + pub MaxSchedulesPerBlock: u32 = 2; pub MaxPriceDifference: Permill = Permill::from_rational(15u32, 1000u32); pub MaxConfigurablePriceDifference: Permill = Permill::from_percent(5); - pub MinimalPeriod: u32 = 5; + pub MinimalPeriod: u32 = 15; pub BumpChance: Percent = Percent::from_percent(17); pub NamedReserveId: NamedReserveIdentifier = *b"dcaorder"; pub MaxNumberOfRetriesOnError: u8 = 3; @@ -1605,7 +1605,7 @@ impl pallet_bonds::Config for Runtime { parameter_types! { pub const StakingPalletId: PalletId = PalletId(*b"staking#"); pub const MinStake: Balance = 1_000 * UNITS; - pub const PeriodLength: BlockNumber = 7_200; // 1d based on 12s blocks, pallet accounts for migration to 6s blocks + pub const PeriodLength: BlockNumber = 7_200; // 1d based on 12s blocks, pallet accounts for migrations to 6s / 2s blocks pub const TimePointsW:Permill = Permill::from_percent(100); pub const ActionPointsW: Perbill = Perbill::from_percent(20); pub const TimePointsPerPeriod: u8 = 1; @@ -2328,7 +2328,7 @@ impl TryConvert<&::RuntimeCall, AssetIdOf BlockNumber { + MinimalPeriod::get() +} + fn schedule_fake( owner: AccountId, asset_in: AssetId, @@ -64,7 +69,7 @@ fn schedule_fake( ) -> Schedule { let schedule1: Schedule = Schedule { owner, - period: 5u32, + period: minimum_schedule_period(), total_amount: 1100 * ONE, max_retries: None, stability_threshold: None, @@ -96,7 +101,7 @@ fn schedule_buy_fake( ) -> Schedule { let schedule1: Schedule = Schedule { owner, - period: 5u32, + period: minimum_schedule_period(), total_amount: 2000 * ONE, max_retries: None, stability_threshold: None, @@ -124,7 +129,7 @@ fn schedule_sell_fake( ) -> Schedule { let schedule1: Schedule = Schedule { owner, - period: 5u32, + period: minimum_schedule_period(), total_amount: 2000 * ONE, max_retries: None, stability_threshold: None, @@ -232,7 +237,7 @@ runtime_benchmarks! { //Make sure that we have other schedules planned in the block where the benchmark schedule is planned, leading to worst case //We leave only one slot - let schedule_period = 5; + let schedule_period = minimum_schedule_period(); let next_block_to_replan = execution_block + schedule_period; let number_of_all_schedules = MaxSchedulesPerBlock::get() + MaxSchedulesPerBlock::get() * RETRY_TO_SEARCH_FOR_FREE_BLOCK - 1; for i in 0..number_of_all_schedules { @@ -277,7 +282,7 @@ runtime_benchmarks! { //Make sure that we have other schedules planned in the block where the benchmark schedule is planned, leading to worst case //We leave only one slot - let schedule_period = 5; + let schedule_period = minimum_schedule_period(); let next_block_to_replan = execution_block + schedule_period; let number_of_all_schedules = MaxSchedulesPerBlock::get() + MaxSchedulesPerBlock::get() * RETRY_TO_SEARCH_FOR_FREE_BLOCK - 1; for i in 0..number_of_all_schedules { @@ -321,7 +326,7 @@ runtime_benchmarks! { //Make sure that we have other schedules planned in the block where the benchmark schedule is planned, leading to worst case //We leave only one slot - let schedule_period = 5; + let schedule_period = minimum_schedule_period(); let next_block_to_replan = execution_block + schedule_period; let number_of_all_schedules = MaxSchedulesPerBlock::get() + MaxSchedulesPerBlock::get() * RETRY_TO_SEARCH_FOR_FREE_BLOCK - 1; for i in 0..number_of_all_schedules { @@ -363,7 +368,7 @@ runtime_benchmarks! { //Make sure that we have other schedules planned in the block where the benchmark schedule is planned, leading to worst case //We leave only one slot - let schedule_period = 5; + let schedule_period = minimum_schedule_period(); let next_block_to_replan = execution_block + schedule_period; let number_of_all_schedules = MaxSchedulesPerBlock::get() + MaxSchedulesPerBlock::get() * RETRY_TO_SEARCH_FOR_FREE_BLOCK - 1; for i in 0..number_of_all_schedules { @@ -419,7 +424,7 @@ runtime_benchmarks! { create_xyk_pool(asset_8, asset_9); create_xyk_pool(asset_9, HDX); - set_period(10); + set_period(minimum_schedule_period()); let route = vec![ Trade { @@ -477,7 +482,7 @@ runtime_benchmarks! { let schedule1: Schedule = Schedule { owner:caller.clone() , - period: 5u32, + period: minimum_schedule_period(), total_amount: 1100 * ONE, max_retries: None, stability_threshold: None, diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 3da81766b4..a1046eebef 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -129,7 +129,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: Cow::Borrowed("hydradx"), impl_name: Cow::Borrowed("hydradx"), authoring_version: 1, - spec_version: 426, + spec_version: 427, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/hydradx/src/migrations/circuit_breaker.rs b/runtime/hydradx/src/migrations/circuit_breaker.rs new file mode 100644 index 0000000000..5f610972d2 --- /dev/null +++ b/runtime/hydradx/src/migrations/circuit_breaker.rs @@ -0,0 +1,139 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +use codec::Encode; +use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; +use pallet_circuit_breaker::{ + pallet, LiquidityAddLimitPerAsset, LiquidityRemoveLimitPerAsset, TradeVolumeLimitPerAsset, +}; +use sp_core::Get; +use sp_runtime::Saturating; +use sp_std::{marker::PhantomData, vec::Vec}; + +const MIGRATION_DONE_KEY: &[u8] = b"HydrationCircuitBreaker2sLimitMigrationDone"; +const MAX_LIMIT_ENTRIES_PER_MAP: u64 = 10; + +pub struct MigrateCircuitBreakerLimitsTo2sBlocks(PhantomData); + +impl MigrateCircuitBreakerLimitsTo2sBlocks { + fn is_done() -> bool { + sp_io::storage::get(MIGRATION_DONE_KEY).is_some() + } + + fn mark_done() { + sp_io::storage::set(MIGRATION_DONE_KEY, &true.encode()); + } + + fn scale_limit((numerator, denominator): (u32, u32)) -> (u32, u32) { + (numerator.saturating_div(3).max(1), denominator) + } + + fn is_within_limit(len: u64, map: &str) -> bool { + if len <= MAX_LIMIT_ENTRIES_PER_MAP { + true + } else { + log::error!( + "MigrateCircuitBreakerLimitsTo2sBlocks skipped because {:?} has {:?} entries, cap: {:?}", + map, + len, + MAX_LIMIT_ENTRIES_PER_MAP, + ); + false + } + } +} + +impl OnRuntimeUpgrade for MigrateCircuitBreakerLimitsTo2sBlocks { + fn on_runtime_upgrade() -> Weight { + if Self::is_done() { + log::warn!("MigrateCircuitBreakerLimitsTo2sBlocks already executed"); + return T::DbWeight::get().reads(1); + } + + let trade_limits: Vec<_> = TradeVolumeLimitPerAsset::::iter().collect(); + let add_limits: Vec<_> = LiquidityAddLimitPerAsset::::iter().collect(); + let remove_limits: Vec<_> = LiquidityRemoveLimitPerAsset::::iter().collect(); + let trade_limits_len = trade_limits.len(); + let add_limits_len = add_limits.len(); + let remove_limits_len = remove_limits.len(); + + let reads = 1u64 + .saturating_add(trade_limits_len as u64) + .saturating_add(add_limits_len as u64) + .saturating_add(remove_limits_len as u64); + + log::info!( + "MigrateCircuitBreakerLimitsTo2sBlocks found entries - trade: {:?}, add liquidity: {:?}, remove liquidity: {:?}, cap per map: {:?}", + trade_limits_len, + add_limits_len, + remove_limits_len, + MAX_LIMIT_ENTRIES_PER_MAP, + ); + + if !Self::is_within_limit(trade_limits_len as u64, "TradeVolumeLimitPerAsset") + || !Self::is_within_limit(add_limits_len as u64, "LiquidityAddLimitPerAsset") + || !Self::is_within_limit(remove_limits_len as u64, "LiquidityRemoveLimitPerAsset") + { + return T::DbWeight::get().reads(reads); + } + + let mut writes = 0u64; + + for (asset, limit) in trade_limits { + TradeVolumeLimitPerAsset::::insert(asset, Self::scale_limit(limit)); + writes.saturating_inc(); + } + + for (asset, limit) in add_limits { + LiquidityAddLimitPerAsset::::insert(asset, limit.map(Self::scale_limit)); + writes.saturating_inc(); + } + + for (asset, limit) in remove_limits { + LiquidityRemoveLimitPerAsset::::insert(asset, limit.map(Self::scale_limit)); + writes.saturating_inc(); + } + + Self::mark_done(); + writes.saturating_inc(); + + log::info!( + "MigrateCircuitBreakerLimitsTo2sBlocks migrated trade: {:?}, add liquidity: {:?}, remove liquidity: {:?}", + trade_limits_len, + add_limits_len, + remove_limits_len, + ); + + T::DbWeight::get().reads_writes(reads, writes) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{AssetId, Runtime}; + + #[test] + fn migrate_circuit_breaker_limits_to_2s_blocks_works() { + let mut ext = sp_io::TestExternalities::new_empty(); + + ext.execute_with(|| { + let asset: AssetId = 1; + + TradeVolumeLimitPerAsset::::insert(asset, (5_000, 10_000)); + LiquidityAddLimitPerAsset::::insert(asset, Some((500, 10_000))); + LiquidityRemoveLimitPerAsset::::insert(asset, None::<(u32, u32)>); + + MigrateCircuitBreakerLimitsTo2sBlocks::::on_runtime_upgrade(); + + assert_eq!(TradeVolumeLimitPerAsset::::get(asset), (1_666, 10_000)); + assert_eq!(LiquidityAddLimitPerAsset::::get(asset), Some((166, 10_000))); + assert_eq!(LiquidityRemoveLimitPerAsset::::get(asset), None::<(u32, u32)>); + + MigrateCircuitBreakerLimitsTo2sBlocks::::on_runtime_upgrade(); + + assert_eq!(TradeVolumeLimitPerAsset::::get(asset), (1_666, 10_000)); + assert_eq!(LiquidityAddLimitPerAsset::::get(asset), Some((166, 10_000))); + }); + } +} diff --git a/runtime/hydradx/src/migrations/conviction_voting.rs b/runtime/hydradx/src/migrations/conviction_voting.rs new file mode 100644 index 0000000000..d0ab831a59 --- /dev/null +++ b/runtime/hydradx/src/migrations/conviction_voting.rs @@ -0,0 +1,135 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +use codec::Encode; +use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; +use pallet_conviction_voting::{pallet, BlockNumberFor, PriorLock, VotingFor}; +use sp_core::Get; +use sp_runtime::traits::{BlockNumberProvider, Saturating}; +use sp_std::marker::PhantomData; + +const MIGRATION_DONE_KEY: &[u8] = b"HydrationConvictionVoting2sBlockMigrationDone"; +const MAX_VOTING_FOR_RECORDS: u64 = 1_000; + +// This migration preserves existing conviction-voting prior-lock wall-clock unlock times when +// moving from 6s to 2s blocks. Only `VotingFor` stores block numbers; `ClassLocksFor` stores +// classes and balances only. +// +// The migration uses a raw storage marker to prevent accidental double execution. Make sure it is +// removed from the Runtime Executive after it has been run. +pub struct MigrateConvictionVotingTo2sBlocks, I: 'static = ()>(PhantomData<(T, I)>); + +impl, I: 'static> MigrateConvictionVotingTo2sBlocks { + fn is_done() -> bool { + sp_io::storage::get(MIGRATION_DONE_KEY).is_some() + } + + fn mark_done() { + sp_io::storage::set(MIGRATION_DONE_KEY, &true.encode()); + } + + fn scale_future_block(block: BlockNumberFor, current_block: BlockNumberFor) -> BlockNumberFor { + if block <= current_block { + block + } else { + current_block.saturating_add(block.saturating_sub(current_block).saturating_mul(3u32.into())) + } + } +} + +impl, I: 'static> OnRuntimeUpgrade for MigrateConvictionVotingTo2sBlocks { + fn on_runtime_upgrade() -> Weight { + if Self::is_done() { + log::warn!("MigrateConvictionVotingTo2sBlocks already executed"); + return T::DbWeight::get().reads(1); + } + + let current_block = >::BlockNumberProvider::current_block_number(); + let voting_for_records = VotingFor::::iter().count() as u64; + let mut reads = 1u64.saturating_add(voting_for_records); + let mut writes = 0u64; + let mut migrated = 0u64; + + log::info!( + "MigrateConvictionVotingTo2sBlocks found VotingFor records: {:?}, cap: {:?}", + voting_for_records, + MAX_VOTING_FOR_RECORDS, + ); + + if voting_for_records > MAX_VOTING_FOR_RECORDS { + log::error!( + "MigrateConvictionVotingTo2sBlocks skipped because VotingFor has {:?} records, cap: {:?}", + voting_for_records, + MAX_VOTING_FOR_RECORDS, + ); + return T::DbWeight::get().reads(reads); + } + + for (who, class, mut voting) in VotingFor::::iter() { + reads.saturating_inc(); + let prior: &mut PriorLock, _> = voting.as_mut(); + let old_until = prior.0; + let new_until = Self::scale_future_block(old_until, current_block); + + if old_until != new_until { + prior.0 = new_until; + VotingFor::::insert(who, class, voting); + writes.saturating_inc(); + migrated.saturating_inc(); + } + } + + Self::mark_done(); + writes.saturating_inc(); + + log::info!("MigrateConvictionVotingTo2sBlocks migrated prior locks: {:?}", migrated); + T::DbWeight::get().reads_writes(reads, writes) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{AccountId, Runtime, System}; + use pallet_conviction_voting::{Casting, Voting}; + + fn prior_until(who: AccountId) -> BlockNumberFor { + match VotingFor::::get(who, 0u16) { + Voting::Casting(Casting { prior, .. }) => prior.0, + Voting::Delegating(delegating) => delegating.prior.0, + } + } + + #[test] + fn migrate_conviction_voting_to_2s_blocks_works() { + let mut ext = sp_io::TestExternalities::new_empty(); + + ext.execute_with(|| { + let who = AccountId::new([1; 32]); + System::set_block_number(100); + VotingFor::::insert( + who.clone(), + 0u16, + Voting::Casting(Casting { + votes: Default::default(), + delegations: Default::default(), + prior: PriorLock(200, 1_000), + }), + ); + + MigrateConvictionVotingTo2sBlocks::::on_runtime_upgrade(); + + let voting = VotingFor::::get(who.clone(), 0u16); + match voting { + Voting::Casting(Casting { prior, .. }) => { + assert_eq!(prior.0, 400); + assert_eq!(prior.1, 1_000); + } + Voting::Delegating(_) => panic!("expected casting voting state"), + } + + MigrateConvictionVotingTo2sBlocks::::on_runtime_upgrade(); + assert_eq!(prior_until(who), 400); + }) + } +} diff --git a/runtime/hydradx/src/migrations/mod.rs b/runtime/hydradx/src/migrations/mod.rs index bce66f6851..11888226e3 100644 --- a/runtime/hydradx/src/migrations/mod.rs +++ b/runtime/hydradx/src/migrations/mod.rs @@ -15,8 +15,23 @@ use crate::Runtime; +pub mod circuit_breaker; +pub mod conviction_voting; +pub mod referenda; +pub mod scheduler; +pub mod stableswap; + // New migrations which need to be cleaned up after every Runtime upgrade -pub type UnreleasedSingleBlockMigrations = (); +pub type UnreleasedSingleBlockMigrations = ( + pallet_staking::migrations::SetTwoSecBlocksSince, + pallet_gigahdx::migrations::SetTwoSecBlocksSince, + pallet_dca::migrations::MultiplySchedulesPeriodBy3, + circuit_breaker::MigrateCircuitBreakerLimitsTo2sBlocks, + stableswap::MigrateStableswapMaxPegUpdateTo2sBlocks, + scheduler::MigrateSchedulerTo2sBlocks, + referenda::MigrateReferendaTo2sBlocks, + conviction_voting::MigrateConvictionVotingTo2sBlocks, +); // These migrations can run on every runtime upgrade pub type PermanentSingleBlockMigrations = pallet_xcm::migration::MigrateToLatestXcmVersion; diff --git a/runtime/hydradx/src/migrations/referenda.rs b/runtime/hydradx/src/migrations/referenda.rs new file mode 100644 index 0000000000..2240609688 --- /dev/null +++ b/runtime/hydradx/src/migrations/referenda.rs @@ -0,0 +1,146 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +use codec::Encode; +use frame_support::{ + traits::{schedule::DispatchTime, OnRuntimeUpgrade}, + weights::Weight, +}; +use pallet_referenda::{pallet, BlockNumberFor, ReferendumInfo, ReferendumInfoFor, ScheduleAddressOf}; +use sp_core::Get; +use sp_runtime::traits::{BlockNumberProvider, Saturating}; +use sp_std::{marker::PhantomData, vec::Vec}; + +const MIGRATION_DONE_KEY: &[u8] = b"HydrationReferenda2sBlockMigrationDone"; +const MAX_ACTIVE_REFERENDA: u64 = 30; + +// Minimal OpenGov active-state migration for the 6s -> 2s block-time change. +// +// `TrackQueue` does not store block numbers in this SDK version; it stores referendum indices and +// votes only. This migration updates only ongoing `ReferendumInfoFor` statuses and keeps their +// wall-clock lifecycle timing roughly unchanged. +pub struct MigrateReferendaTo2sBlocks, I: 'static = ()>(PhantomData<(T, I)>); + +impl, I: 'static> MigrateReferendaTo2sBlocks { + fn is_done() -> bool { + sp_io::storage::get(MIGRATION_DONE_KEY).is_some() + } + + fn mark_done() { + sp_io::storage::set(MIGRATION_DONE_KEY, &true.encode()); + } + + fn scale_future_block(block: BlockNumberFor, current_block: BlockNumberFor) -> BlockNumberFor { + if block <= current_block { + block + } else { + current_block.saturating_add(block.saturating_sub(current_block).saturating_mul(3u32.into())) + } + } + + fn scale_anchor_block(block: BlockNumberFor, current_block: BlockNumberFor) -> BlockNumberFor { + if block <= current_block { + current_block.saturating_sub(current_block.saturating_sub(block).saturating_mul(3u32.into())) + } else { + Self::scale_future_block(block, current_block) + } + } + + fn scale_dispatch_time( + dispatch_time: DispatchTime>, + current_block: BlockNumberFor, + ) -> DispatchTime> { + match dispatch_time { + DispatchTime::At(block) => DispatchTime::At(Self::scale_future_block(block, current_block)), + DispatchTime::After(blocks) => DispatchTime::After(blocks.saturating_mul(3u32.into())), + } + } +} + +impl OnRuntimeUpgrade for MigrateReferendaTo2sBlocks +where + T: pallet::Config, + I: 'static, + ScheduleAddressOf: Into<(BlockNumberFor, u32)>, + (BlockNumberFor, u32): Into>, +{ + fn on_runtime_upgrade() -> Weight { + if Self::is_done() { + log::warn!("MigrateReferendaTo2sBlocks already executed"); + return T::DbWeight::get().reads(1); + } + + let mut reads = 1u64; + let mut ongoing_referenda = Vec::new(); + let mut ongoing_count = 0u64; + + for (index, info) in ReferendumInfoFor::::iter() { + reads.saturating_inc(); + + if let ReferendumInfo::Ongoing(status) = info { + ongoing_count.saturating_inc(); + + if ongoing_count <= MAX_ACTIVE_REFERENDA { + ongoing_referenda.push((index, status)); + } + } + } + + let current_block = >::BlockNumberProvider::current_block_number(); + let checked = reads.saturating_sub(1); + log::info!( + "MigrateReferendaTo2sBlocks found ReferendumInfoFor records: {:?}, ongoing: {:?}, ongoing cap: {:?}", + checked, + ongoing_count, + MAX_ACTIVE_REFERENDA, + ); + + if ongoing_count > MAX_ACTIVE_REFERENDA { + log::error!( + "MigrateReferendaTo2sBlocks skipped because ReferendumInfoFor has {:?} ongoing referenda, cap: {:?}", + ongoing_count, + MAX_ACTIVE_REFERENDA, + ); + return T::DbWeight::get().reads(reads); + } + + let ongoing_len = ongoing_referenda.len(); + let mut migrated = 0u64; + let mut writes = 0u64; + + for (index, mut status) in ongoing_referenda { + status.enactment = Self::scale_dispatch_time(status.enactment, current_block); + status.submitted = Self::scale_anchor_block(status.submitted, current_block); + + if let Some(deciding) = status.deciding.as_mut() { + deciding.since = Self::scale_anchor_block(deciding.since, current_block); + if let Some(confirming) = deciding.confirming.as_mut() { + *confirming = Self::scale_future_block(*confirming, current_block); + } + } + + if let Some((alarm_at, address)) = status.alarm.as_mut() { + *alarm_at = Self::scale_future_block(*alarm_at, current_block); + + let (address_block, address_index) = address.clone().into(); + *address = (Self::scale_future_block(address_block, current_block), address_index).into(); + } + + ReferendumInfoFor::::insert(index, ReferendumInfo::Ongoing(status)); + writes.saturating_inc(); + migrated.saturating_inc(); + } + + Self::mark_done(); + writes.saturating_inc(); + + log::info!( + "MigrateReferendaTo2sBlocks checked referenda records: {:?}, ongoing: {:?}, migrated ongoing: {:?}", + checked, + ongoing_len, + migrated, + ); + + T::DbWeight::get().reads_writes(reads, writes) + } +} diff --git a/runtime/hydradx/src/migrations/scheduler.rs b/runtime/hydradx/src/migrations/scheduler.rs new file mode 100644 index 0000000000..d3e91684f7 --- /dev/null +++ b/runtime/hydradx/src/migrations/scheduler.rs @@ -0,0 +1,172 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +use codec::Encode; +use frame_support::{traits::OnRuntimeUpgrade, weights::Weight, BoundedVec}; +use pallet_scheduler::{pallet, BlockNumberFor, ScheduledOf}; +use sp_core::Get; +use sp_runtime::{traits::BlockNumberProvider, Saturating}; +use sp_std::{marker::PhantomData, vec::Vec}; + +const MIGRATION_DONE_KEY: &[u8] = b"HydrationScheduler2sBlockMigrationDone"; + +// This migration migrates the Scheduler to 2s block times by multiplying by 3 the spread between +// stored scheduler block numbers and the current block, and by multiplying periodic intervals by 3. +// +// The migration uses a raw storage marker to prevent accidental double execution. Make sure it is +// removed from the Runtime Executive after it has been run. +pub struct MigrateSchedulerTo2sBlocks(PhantomData); + +impl MigrateSchedulerTo2sBlocks { + fn is_done() -> bool { + sp_io::storage::get(MIGRATION_DONE_KEY).is_some() + } + + fn mark_done() { + sp_io::storage::set(MIGRATION_DONE_KEY, &true.encode()); + } + + fn scale_block(block: BlockNumberFor, current_block: BlockNumberFor) -> BlockNumberFor { + let old_spread = block.saturating_sub(current_block); + let new_spread = old_spread.saturating_mul(3u32.into()); + current_block.saturating_add(new_spread) + } +} + +impl OnRuntimeUpgrade for MigrateSchedulerTo2sBlocks { + fn on_runtime_upgrade() -> Weight { + if Self::is_done() { + log::warn!("MigrateSchedulerTo2sBlocks already executed"); + return T::DbWeight::get().reads(1); + } + + let current_block = T::BlockNumberProvider::current_block_number(); + let agenda: Vec<( + BlockNumberFor, + BoundedVec>, T::MaxScheduledPerBlock>, + )> = pallet_scheduler::Agenda::::iter().collect(); + let agenda_len = agenda.len() as u64; + + let lookup: Vec<_> = pallet_scheduler::Lookup::::iter().collect(); + let lookup_len = lookup.len() as u64; + + log::info!( + "MigrateSchedulerTo2sBlocks found Agenda entries: {:?}, Agenda cap: 150, Lookup entries: {:?}, Lookup cap: 5", + agenda_len, + lookup_len, + ); + + if agenda_len >= 150 { + log::error!( + "MigrateSchedulerTo2sBlocks skipped because Agenda has {:?} entries, cap: 150", + agenda_len + ); + return T::DbWeight::get().reads_writes(agenda_len.saturating_add(lookup_len).saturating_add(1), 0); + } + + // We expect Lookup to be empty on-chain, but migrate up to 5 entries defensively in case + // any named schedules exist at upgrade time. If there are more, skip only Lookup migration. + let migrate_lookup = lookup_len <= 5; + if !migrate_lookup { + log::error!( + "Skipping Scheduler Lookup migration because more than 5 entries exist, len: {:?}", + lookup_len + ); + } + + for (old_block, mut schedules) in agenda { + for scheduled in schedules.iter_mut().flatten() { + if let Some((period, _remaining)) = scheduled.maybe_periodic.as_mut() { + *period = period.saturating_mul(3u32.into()); + } + } + + let new_block = Self::scale_block(old_block, current_block); + + pallet_scheduler::Agenda::::remove(old_block); + pallet_scheduler::Agenda::::insert(new_block, schedules); + } + + let lookup_writes = if migrate_lookup { lookup_len } else { 0 }; + if migrate_lookup { + for (name, (block, index)) in lookup { + pallet_scheduler::Lookup::::insert(name, (Self::scale_block(block, current_block), index)); + } + } + + Self::mark_done(); + + log::info!( + "MigrateSchedulerTo2sBlocks processed agenda items: {:?}, lookup entries: {:?}, lookup migrated: {:?}", + agenda_len, + lookup_len, + migrate_lookup + ); + T::DbWeight::get().reads_writes( + agenda_len.saturating_add(lookup_len).saturating_add(1), + agenda_len + .saturating_mul(2) + .saturating_add(lookup_writes) + .saturating_add(1), + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{Runtime, RuntimeCall, RuntimeOrigin, Scheduler, System}; + use frame_support::assert_ok; + + #[test] + fn migrate_scheduler_to_2s_blocks_works() { + let mut ext = sp_io::TestExternalities::new_empty(); + + ext.execute_with(|| { + System::set_block_number(0); + + let periodic_call = Box::new(RuntimeCall::System(frame_system::Call::remark_with_event { + remark: vec![1], + })); + let named_call = Box::new(RuntimeCall::System(frame_system::Call::remark_with_event { + remark: vec![2], + })); + let named_id = [7u8; 32]; + + assert_ok!(Scheduler::schedule( + RuntimeOrigin::root(), + 200, + Some((10, 3)), + 3, + periodic_call + )); + assert_ok!(Scheduler::schedule_named( + RuntimeOrigin::root(), + named_id, + 220, + None, + 3, + named_call + )); + assert!(pallet_scheduler::Agenda::::contains_key(200)); + assert!(pallet_scheduler::Agenda::::contains_key(220)); + assert_eq!(pallet_scheduler::Lookup::::get(named_id), Some((220, 0))); + + System::set_block_number(100); + MigrateSchedulerTo2sBlocks::::on_runtime_upgrade(); + + assert!(!pallet_scheduler::Agenda::::contains_key(200)); + assert!(!pallet_scheduler::Agenda::::contains_key(220)); + assert!(pallet_scheduler::Agenda::::contains_key(400)); + assert!(pallet_scheduler::Agenda::::contains_key(460)); + let migrated_agenda = pallet_scheduler::Agenda::::get(400); + let migrated_schedule = migrated_agenda.get(0).and_then(Option::as_ref).unwrap(); + assert_eq!(migrated_schedule.maybe_periodic, Some((30, 2))); + assert_eq!(pallet_scheduler::Lookup::::get(named_id), Some((460, 0))); + + MigrateSchedulerTo2sBlocks::::on_runtime_upgrade(); + assert!(pallet_scheduler::Agenda::::contains_key(400)); + assert!(!pallet_scheduler::Agenda::::contains_key(1000)); + }) + } +} diff --git a/runtime/hydradx/src/migrations/stableswap.rs b/runtime/hydradx/src/migrations/stableswap.rs new file mode 100644 index 0000000000..c7834a4691 --- /dev/null +++ b/runtime/hydradx/src/migrations/stableswap.rs @@ -0,0 +1,140 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +use codec::Encode; +use frame_support::{traits::OnRuntimeUpgrade, weights::Weight}; +use pallet_stableswap::{pallet, PoolPegs}; +use sp_core::Get; +use sp_runtime::{Perbill, Saturating}; +use sp_std::{marker::PhantomData, vec::Vec}; + +const MIGRATION_DONE_KEY: &[u8] = b"HydrationStableswapPegUpdate2sMigrationDone"; +const MAX_POOL_PEG_ENTRIES: u64 = 15; + +// `max_peg_update` is a per-block movement cap. Divide configured non-zero caps by 3 +// to preserve roughly the same wall-clock peg movement when moving from 6s to 2s blocks. +pub struct MigrateStableswapMaxPegUpdateTo2sBlocks(PhantomData); + +impl MigrateStableswapMaxPegUpdateTo2sBlocks { + fn is_done() -> bool { + sp_io::storage::get(MIGRATION_DONE_KEY).is_some() + } + + fn mark_done() { + sp_io::storage::set(MIGRATION_DONE_KEY, &true.encode()); + } + + fn scale_max_peg_update(max_peg_update: Perbill) -> Perbill { + let parts = max_peg_update.deconstruct(); + + if parts == 0 { + Perbill::zero() + } else { + Perbill::from_parts((parts / 3).max(1)) + } + } +} + +impl OnRuntimeUpgrade for MigrateStableswapMaxPegUpdateTo2sBlocks { + fn on_runtime_upgrade() -> Weight { + if Self::is_done() { + log::warn!("MigrateStableswapMaxPegUpdateTo2sBlocks already executed"); + return T::DbWeight::get().reads(1); + } + + let pool_pegs: Vec<_> = PoolPegs::::iter().collect(); + let reads = 1u64.saturating_add(pool_pegs.len() as u64); + let pool_pegs_len = pool_pegs.len(); + + log::info!( + "MigrateStableswapMaxPegUpdateTo2sBlocks found PoolPegs entries: {:?}, cap: {:?}", + pool_pegs_len, + MAX_POOL_PEG_ENTRIES + ); + + if pool_pegs.len() as u64 > MAX_POOL_PEG_ENTRIES { + log::error!( + "MigrateStableswapMaxPegUpdateTo2sBlocks skipped because PoolPegs has {:?} entries, cap: {:?}", + pool_pegs_len, + MAX_POOL_PEG_ENTRIES, + ); + return T::DbWeight::get().reads(reads); + } + + let mut migrated = 0u64; + let mut writes = 0u64; + + for (pool_id, mut peg_info) in pool_pegs { + let old_max_peg_update = peg_info.max_peg_update; + let new_max_peg_update = Self::scale_max_peg_update(old_max_peg_update); + + if old_max_peg_update != new_max_peg_update { + peg_info.max_peg_update = new_max_peg_update; + PoolPegs::::insert(pool_id, peg_info); + writes.saturating_inc(); + migrated.saturating_inc(); + } + } + + Self::mark_done(); + writes.saturating_inc(); + + log::info!( + "MigrateStableswapMaxPegUpdateTo2sBlocks checked pools: {:?}, migrated: {:?}", + pool_pegs_len, + migrated, + ); + + T::DbWeight::get().reads_writes(reads, writes) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{AssetId, Runtime}; + use frame_support::BoundedVec; + use pallet_stableswap::types::{PegSource, PoolPegInfo}; + + fn peg_info(max_peg_update: Perbill) -> PoolPegInfo, AssetId> { + PoolPegInfo { + source: BoundedVec::truncate_from(vec![PegSource::Value((1, 1))]), + updated_at: 1, + max_peg_update, + current: BoundedVec::truncate_from(vec![(1, 1)]), + } + } + + type BlockNumberFor = frame_system::pallet_prelude::BlockNumberFor; + + #[test] + fn migrate_stableswap_max_peg_update_to_2s_blocks_works() { + let mut ext = sp_io::TestExternalities::new_empty(); + + ext.execute_with(|| { + let non_zero_pool: AssetId = 690; + let zero_pool: AssetId = 143; + + PoolPegs::::insert(non_zero_pool, peg_info(Perbill::from_percent(6))); + PoolPegs::::insert(zero_pool, peg_info(Perbill::zero())); + + MigrateStableswapMaxPegUpdateTo2sBlocks::::on_runtime_upgrade(); + + assert_eq!( + PoolPegs::::get(non_zero_pool).unwrap().max_peg_update, + Perbill::from_percent(2) + ); + assert_eq!( + PoolPegs::::get(zero_pool).unwrap().max_peg_update, + Perbill::zero() + ); + + MigrateStableswapMaxPegUpdateTo2sBlocks::::on_runtime_upgrade(); + + assert_eq!( + PoolPegs::::get(non_zero_pool).unwrap().max_peg_update, + Perbill::from_percent(2) + ); + }); + } +} diff --git a/runtime/hydradx/src/system.rs b/runtime/hydradx/src/system.rs index 5879f3a9a4..ef0ff15820 100644 --- a/runtime/hydradx/src/system.rs +++ b/runtime/hydradx/src/system.rs @@ -158,7 +158,8 @@ parameter_types! { .avg_block_initialization(AVERAGE_ON_INITIALIZE_RATIO) .build_or_panic(); pub ExtrinsicBaseWeight: Weight = get_extrinsic_base_weight(); - pub const BlockHashCount: BlockNumber = 2400; + /// Keep about 4 hours of block hashes with 2s blocks. + pub const BlockHashCount: BlockNumber = 7200; /// Maximum length of block. Up to 5MB. pub BlockLength: frame_system::limits::BlockLength = frame_system::limits::BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); @@ -270,7 +271,7 @@ impl pallet_timestamp::Config for Runtime { /// A timestamp: milliseconds since the unix epoch. type Moment = u64; type OnTimestampSet = (); - type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; + type MinimumPeriod = ConstU64<0>; type WeightInfo = weights::pallet_timestamp::HydraWeight; } @@ -661,7 +662,7 @@ parameter_types! { pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(25); /// The adjustment variable of the runtime. Higher values will cause `TargetBlockFullness` to /// change the fees more rapidly. - pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(10, 113); + pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(10, 339); /// Minimum amount of the multiplier. This value cannot be too low. A test case should ensure /// that combined with `AdjustmentVariable`, we can recover from the minimum. pub MinimumMultiplier: Multiplier = Multiplier::saturating_from_rational(1, 1000u128); diff --git a/scripts/assign_cores.js b/scripts/assign_cores.js new file mode 100644 index 0000000000..ca0dc5495c --- /dev/null +++ b/scripts/assign_cores.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +let ApiPromise; +let WsProvider; +let Keyring; +let cryptoWaitReady; + +function loadPackage(packageName) { + try { + return require(packageName); + } catch (error) { + const paths = (process.env.PATH || "").split(require("path").delimiter); + for (const entry of paths) { + if (!entry.endsWith(`${require("path").sep}node_modules${require("path").sep}.bin`)) { + continue; + } + + const nodeModules = require("path").dirname(entry); + try { + return require(require("path").join(nodeModules, packageName)); + } catch (_) { + // Try the next npm exec temp directory. + } + } + + throw error; + } +} + +try { + ({ ApiPromise, WsProvider, Keyring } = loadPackage("@polkadot/api")); + ({ cryptoWaitReady } = loadPackage("@polkadot/util-crypto")); +} catch (error) { + console.error( + "Missing JS deps. Run without installing into the repo via:\n" + + "npm exec --yes --package=@polkadot/api --package=@polkadot/util-crypto -- node scripts/assign_cores.js", + ); + process.exit(1); +} + +function parseArgs(argv) { + const defaults = { + ws: "ws://127.0.0.1:9945", + suri: "//Alice", + paraId: 2032, + cores: [0, 1, 2], + begin: 0, + finalized: true, + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--ws") { + defaults.ws = argv[++i]; + } else if (arg === "--suri") { + defaults.suri = argv[++i]; + } else if (arg === "--para") { + defaults.paraId = Number(argv[++i]); + } else if (arg === "--cores") { + defaults.cores = argv[++i] + .split(",") + .filter(Boolean) + .map((value) => Number(value.trim())); + } else if (arg === "--begin") { + defaults.begin = Number(argv[++i]); + } else if (arg === "--in-block") { + defaults.finalized = false; + } else if (arg === "--help" || arg === "-h") { + printHelp(); + process.exit(0); + } else { + throw new Error(`Unknown argument: ${arg}`); + } + } + + if (!defaults.cores.length || defaults.cores.some((core) => Number.isNaN(core))) { + throw new Error("Expected --cores to contain a comma-separated list of integers"); + } + + if (Number.isNaN(defaults.paraId)) { + throw new Error("Expected --para to be an integer"); + } + + if (Number.isNaN(defaults.begin)) { + throw new Error("Expected --begin to be an integer"); + } + + return defaults; +} + +function printHelp() { + console.log(`Assign relay-chain cores to a parachain on a local Zombienet relay node. + +Usage: + node scripts/assign_cores.js [options] + +Options: + --ws Relay-chain websocket endpoint (default: ws://127.0.0.1:9945) + --suri Signing account SURI (default: //Alice) + --para Parachain id to assign cores to (default: 2032) + --cores Comma-separated core indexes (default: 0,1,2) + --begin Relay block number to start assignment from (default: 0) + --in-block Exit once included in a block instead of waiting for finalization + --help, -h Show this message +`); +} + +async function main() { + const { ws, suri, paraId, cores, begin, finalized } = parseArgs(process.argv.slice(2)); + + await cryptoWaitReady(); + + const provider = new WsProvider(ws); + const api = await ApiPromise.create({ provider }); + const keyring = new Keyring({ type: "sr25519" }); + const signer = keyring.addFromUri(suri); + + const calls = cores.map((core) => + api.tx.coretime.assignCore( + core, + begin, + [[{ Task: paraId }, 57600]], + null, + ), + ); + + const tx = api.tx.sudo.sudo(api.tx.utility.batch(calls)); + + console.log( + `Submitting assign_core for para ${paraId} on cores [${cores.join(", ")}] via ${ws} as ${signer.address}`, + ); + + await new Promise(async (resolve, reject) => { + let unsub = null; + + try { + unsub = await tx.signAndSend(signer, ({ status, dispatchError, events }) => { + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + reject( + new Error( + `${decoded.section}.${decoded.name}: ${decoded.docs.join(" ")}`, + ), + ); + } else { + reject(new Error(dispatchError.toString())); + } + return; + } + + if (status.isInBlock) { + console.log(`Included at ${status.asInBlock.toHex()}`); + if (!finalized) { + if (unsub) { + unsub(); + } + resolve(); + } + } + + if (status.isFinalized) { + console.log(`Finalized at ${status.asFinalized.toHex()}`); + for (const { event } of events) { + console.log(`Event: ${event.section}.${event.method}`); + } + if (unsub) { + unsub(); + } + resolve(); + } + }); + } catch (error) { + if (unsub) { + unsub(); + } + reject(error); + } + }); + + await api.disconnect(); +} + +main().catch((error) => { + console.error(error.message || error); + process.exit(1); +}); diff --git a/scripts/circuit-breaker-dashboard/README.md b/scripts/circuit-breaker-dashboard/README.md index d5604c633d..1d7451bcc2 100644 --- a/scripts/circuit-breaker-dashboard/README.md +++ b/scripts/circuit-breaker-dashboard/README.md @@ -44,6 +44,6 @@ mcp__swarmpit-lark__create_stack(name="circuit-breaker", compose="Per-Asset Deposit Lockdown setTimeout(r, ms)); @@ -169,7 +170,7 @@ async function main() { // Step 5: Wait for approval console.log(" [5/5] Waiting for referendum to pass..."); for (let i = 0; i < 60; i++) { - await sleep(6000); + await sleep(BLOCK_TIME_MS); const info = await api.query.referenda.referendumInfoFor(refIndex); const infoJson = info.toJSON(); @@ -193,7 +194,7 @@ async function main() { // Verify console.log("\n Verifying EVM setup..."); - await sleep(6000); + await sleep(BLOCK_TIME_MS); const newBalance = await api.rpc.eth .getBalance("0xC19A2970A13ac19898c47d59Cbd0278D428EBC7c") diff --git a/scripts/opengov-force-remove-votes/.gitignore b/scripts/opengov-force-remove-votes/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/scripts/opengov-force-remove-votes/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/scripts/opengov-force-remove-votes/README.md b/scripts/opengov-force-remove-votes/README.md new file mode 100644 index 0000000000..9f71e1bf65 --- /dev/null +++ b/scripts/opengov-force-remove-votes/README.md @@ -0,0 +1,42 @@ +# OpenGov Force Remove Votes + +Builds `convictionVoting.forceRemoveVote(target, class, refIndex)` calls for +finished referenda found in `ConvictionVoting::VotingFor`. + +The signer must satisfy the runtime `VoteRemovalOrigin` configured for +`pallet-conviction-voting` (on Hydration, a Technical Committee member after the +SDK patch that enables this call). + +## Usage + +```sh +cd scripts/opengov-force-remove-votes +npm install + +RPC_SERVER=wss://rpc.hydradx.cloud npm run dry-run + +RPC_SERVER=wss://rpc.hydradx.cloud npm run submit +``` + +Submit mode prompts for `ACCOUNT_SECRET` interactively, so the seed is not +written to shell history. + +If the script reports unsupported signed extensions, refresh dependencies: + +```sh +rm -rf node_modules package-lock.json +npm install +``` + +The script logs `ConvictionVoting::VotingFor` record and vote counts before +building calls, and again after submitted batches complete. + +Optional environment variables: + +- `RPC_SERVER` - chain RPC, defaults to `ws://127.0.0.1:9944` +- `BATCH_SIZE` - calls per `utility.batch`, defaults to `20` +- `LIMIT` - maximum force-remove calls to build, useful for staged runs +- `TX_TIMEOUT_MS` - per-batch inclusion timeout before skipping, defaults to `60000` + +Batches that do not reach inclusion before `TX_TIMEOUT_MS` are skipped. Re-run +the script later to rescan chain state and pick up any skipped votes. diff --git a/scripts/opengov-force-remove-votes/index.js b/scripts/opengov-force-remove-votes/index.js new file mode 100644 index 0000000000..a04bb201c4 --- /dev/null +++ b/scripts/opengov-force-remove-votes/index.js @@ -0,0 +1,407 @@ +const { ApiPromise, Keyring, WsProvider } = require("@polkadot/api"); +const { findUnknownExtensions } = require("@polkadot/types/extrinsic/signedExtensions"); +const { cryptoWaitReady, encodeAddress } = require("@polkadot/util-crypto"); +const readline = require("readline"); + +const RPC = process.env.RPC_SERVER || "ws://127.0.0.1:9944"; +const BATCH_SIZE = Number(process.env.BATCH_SIZE || 20); +const LIMIT = process.env.LIMIT ? Number(process.env.LIMIT) : undefined; +const TX_TIMEOUT_MS = Number(process.env.TX_TIMEOUT_MS || 60_000); +const SUBMIT = process.argv[2] === "submit"; +const ALLOWED_NO_EFFECT_EXTENSIONS = new Set(["ValidateClaim", "StorageWeightReclaim"]); + +const hdxAddress = (pubKey) => encodeAddress(pubKey, 63); +const chunkify = (items, size) => + Array(Math.ceil(items.length / size)) + .fill() + .map((_, i) => items.slice(i * size, i * size + size)); + +function assertConfig() { + if (!Number.isInteger(BATCH_SIZE) || BATCH_SIZE <= 0) { + throw new Error(`BATCH_SIZE must be a positive integer, got: ${process.env.BATCH_SIZE}`); + } + + if (LIMIT !== undefined && (!Number.isInteger(LIMIT) || LIMIT <= 0)) { + throw new Error(`LIMIT must be a positive integer, got: ${process.env.LIMIT}`); + } + + if (!Number.isInteger(TX_TIMEOUT_MS) || TX_TIMEOUT_MS <= 0) { + throw new Error(`TX_TIMEOUT_MS must be a positive integer, got: ${process.env.TX_TIMEOUT_MS}`); + } +} + +async function promptSecret(prompt) { + if (!process.stdin.isTTY) { + throw new Error("submit mode requires an interactive TTY to prompt for ACCOUNT_SECRET"); + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + const stdin = process.stdin; + const onData = (char) => { + char = char.toString(); + switch (char) { + case "\n": + case "\r": + case "\u0004": + stdin.removeListener("data", onData); + break; + default: + readline.moveCursor(process.stdout, -rl.line.length, 0); + readline.clearLine(process.stdout, 1); + process.stdout.write("*".repeat(rl.line.length)); + break; + } + }; + + stdin.on("data", onData); + rl.question(prompt, (value) => { + rl.close(); + process.stdout.write("\n"); + resolve(value); + }); + }); +} + +function isOngoingReferendum(info) { + if (info.isNone) { + return false; + } + + const unwrapped = info.unwrap(); + return unwrapped.isOngoing; +} + +function castingVotes(voting) { + if (!voting.isCasting) { + return []; + } + + return voting.asCasting.votes.map(([refIndex]) => refIndex.toNumber()); +} + +async function votingStats(api) { + const entries = await api.query.convictionVoting.votingFor.entries(); + let castingRecords = 0; + let delegatingRecords = 0; + let votes = 0; + + for (const [, voting] of entries) { + if (voting.isCasting) { + castingRecords += 1; + votes += castingVotes(voting).length; + } else if (voting.isDelegating) { + delegatingRecords += 1; + } + } + + return { + votingForRecords: entries.length, + castingRecords, + delegatingRecords, + votes, + }; +} + +function logVotingStats(label, stats) { + console.log( + `${label} VotingFor stats - records: ${stats.votingForRecords}, casting: ${stats.castingRecords}, delegating: ${stats.delegatingRecords}, votes: ${stats.votes}` + ); +} + +function assertSignedExtensionsSupported(api) { + const unknown = findUnknownExtensions(api.registry.signedExtensions); + const unsupported = unknown.filter((name) => !ALLOWED_NO_EFFECT_EXTENSIONS.has(name)); + + if (unknown.length > 0) { + console.log(`unknown no-effect signed extensions allowed: ${unknown.join(", ")}`); + } + + if (unsupported.length === 0) { + return; + } + + throw new Error( + [ + `Unsupported signed extensions: ${unsupported.join(", ")}`, + "Your @polkadot/api version cannot safely sign transactions for this runtime.", + "Update dependencies before submitting:", + " rm -rf node_modules package-lock.json", + " npm install", + ].join("\n") + ); +} + +async function submitAndWait(api, signer, tx, nonce) { + return new Promise((resolve, reject) => { + let unsub; + let done = false; + + const finish = (fn, value) => { + if (done) { + return; + } + + done = true; + if (unsub) { + unsub(); + } + fn(value); + }; + + tx.signAndSend(signer, { nonce }, (receipt) => { + if (receipt.status.isInBlock || receipt.status.isFinalized) { + let failed = false; + let batchItemFailures = 0; + receipt.events.forEach(({ event }) => { + if (api.events.system.ExtrinsicFailed.is(event)) { + failed = true; + const [dispatchError] = event.data; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + console.error( + `extrinsic failed: ${decoded.section}.${decoded.name}: ${decoded.docs.join(" ")}` + ); + } else { + console.error(`extrinsic failed: ${dispatchError.toString()}`); + } + } + + if (api.events.utility?.BatchInterrupted?.is(event)) { + batchItemFailures += 1; + const [index, dispatchError] = event.data; + console.error(`batch interrupted at item ${index.toString()}: ${dispatchError.toString()}`); + } + + if (api.events.utility?.ItemFailed?.is(event)) { + batchItemFailures += 1; + const [dispatchError] = event.data; + console.error(`batch item failed: ${dispatchError.toString()}`); + } + }); + + if (failed) { + finish(reject, new Error(`extrinsic ${tx.hash.toHex()} failed`)); + } else { + finish(resolve, { receipt, batchItemFailures }); + } + } + }) + .then((unsubscribe) => { + unsub = unsubscribe; + }) + .catch((error) => finish(reject, error)); + }); +} + +async function submitAndWaitWithTimeout(api, signer, tx, nonce) { + let timeoutId; + const timeout = new Promise((_, reject) => { + timeoutId = setTimeout( + () => reject(new Error(`transaction ${tx.hash.toHex()} was not included within ${TX_TIMEOUT_MS}ms`)), + TX_TIMEOUT_MS + ); + }); + + try { + return await Promise.race([submitAndWait(api, signer, tx, nonce), timeout]); + } finally { + clearTimeout(timeoutId); + } +} + +async function nextAccountNonce(api, address) { + return api.rpc.system.accountNextIndex(address).then((n) => n.toNumber()); +} + +async function submitBatch(api, signer, batch, batchNumber, totalBatches, nonce) { + const txHash = batch.tx.hash.toHex(); + + console.log(`submitting batch ${batchNumber}/${totalBatches} (${batch.calls.length} calls)`); + console.log(`batch ${batchNumber}/${totalBatches} tx hash: ${txHash}`); + + try { + const result = await submitAndWaitWithTimeout(api, signer, batch.tx, nonce); + return { ...result, included: true, nonce: nonce + 1 }; + } catch (error) { + const currentNonce = await nextAccountNonce(api, signer.address); + if (currentNonce > nonce) { + console.warn( + `batch ${batchNumber}/${totalBatches}: nonce advanced from ${nonce} to ${currentNonce}; treating batch as included` + ); + return { batchItemFailures: "unknown", included: true, nonce: currentNonce }; + } + + console.warn( + `batch ${batchNumber}/${totalBatches}: ${error.message || error}; nonce is still ${currentNonce}, skipping this batch` + ); + console.warn(`skipped batch first call: ${JSON.stringify(batch.calls[0], (key, value) => (key === "tx" ? undefined : value))}`); + return { batchItemFailures: "skipped", included: false, nonce: currentNonce }; + } +} + +async function buildForceRemoveCalls(api) { + const entries = await api.query.convictionVoting.votingFor.entries(); + + const referendumStatusCache = new Map(); + const calls = []; + let castingRecords = 0; + let delegatingRecords = 0; + let totalVotes = 0; + let ongoingVotes = 0; + let missingOrFinishedVotes = 0; + + for (const [key, voting] of entries) { + const [target, classId] = key.args; + + if (voting.isDelegating) { + delegatingRecords += 1; + continue; + } + + if (!voting.isCasting) { + continue; + } + + castingRecords += 1; + const votes = castingVotes(voting); + totalVotes += votes.length; + + for (const refIndex of votes) { + let ongoing = referendumStatusCache.get(refIndex); + if (ongoing === undefined) { + const info = await api.query.referenda.referendumInfoFor(refIndex); + ongoing = isOngoingReferendum(info); + referendumStatusCache.set(refIndex, ongoing); + } + + if (ongoing) { + ongoingVotes += 1; + continue; + } + + missingOrFinishedVotes += 1; + const targetAddress = target.toString(); + calls.push({ + target: targetAddress, + classId: classId.toString(), + refIndex, + tx: api.tx.convictionVoting.forceRemoveVote(targetAddress, classId, refIndex), + }); + + if (LIMIT !== undefined && calls.length >= LIMIT) { + break; + } + } + + if (LIMIT !== undefined && calls.length >= LIMIT) { + break; + } + } + + console.log(`casting records: ${castingRecords}`); + console.log(`delegating records: ${delegatingRecords}`); + console.log(`votes in casting records scanned: ${totalVotes}`); + console.log(`ongoing votes skipped: ${ongoingVotes}`); + console.log(`finished/missing votes selected: ${missingOrFinishedVotes}`); + console.log(`forceRemoveVote calls built: ${calls.length}`); + + return calls; +} + +async function main() { + assertConfig(); + await cryptoWaitReady(); + + const provider = new WsProvider(RPC); + const api = await ApiPromise.create({ provider }); + + const [chain, nodeVersion] = await Promise.all([ + api.rpc.system.chain(), + api.rpc.system.version(), + ]); + + console.log(`connected to ${RPC} (${chain} ${nodeVersion})`); + console.log(`mode: ${SUBMIT ? "submit" : "dry-run"}`); + console.log(`tx timeout: ${TX_TIMEOUT_MS}ms`); + + assertSignedExtensionsSupported(api); + + if (!api.tx.convictionVoting.forceRemoveVote) { + throw new Error("convictionVoting.forceRemoveVote is not available in chain metadata"); + } + + const beforeStats = await votingStats(api); + logVotingStats("before", beforeStats); + + const calls = await buildForceRemoveCalls(api); + if (calls.length === 0) { + console.log("nothing to submit"); + await api.disconnect(); + return; + } + + let signer; + if (SUBMIT) { + const accountSecret = await promptSecret("ACCOUNT_SECRET: "); + const keyring = new Keyring({ type: "sr25519" }); + signer = keyring.addFromUri(accountSecret); + console.log(`active account: ${hdxAddress(signer.addressRaw)}`); + } + + const batches = chunkify(calls, BATCH_SIZE).map((chunk) => ({ + calls: chunk, + tx: api.tx.utility.batch(chunk.map(({ tx }) => tx)), + })); + + console.log(`batch size: ${BATCH_SIZE}`); + console.log(`batches: ${batches.length}`); + console.log("first call:"); + console.log(JSON.stringify(calls[0], (key, value) => (key === "tx" ? undefined : value), 2)); + console.log("encoded first batch:"); + console.log(batches[0].tx.toHex()); + + if (!SUBMIT) { + console.log("dry-run only; pass `submit` to broadcast"); + await api.disconnect(); + return; + } + + let skippedBatches = 0; + for (let i = 0; i < batches.length; i += 1) { + const batch = batches[i]; + const nonce = await nextAccountNonce(api, signer.address); + const { batchItemFailures, included } = await submitBatch( + api, + signer, + batch, + i + 1, + batches.length, + nonce + ); + + if (included) { + console.log(`batch ${i + 1}/${batches.length} included; item failures: ${batchItemFailures}`); + } else { + skippedBatches += 1; + console.log(`batch ${i + 1}/${batches.length} skipped`); + } + } + + const afterStats = await votingStats(api); + logVotingStats("after", afterStats); + console.log(`VotingFor record delta: ${beforeStats.votingForRecords - afterStats.votingForRecords}`); + console.log(`vote delta: ${beforeStats.votes - afterStats.votes}`); + console.log(`skipped batches: ${skippedBatches}`); + + await api.disconnect(); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/opengov-force-remove-votes/package-lock.json b/scripts/opengov-force-remove-votes/package-lock.json new file mode 100644 index 0000000000..39e8f116c4 --- /dev/null +++ b/scripts/opengov-force-remove-votes/package-lock.json @@ -0,0 +1,940 @@ +{ + "name": "opengov-force-remove-votes", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opengov-force-remove-votes", + "version": "1.0.0", + "dependencies": { + "@polkadot/api": "latest", + "@polkadot/util-crypto": "latest" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz", + "integrity": "sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.1.0.tgz", + "integrity": "sha512-8GSFE5+EF73MCuLQm8tjrbCqlgclcHBSRaswvXziJ0ZW7iw3UEMsKkkKvELayWyBuOPa2T5i1nj6gFOeIsqvrg==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot-api/metadata-builders": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.3.2.tgz", + "integrity": "sha512-TKpfoT6vTb+513KDzMBTfCb/ORdgRnsS3TDFpOhAhZ08ikvK+hjHMt5plPiAX/OWkm1Wc9I3+K6W0hX5Ab7MVg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/substrate-bindings": "0.6.0", + "@polkadot-api/utils": "0.1.0" + } + }, + "node_modules/@polkadot-api/observable-client": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.3.2.tgz", + "integrity": "sha512-HGgqWgEutVyOBXoGOPp4+IAq6CNdK/3MfQJmhCJb8YaJiaK4W6aRGrdQuQSTPHfERHCARt9BrOmEvTXAT257Ug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/metadata-builders": "0.3.2", + "@polkadot-api/substrate-bindings": "0.6.0", + "@polkadot-api/utils": "0.1.0" + }, + "peerDependencies": { + "@polkadot-api/substrate-client": "0.1.4", + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/substrate-bindings": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.6.0.tgz", + "integrity": "sha512-lGuhE74NA1/PqdN7fKFdE5C1gNYX357j1tWzdlPXI0kQ7h3kN0zfxNOpPUN7dIrPcOFZ6C0tRRVrBylXkI6xPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.3.1", + "@polkadot-api/utils": "0.1.0", + "@scure/base": "^1.1.1", + "scale-ts": "^1.6.0" + } + }, + "node_modules/@polkadot-api/substrate-client": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.1.4.tgz", + "integrity": "sha512-MljrPobN0ZWTpn++da9vOvt+Ex+NlqTlr/XT7zi9sqPtDJiQcYl+d29hFAgpaeTqbeQKZwz3WDE9xcEfLE8c5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/json-rpc-provider": "0.0.1", + "@polkadot-api/utils": "0.1.0" + } + }, + "node_modules/@polkadot-api/utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.1.0.tgz", + "integrity": "sha512-MXzWZeuGxKizPx2Xf/47wx9sr/uxKw39bVJUptTJdsaQn/TGq+z310mHzf1RCGvC1diHM8f593KrnDgc9oNbJA==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot/api": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-16.5.6.tgz", + "integrity": "sha512-5h/X3pY8WpqGk4XTaiIUjKD6Pnk8k4bJ6EIwPKLP8/kfFWKSOenpN6ggZxANr+Qj+RgXrp4TxJVcuhXSiBh9Sg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-augment": "16.5.6", + "@polkadot/api-base": "16.5.6", + "@polkadot/api-derive": "16.5.6", + "@polkadot/keyring": "^14.0.3", + "@polkadot/rpc-augment": "16.5.6", + "@polkadot/rpc-core": "16.5.6", + "@polkadot/rpc-provider": "16.5.6", + "@polkadot/types": "16.5.6", + "@polkadot/types-augment": "16.5.6", + "@polkadot/types-codec": "16.5.6", + "@polkadot/types-create": "16.5.6", + "@polkadot/types-known": "16.5.6", + "@polkadot/util": "^14.0.3", + "@polkadot/util-crypto": "^14.0.3", + "eventemitter3": "^5.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-augment": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-16.5.6.tgz", + "integrity": "sha512-bunJF1c3nIuDtU6iwa+reTt9U47Y8iOC8Gw7PfANlZmLJmO/XVXnWc3JJLM+g9ESDn2raHJELeWBFVOXQrbtUw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-base": "16.5.6", + "@polkadot/rpc-augment": "16.5.6", + "@polkadot/types": "16.5.6", + "@polkadot/types-augment": "16.5.6", + "@polkadot/types-codec": "16.5.6", + "@polkadot/util": "^14.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-base": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-16.5.6.tgz", + "integrity": "sha512-eBLIv86ZZY4t5OrobVoGC+QXbErOGlBpI2rJI5OMvTNPoVvtEoI++u+wwRScjkOZaUhXyQikd+0Uv71qr3xnsA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "16.5.6", + "@polkadot/types": "16.5.6", + "@polkadot/util": "^14.0.3", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-derive": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-16.5.6.tgz", + "integrity": "sha512-cHdvPvhYFch18uPTcuOZJ8VceOfercod2fi4xCnHJAmattzlgj9qCgnOoxdmBS9GZ403ZyRHOjBuUwZy/IsUWQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "16.5.6", + "@polkadot/api-augment": "16.5.6", + "@polkadot/api-base": "16.5.6", + "@polkadot/rpc-core": "16.5.6", + "@polkadot/types": "16.5.6", + "@polkadot/types-codec": "16.5.6", + "@polkadot/util": "^14.0.3", + "@polkadot/util-crypto": "^14.0.3", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/keyring": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-14.0.3.tgz", + "integrity": "sha512-ozp1dQwaHCjgX/fpTTORmHjxdUNQnyiTVJszpzUaUpvtH/IGZhSU/mSHXMqNETS/g57vQa7NatIDcWfyR9abyA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "14.0.3", + "@polkadot/util-crypto": "14.0.3", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.3", + "@polkadot/util-crypto": "14.0.3" + } + }, + "node_modules/@polkadot/networks": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-14.0.3.tgz", + "integrity": "sha512-/VqTLUDn+Wm8S2L/yaGFddo3oW4vRYav0Rg4pLg/semMZLaN8PJ6h927ucn9JyWdH82QfZfyiIPORt0ZF3isyw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "14.0.3", + "@substrate/ss58-registry": "^1.51.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-augment": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-16.5.6.tgz", + "integrity": "sha512-vlrNvl2VtU09jZV/AvH7jBb/cNUO+dWu8Xj9pId5ctSUnZHm8o8wRk9ekyieKP57OUoKMd8+VScwMKd624SxTw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "16.5.6", + "@polkadot/types": "16.5.6", + "@polkadot/types-codec": "16.5.6", + "@polkadot/util": "^14.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-core": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-16.5.6.tgz", + "integrity": "sha512-l6od++WlvKH4mw5mtsIh2AhiBs3H+TtdOoUHVLCx/R9il7+gl+arltzZ8vBuffyh/O+uQ36lI8yUoD1g4gi1tA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-augment": "16.5.6", + "@polkadot/rpc-provider": "16.5.6", + "@polkadot/types": "16.5.6", + "@polkadot/util": "^14.0.3", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-provider": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-16.5.6.tgz", + "integrity": "sha512-46sHIjKYr4aSzBCfbyqtCwuP8MMJ3jOp0xx9eggOGbKyP8Z0j0Cp+1nNkZUYzehcdGjjrmCxCbQp17wc6cj4zA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^14.0.3", + "@polkadot/types": "16.5.6", + "@polkadot/types-support": "16.5.6", + "@polkadot/util": "^14.0.3", + "@polkadot/util-crypto": "^14.0.3", + "@polkadot/x-fetch": "^14.0.3", + "@polkadot/x-global": "^14.0.3", + "@polkadot/x-ws": "^14.0.3", + "eventemitter3": "^5.0.1", + "mock-socket": "^9.3.1", + "nock": "^13.5.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@substrate/connect": "0.8.11" + } + }, + "node_modules/@polkadot/types": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-16.5.6.tgz", + "integrity": "sha512-X/sfMHJS4RkRhnsc4CQqzUy7BM/s2y71TrBFHPYAjs2q/rbZ/BwvBk70SrUiSa0+iRRn3RewbBZm+AB8CbkdKw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^14.0.3", + "@polkadot/types-augment": "16.5.6", + "@polkadot/types-codec": "16.5.6", + "@polkadot/types-create": "16.5.6", + "@polkadot/util": "^14.0.3", + "@polkadot/util-crypto": "^14.0.3", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-augment": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-16.5.6.tgz", + "integrity": "sha512-QN5UrluUZCVgknUDW0gps/FRQ13Qgm24w53pCd2HgD0nmTtXDt9D4psjWwx5JkGTkUAvpzFWwN41bkxAeCiV6g==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types": "16.5.6", + "@polkadot/types-codec": "16.5.6", + "@polkadot/util": "^14.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-codec": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-16.5.6.tgz", + "integrity": "sha512-3tzUv1LZOL97IlQmko4dqbfRC0cg9IQ2QAHRVoDIWsXrVovp1V3kPdP0o6e3I8T2XB9IlbabK91v+ZiIxhGMZw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^14.0.3", + "@polkadot/x-bigint": "^14.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-create": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-16.5.6.tgz", + "integrity": "sha512-g7g3hrjpz4KgqQqei9PU0JY9fsFHBmThWALZk5pWB32vyDyDcXZiyhH3agDhqfmzQiolTW2FuvcNJxgS634J1w==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types-codec": "16.5.6", + "@polkadot/util": "^14.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-known": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-16.5.6.tgz", + "integrity": "sha512-c78NcVO3LIvi4xzxB39WewE+80I4jOYUtPBaB4AzSMespEwIr92VTeX3KzFWuutxDXLSPqeVfXhaAhBB0NssiQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/networks": "^14.0.3", + "@polkadot/types": "16.5.6", + "@polkadot/types-codec": "16.5.6", + "@polkadot/types-create": "16.5.6", + "@polkadot/util": "^14.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-support": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-16.5.6.tgz", + "integrity": "sha512-Hqpa/hCvXZXUTUiJMAE55UXpzAeCVLaFlzzXQXLkne0vhmv3/JkWcBnX755a/b9+C4b3MKEz2i0tSKLsa3DldA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^14.0.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-14.0.3.tgz", + "integrity": "sha512-mg1NR7ixHlNiz2zbvdcdy1OXZmca2tVA4DpewGpY/qFkW/gq9HdDrHLu7g0k90QnunDcFW4emb7NB60sGJQ0bw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-bigint": "14.0.3", + "@polkadot/x-global": "14.0.3", + "@polkadot/x-textdecoder": "14.0.3", + "@polkadot/x-textencoder": "14.0.3", + "@types/bn.js": "^5.1.6", + "bn.js": "^5.2.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-14.0.3.tgz", + "integrity": "sha512-V00BI6XnZLCkrAmV8uN0eSB6fy48CkxdDZT29cgSMSwHPtY6oKUNgd1ST07PGCL5x8XflwjoA7CTlhdbp1Y9gw==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "14.0.3", + "@polkadot/util": "14.0.3", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "14.0.3", + "@polkadot/x-randomvalues": "14.0.3", + "@scure/base": "^1.1.7", + "@scure/sr25519": "^0.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.3" + } + }, + "node_modules/@polkadot/wasm-bridge": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-bridge/-/wasm-bridge-7.5.4.tgz", + "integrity": "sha512-6xaJVvoZbnbgpQYXNw9OHVNWjXmtcoPcWh7hlwx3NpfiLkkjljj99YS+XGZQlq7ks2fVCg7FbfknkNb8PldDaA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.5.4.tgz", + "integrity": "sha512-1seyClxa7Jd7kQjfnCzTTTfYhTa/KUTDUaD3DMHBk5Q4ZUN1D1unJgX+v1aUeXSPxmzocdZETPJJRZjhVOqg9g==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.4", + "@polkadot/wasm-crypto-asmjs": "7.5.4", + "@polkadot/wasm-crypto-init": "7.5.4", + "@polkadot/wasm-crypto-wasm": "7.5.4", + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-asmjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.5.4.tgz", + "integrity": "sha512-ZYwxQHAJ8pPt6kYk9XFmyuFuSS+yirJLonvP+DYbxOrARRUHfN4nzp4zcZNXUuaFhpbDobDSFn6gYzye6BUotA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-init": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.5.4.tgz", + "integrity": "sha512-U6s4Eo2rHs2n1iR01vTz/sOQ7eOnRPjaCsGWhPV+ZC/20hkVzwPAhiizu/IqMEol4tO2yiSheD4D6bn0KxUJhg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.4", + "@polkadot/wasm-crypto-asmjs": "7.5.4", + "@polkadot/wasm-crypto-wasm": "7.5.4", + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-wasm": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.5.4.tgz", + "integrity": "sha512-PsHgLsVTu43eprwSvUGnxybtOEuHPES6AbApcs7y5ZbM2PiDMzYbAjNul098xJK/CPtrxZ0ePDFnaQBmIJyTFw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-util": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-util/-/wasm-util-7.5.4.tgz", + "integrity": "sha512-hqPpfhCpRAqCIn/CYbBluhh0TXmwkJnDRjxrU9Bnqtw9nMNa97D8JuOjdd2pi0rxm+eeLQ/f1rQMp71RMM9t4w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/x-bigint": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-14.0.3.tgz", + "integrity": "sha512-U0al6BKgldFrEbmSObRAlzv9VDs5SMa/rbvZKvvkVec0sWTzYPWQZU1ZC/biXLYdjdKML89BeuCKmXZtCcGhUQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.3", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-fetch": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-14.0.3.tgz", + "integrity": "sha512-695c5aPBPtYcnn2zM+u0mXgyNHINlO0qGlGcJq3/0t5NVRZv5KZhk7NNm6antOay9uUjGG40F/r+LPzDT3QamA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.3", + "node-fetch": "^3.3.2", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-global": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-14.0.3.tgz", + "integrity": "sha512-MzMEynJ7HMTy/plLmdyP8rv14RS/6s29HZodUG9aCOscBnEiEDxVEax/ztRJqxhhQuHeYdx0LYDwVbdQDTkqNw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-randomvalues": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-14.0.3.tgz", + "integrity": "sha512-qTPcrk0nIHL2tIu5e0cLj3puQvjCK7onehnqO2fvlmWeIlvDel66fwWs06Ipsib+CwLJdmE6WgNy+8Jv74r6YA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.3", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.3", + "@polkadot/wasm-util": "*" + } + }, + "node_modules/@polkadot/x-textdecoder": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-14.0.3.tgz", + "integrity": "sha512-4RJYDG00iUzQ7YAuS/yvkWRZlkjYU8PUNdJHRfqtJ+SjrSPB7LYYxFhLgw43TZUtHmIueNTsml2Ukv3xXTr2kA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.3", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-textencoder": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-14.0.3.tgz", + "integrity": "sha512-9HH6o2L+r99wEfXhPb5g+Xwn7qouqD32PsMux7B0dFGR2KNqP4KwO19Hu+gdij6wsEhy7delhZwzHenrWwDfhQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.3", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-ws": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-14.0.3.tgz", + "integrity": "sha512-tOPdkMye3iuXnuFtdNg5+iSu7Cz9LRL8z5psMuZpUpThMYChGsS2pDFtNvXOKU8ohhO+frY9VdJ9VBg1WL9Iug==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.3", + "tslib": "^2.8.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/sr25519": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-0.2.0.tgz", + "integrity": "sha512-uUuLP7Z126XdSizKtrCGqYyR3b3hYtJ6Fg/XFUXmc2//k2aXHDLqZwFeXxL97gg4XydPROPVnuaHGF2+xriSKg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.2", + "@noble/hashes": "~1.8.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@substrate/connect": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.11.tgz", + "integrity": "sha512-ofLs1PAO9AtDdPbdyTYj217Pe+lBfTLltdHDs3ds8no0BseoLeAGxpz1mHfi7zB4IxI3YyAiLjH6U8cw4pj4Nw==", + "deprecated": "versions below 1.x are no longer maintained", + "license": "GPL-3.0-only", + "optional": true, + "dependencies": { + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.5", + "@substrate/light-client-extension-helpers": "^1.0.0", + "smoldot": "2.0.26" + } + }, + "node_modules/@substrate/connect-extension-protocol": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@substrate/connect-extension-protocol/-/connect-extension-protocol-2.2.2.tgz", + "integrity": "sha512-t66jwrXA0s5Goq82ZtjagLNd7DPGCNjHeehRlE/gcJmJ+G56C0W+2plqOMRicJ8XGR1/YFnUSEqUFiSNbjGrAA==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/connect-known-chains": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@substrate/connect-known-chains/-/connect-known-chains-1.10.3.tgz", + "integrity": "sha512-OJEZO1Pagtb6bNE3wCikc2wrmvEU5x7GxFFLqqbz1AJYYxSlrPCGu4N2og5YTExo4IcloNMQYFRkBGue0BKZ4w==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-1.0.0.tgz", + "integrity": "sha512-TdKlni1mBBZptOaeVrKnusMg/UBpWUORNDv5fdCaJklP4RJiFOzBCrzC+CyVI5kQzsXBisZ+2pXm+rIjS38kHg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/json-rpc-provider": "^0.0.1", + "@polkadot-api/json-rpc-provider-proxy": "^0.1.0", + "@polkadot-api/observable-client": "^0.3.0", + "@polkadot-api/substrate-client": "^0.1.2", + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.5", + "rxjs": "^7.8.1" + }, + "peerDependencies": { + "smoldot": "2.x" + } + }, + "node_modules/@substrate/ss58-registry": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@substrate/ss58-registry/-/ss58-registry-1.51.0.tgz", + "integrity": "sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ==", + "license": "Apache-2.0" + }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nock": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scale-ts": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.1.tgz", + "integrity": "sha512-PBMc2AWc6wSEqJYBDPcyCLUj9/tMKnLX70jLOSndMtcUoLQucP/DM0vnQo1wJAYjTrQiq8iG9rD0q6wFzgjH7g==", + "license": "MIT", + "optional": true + }, + "node_modules/smoldot": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-2.0.26.tgz", + "integrity": "sha512-F+qYmH4z2s2FK+CxGj8moYcd1ekSIKH8ywkdqlOz88Dat35iB1DIYL11aILN46YSGMzQW/lbJNS307zBSDN5Ig==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "optional": true, + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/scripts/opengov-force-remove-votes/package.json b/scripts/opengov-force-remove-votes/package.json new file mode 100644 index 0000000000..42df11def3 --- /dev/null +++ b/scripts/opengov-force-remove-votes/package.json @@ -0,0 +1,15 @@ +{ + "name": "opengov-force-remove-votes", + "version": "1.0.0", + "private": true, + "description": "Force-remove finished conviction-voting votes from live chain state", + "main": "index.js", + "scripts": { + "dry-run": "node index.js", + "submit": "node index.js submit" + }, + "dependencies": { + "@polkadot/api": "latest", + "@polkadot/util-crypto": "latest" + } +} diff --git a/scripts/proxy-fee-test/scripts/setup-chain.js b/scripts/proxy-fee-test/scripts/setup-chain.js index 2c7fb41971..1197a23223 100644 --- a/scripts/proxy-fee-test/scripts/setup-chain.js +++ b/scripts/proxy-fee-test/scripts/setup-chain.js @@ -3,6 +3,7 @@ const { Keyring } = require("@polkadot/keyring"); const { blake2AsHex } = require("@polkadot/util-crypto"); const WS_URL = process.env.WS_URL || "ws://127.0.0.1:9999"; +const BLOCK_TIME_MS = 2000; function sendAndWait(tx, signer, api, timeoutMs = 60000) { return new Promise((resolve, reject) => { @@ -90,7 +91,7 @@ async function main() { await setupViaTcProposal(api, keyring); } - await sleep(6000); + await sleep(BLOCK_TIME_MS); const finalWeth = await api.query.assetRegistry.locationAssets(wethLoc); const finalId = finalWeth.isSome ? finalWeth.toJSON() : null; @@ -157,7 +158,7 @@ async function setupViaGovernance(api, alice) { console.log(" [4/4] Waiting for referendum..."); for (let i = 0; i < 30; i++) { - await sleep(6000); + await sleep(BLOCK_TIME_MS); const info = await api.query.referenda.referendumInfoFor(refIndex); const json = info.toJSON(); if (json.approved) { console.log(` Referendum #${refIndex} approved!`); return; } diff --git a/scripts/upgrade-runtime/upgrade.mjs b/scripts/upgrade-runtime/upgrade.mjs index 5f4dbe4349..a5a81ffcf9 100644 --- a/scripts/upgrade-runtime/upgrade.mjs +++ b/scripts/upgrade-runtime/upgrade.mjs @@ -31,6 +31,7 @@ const WASM_PATH = process.env.WASM || './hydradx_runtime.compact.compressed.wasm const SURI = process.env.SURI || '//Alice'; const VOTE_HDX = BigInt(process.env.VOTE_HDX || '3000000000'); const ENACT_AFTER = parseInt(process.env.ENACT_AFTER || '10', 10); +const BLOCK_TIME_MS = 2000; const HDX_DECIMALS = 12n; const VOTE_AMOUNT = VOTE_HDX * 10n ** HDX_DECIMALS; @@ -137,7 +138,7 @@ async function main() { const head = await api.rpc.chain.getHeader(); const state = h?.Ongoing?.deciding ? 'deciding' : (h?.Ongoing ? 'preparing' : 'unknown'); console.log(` block #${head.number.toNumber()} — ${state}`); - await new Promise((r) => setTimeout(r, 6000)); + await new Promise((r) => setTimeout(r, BLOCK_TIME_MS)); } if (!approved) throw new Error('Referendum did not pass in time'); @@ -151,7 +152,7 @@ async function main() { break; } console.log(` block #${head.number.toNumber()}: not yet authorized`); - await new Promise((r) => setTimeout(r, 6000)); + await new Promise((r) => setTimeout(r, BLOCK_TIME_MS)); } if (auth.isNone) throw new Error('Upgrade not authorized after waiting'); @@ -170,7 +171,7 @@ async function main() { // 6. Wait for specVersion to bump console.log('Waiting for runtime upgrade to take effect...'); for (let i = 0; i < 30; i++) { - await new Promise((r) => setTimeout(r, 6000)); + await new Promise((r) => setTimeout(r, BLOCK_TIME_MS)); const v = await api.rpc.state.getRuntimeVersion(); const head = await api.rpc.chain.getHeader(); const cur = v.specVersion.toNumber(); diff --git a/traits/src/oracle.rs b/traits/src/oracle.rs index bea5a87946..ed89334d79 100644 --- a/traits/src/oracle.rs +++ b/traits/src/oracle.rs @@ -87,7 +87,7 @@ impl OraclePeriod { pub const fn as_period(&self) -> u64 { match self { OraclePeriod::LastBlock => 1, - OraclePeriod::Short => 20, + OraclePeriod::Short => 60, OraclePeriod::TenMinutes => 10 * MINUTES as u64, OraclePeriod::Hour => HOURS as u64, OraclePeriod::Day => DAYS as u64,