From 868c07b51ac824753268a337288ab3d8bb76f8e2 Mon Sep 17 00:00:00 2001 From: cl0w Date: Wed, 29 Apr 2026 13:40:18 +0200 Subject: [PATCH 01/10] tests --- integration-tests/src/polkadot_test_net.rs | 32 ++++++++++++++++++++++ integration-tests/src/sessions.rs | 27 +++++++++++++++--- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/integration-tests/src/polkadot_test_net.rs b/integration-tests/src/polkadot_test_net.rs index dcbcb250e..9a993f420 100644 --- a/integration-tests/src/polkadot_test_net.rs +++ b/integration-tests/src/polkadot_test_net.rs @@ -490,6 +490,38 @@ pub mod collators { get_account_id_from_seed::("Bob"), get_from_seed::("Bob"), ), + ( + get_account_id_from_seed::("Charlie"), + get_from_seed::("Charlie"), + ), + ( + get_account_id_from_seed::("Collator4"), + get_from_seed::("Collator4"), + ), + ( + get_account_id_from_seed::("Collator5"), + get_from_seed::("Collator5"), + ), + ( + get_account_id_from_seed::("Collator6"), + get_from_seed::("Collator6"), + ), + ( + get_account_id_from_seed::("Collator7"), + get_from_seed::("Collator7"), + ), + ( + get_account_id_from_seed::("Collator8"), + get_from_seed::("Collator8"), + ), + ( + get_account_id_from_seed::("Collator9"), + get_from_seed::("Collator9"), + ), + ( + get_account_id_from_seed::("Collator10"), + get_from_seed::("Collator10"), + ), ] } } diff --git a/integration-tests/src/sessions.rs b/integration-tests/src/sessions.rs index 2b03a25e8..1a8710dbe 100644 --- a/integration-tests/src/sessions.rs +++ b/integration-tests/src/sessions.rs @@ -15,10 +15,29 @@ fn new_session_should_alternate_full_set_and_benched_set() { // inner `CollatorSelection::new_session` returns them in sorted order. // `pallet_collator_rotation` then benches `(N / 2) % len` on odd sessions // only; even sessions pass the full set through unchanged. - let alice = collators::invulnerables()[0].0.clone(); // d435... - let bob = collators::invulnerables()[1].0.clone(); // 8eaf... - - let full_sorted = vec![bob.clone(), alice.clone()]; + let collator1 = collators::invulnerables()[0].0.clone(); // d435... + let collator2 = collators::invulnerables()[1].0.clone(); // 8eaf... + let collator3 = collators::invulnerables()[2].0.clone(); // 90b5... + let collator4 = collators::invulnerables()[3].0.clone(); // 6ebe... + let collator5 = collators::invulnerables()[4].0.clone(); // ec5e... + let collator6 = collators::invulnerables()[5].0.clone(); // 9c78... + let collator7 = collators::invulnerables()[6].0.clone(); // a678... + let collator8 = collators::invulnerables()[7].0.clone(); // 2433... + let collator9 = collators::invulnerables()[8].0.clone(); // ee28... + let collator10 = collators::invulnerables()[9].0.clone(); // da53... + + let full_sorted = vec![ + collator8.clone(), + collator4.clone(), + collator2.clone(), + collator3.clone(), + collator6.clone(), + collator7.clone(), + collator1.clone(), + collator10.clone(), + collator5.clone(), + collator9.clone(), + ]; // Session 0 (even): full set, no bench. assert_eq!(CollatorRewards::new_session(0).unwrap(), full_sorted); From 2091735b863c56f8017c99592d10c86a171fea89 Mon Sep 17 00:00:00 2001 From: cl0w Date: Thu, 30 Apr 2026 17:00:07 +0200 Subject: [PATCH 02/10] runtime support for 2s blocktime --- primitives/Cargo.toml | 2 +- primitives/src/constants.rs | 22 +++++++++++++--------- runtime/hydradx/src/lib.rs | 2 +- runtime/hydradx/src/system.rs | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index b315a59df..2dc61c8f9 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "primitives" -version = "6.3.0" +version = "6.3.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 585bc07ee..c91ce7570 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, ); @@ -90,7 +94,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; } @@ -106,10 +110,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/src/lib.rs b/runtime/hydradx/src/lib.rs index 622b7dd08..8f4ee8886 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: 412, + spec_version: 413, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/hydradx/src/system.rs b/runtime/hydradx/src/system.rs index d36dc2fd4..2e2413442 100644 --- a/runtime/hydradx/src/system.rs +++ b/runtime/hydradx/src/system.rs @@ -270,7 +270,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; } From 33262e0aada93c65346d55f89bca611eb9d5540b Mon Sep 17 00:00:00 2001 From: cl0w Date: Thu, 30 Apr 2026 17:02:14 +0200 Subject: [PATCH 03/10] node support for 2s blocktime --- Cargo.lock | 6 +++--- node/Cargo.toml | 2 +- node/src/service.rs | 2 +- runtime/hydradx/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13136477e..bd37d71fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6230,7 +6230,7 @@ dependencies = [ [[package]] name = "hydradx" -version = "15.0.0" +version = "15.1.0" dependencies = [ "async-trait", "clap", @@ -6379,7 +6379,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "412.0.0" +version = "413.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", @@ -14700,7 +14700,7 @@ dependencies = [ [[package]] name = "primitives" -version = "6.3.0" +version = "6.3.1" dependencies = [ "frame-support", "hex-literal", diff --git a/node/Cargo.toml b/node/Cargo.toml index dce8c2d5e..d9ad6a8a9 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx" -version = "15.0.0" +version = "15.1.0" description = "Hydration node" authors = ["GalacticCouncil"] edition = "2021" diff --git a/node/src/service.rs b/node/src/service.rs index f42c095b7..49d8fe6d5 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -630,7 +630,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/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 455c12e0c..aff46fcf0 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "412.0.0" +version = "413.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" From 40a67ac3e85153af22a00c9a20f94051660bd407 Mon Sep 17 00:00:00 2001 From: cl0w Date: Thu, 30 Apr 2026 17:02:59 +0200 Subject: [PATCH 04/10] zombienet support for 2s blocktime --- launch-configs/zombienet/local.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/launch-configs/zombienet/local.json b/launch-configs/zombienet/local.json index ca9d05666..fde828e23 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 }, { From c74765d0a03abf1d908db59652b59da0e56c7ede Mon Sep 17 00:00:00 2001 From: cl0w Date: Thu, 30 Apr 2026 17:24:23 +0200 Subject: [PATCH 05/10] add script to assign cores --- README.md | 5 +- scripts/assign_cores.js | 187 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 scripts/assign_cores.js diff --git a/README.md b/README.md index 83791f0c7..78f858f31 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/scripts/assign_cores.js b/scripts/assign_cores.js new file mode 100644 index 000000000..ca0dc5495 --- /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); +}); From ee18986c281cffa198a2ef9f09ceee0915b01710 Mon Sep 17 00:00:00 2001 From: cl0w Date: Mon, 4 May 2026 13:21:26 +0200 Subject: [PATCH 06/10] frontier at 2s --- node/src/service/evm.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node/src/service/evm.rs b/node/src/service/evm.rs index 1934a0145..e02d13abb 100644 --- a/node/src/service/evm.rs +++ b/node/src/service/evm.rs @@ -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(), @@ -177,6 +177,7 @@ pub fn spawn_frontier_tasks( // Spawn Frontier EthFilterApi maintenance task. // Each filter is allowed to stay in the pool for 100 blocks. + // TODO: 2s increase? const FILTER_RETAIN_THRESHOLD: u64 = 100; task_manager.spawn_essential_handle().spawn( "frontier-filter-pool", @@ -185,6 +186,7 @@ pub fn spawn_frontier_tasks( ); // Spawn Frontier FeeHistory cache maintenance task. + // TODO: 2s increase default from 2048? task_manager.spawn_essential_handle().spawn( "frontier-fee-history", None, From f12ae34a4f9f7e85b0ad1449f928a5cd4b7966ed Mon Sep 17 00:00:00 2001 From: cl0w Date: Mon, 4 May 2026 14:19:45 +0200 Subject: [PATCH 07/10] comment --- pallets/lbp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/lbp/src/lib.rs b/pallets/lbp/src/lib.rs index 5dc5a340e..697bff68f 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 From c43063344085c8112a001db90b75b8f5cf1f407d Mon Sep 17 00:00:00 2001 From: cl0w Date: Thu, 7 May 2026 19:33:34 +0200 Subject: [PATCH 08/10] pallet-staking at 2s --- Cargo.lock | 10 +-- math/src/staking/math.rs | 36 ++++++-- math/src/staking/tests.rs | 74 +++++++++++++++-- pallets/staking/Cargo.toml | 2 +- pallets/staking/src/lib.rs | 14 +++- pallets/staking/src/migrations.rs | 113 +++++++++++++++++++++++++- pallets/staking/src/tests/tests.rs | 14 ++++ runtime/hydradx/src/assets.rs | 2 +- runtime/hydradx/src/migrations/mod.rs | 6 +- 9 files changed, 243 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd37d71fb..d98ebd8ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6357,7 +6357,7 @@ dependencies = [ "pallet-referrals", "pallet-route-executor", "pallet-stableswap", - "pallet-staking 4.3.1", + "pallet-staking 4.4.0", "pallet-timestamp", "pallet-transaction-multi-payment", "pallet-uniques", @@ -6493,7 +6493,7 @@ dependencies = [ "pallet-session", "pallet-signet", "pallet-stableswap", - "pallet-staking 4.3.1", + "pallet-staking 4.4.0", "pallet-state-trie-migration", "pallet-timestamp", "pallet-tips", @@ -11929,7 +11929,7 @@ dependencies = [ [[package]] name = "pallet-staking" -version = "4.3.1" +version = "4.4.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -15746,7 +15746,7 @@ dependencies = [ "pallet-scheduler", "pallet-session", "pallet-stableswap", - "pallet-staking 4.3.1", + "pallet-staking 4.4.0", "pallet-timestamp", "pallet-transaction-multi-payment", "pallet-transaction-pause", @@ -15812,7 +15812,7 @@ dependencies = [ "pallet-asset-registry", "pallet-omnipool", "pallet-stableswap", - "pallet-staking 4.3.1", + "pallet-staking 4.4.0", "primitives", "scraper", "serde", diff --git a/math/src/staking/math.rs b/math/src/staking/math.rs index 7b9d63435..c04490fa1 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 79b7df2a7..37d88567b 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/pallets/staking/Cargo.toml b/pallets/staking/Cargo.toml index 6d7bbe8df..ad6a6807a 100644 --- a/pallets/staking/Cargo.toml +++ b/pallets/staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-staking" -version = "4.3.1" +version = "4.4.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/staking/src/lib.rs b/pallets/staking/src/lib.rs index 03e953e88..e100cd3a6 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)] @@ -189,6 +189,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)] @@ -251,6 +256,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 { @@ -951,6 +962,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 086064a49..9679e4799 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,10 +25,64 @@ 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 frame_system::pallet_prelude::BlockNumberFor; @@ -58,4 +115,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 71ad50ca2..1d30f2a67 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/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index e28cb16be..7693c080e 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -1595,7 +1595,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; diff --git a/runtime/hydradx/src/migrations/mod.rs b/runtime/hydradx/src/migrations/mod.rs index cf4edd684..6d92ce68e 100644 --- a/runtime/hydradx/src/migrations/mod.rs +++ b/runtime/hydradx/src/migrations/mod.rs @@ -16,8 +16,10 @@ use crate::Runtime; // New migrations which need to be cleaned up after every Runtime upgrade -pub type UnreleasedSingleBlockMigrations = - pallet_ema_oracle::migrations::v2::MigrateV1ToV2; +pub type UnreleasedSingleBlockMigrations = ( + pallet_ema_oracle::migrations::v2::MigrateV1ToV2, + pallet_staking::migrations::SetTwoSecBlocksSince, +); // These migrations can run on every runtime upgrade pub type PermanentSingleBlockMigrations = pallet_xcm::migration::MigrateToLatestXcmVersion; From 9c38d405b5f6089608d7b9be786586693a9062dc Mon Sep 17 00:00:00 2001 From: vgantchev Date: Tue, 19 May 2026 10:37:15 +0200 Subject: [PATCH 09/10] pallet-dca at 2s --- Cargo.lock | 2 +- pallets/dca/Cargo.toml | 2 +- pallets/dca/src/lib.rs | 2 +- pallets/dca/src/migrations.rs | 55 ++++++++++++++++++++------- runtime/hydradx/src/migrations/mod.rs | 1 + 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d98ebd8ff..92acc2bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10115,7 +10115,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.18.1" +version = "1.19.0" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", diff --git a/pallets/dca/Cargo.toml b/pallets/dca/Cargo.toml index b65f45a52..45aedea4e 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 cc7b6d441..802572784 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 a4a4b969a..12cefc8a1 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,13 +25,18 @@ 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); } + 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); + } + for (key, mut schedule) in crate::Schedules::::iter() { - schedule.period = schedule.period.saturating_mul(2u32.into()); + schedule.period = schedule.period.saturating_mul(3u32.into()); crate::Schedules::::insert(key, schedule); reads.saturating_inc(); writes.saturating_inc(); @@ -36,11 +50,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 +81,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 +97,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/runtime/hydradx/src/migrations/mod.rs b/runtime/hydradx/src/migrations/mod.rs index 6d92ce68e..7f45c63d3 100644 --- a/runtime/hydradx/src/migrations/mod.rs +++ b/runtime/hydradx/src/migrations/mod.rs @@ -19,6 +19,7 @@ use crate::Runtime; pub type UnreleasedSingleBlockMigrations = ( pallet_ema_oracle::migrations::v2::MigrateV1ToV2, pallet_staking::migrations::SetTwoSecBlocksSince, + pallet_dca::migrations::MultiplySchedulesPeriodBy3, ); // These migrations can run on every runtime upgrade From 07d32c3dc81465d175b49c1aae5bfacf45fb4edc Mon Sep 17 00:00:00 2001 From: vgantchev Date: Tue, 19 May 2026 11:58:19 +0200 Subject: [PATCH 10/10] scheduler at 2s --- runtime/hydradx/src/migrations/mod.rs | 3 + runtime/hydradx/src/migrations/scheduler.rs | 163 ++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 runtime/hydradx/src/migrations/scheduler.rs diff --git a/runtime/hydradx/src/migrations/mod.rs b/runtime/hydradx/src/migrations/mod.rs index 7f45c63d3..0b152efe8 100644 --- a/runtime/hydradx/src/migrations/mod.rs +++ b/runtime/hydradx/src/migrations/mod.rs @@ -15,11 +15,14 @@ use crate::Runtime; +pub mod scheduler; + // New migrations which need to be cleaned up after every Runtime upgrade pub type UnreleasedSingleBlockMigrations = ( pallet_ema_oracle::migrations::v2::MigrateV1ToV2, pallet_staking::migrations::SetTwoSecBlocksSince, pallet_dca::migrations::MultiplySchedulesPeriodBy3, + scheduler::MigrateSchedulerTo2sBlocks, ); // These migrations can run on every runtime upgrade diff --git a/runtime/hydradx/src/migrations/scheduler.rs b/runtime/hydradx/src/migrations/scheduler.rs new file mode 100644 index 000000000..0d6bb2d32 --- /dev/null +++ b/runtime/hydradx/src/migrations/scheduler.rs @@ -0,0 +1,163 @@ +// Copyright (C) 2020-2025 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; + + if agenda_len >= 150 { + log::error!("Error: more than 150 agendas exist, len: {:?}", 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, 3))); + 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)); + }) + } +}