diff --git a/Cargo.lock b/Cargo.lock index e6ca24864b2f6..199196bc48c29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4242,6 +4242,7 @@ dependencies = [ "pallet-bags-list", "pallet-balances", "pallet-bounties", + "pallet-broker", "pallet-child-bounties", "pallet-collective", "pallet-contracts", @@ -6436,6 +6437,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-broker" +version = "0.1.0" +dependencies = [ + "bitvec", + "frame-benchmarking", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-child-bounties" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 9ee8142e23e76..1411e2ca796da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ members = [ "frame/benchmarking", "frame/benchmarking/pov", "frame/bounties", + "frame/broker", "frame/child-bounties", "frame/collective", "frame/contracts", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 61202a30d69e1..135621a3be730 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -67,6 +67,7 @@ pallet-babe = { version = "4.0.0-dev", default-features = false, path = "../../. pallet-bags-list = { version = "4.0.0-dev", default-features = false, path = "../../../frame/bags-list" } pallet-balances = { version = "4.0.0-dev", default-features = false, path = "../../../frame/balances" } pallet-bounties = { version = "4.0.0-dev", default-features = false, path = "../../../frame/bounties" } +pallet-broker = { version = "0.1.0", default-features = false, path = "../../../frame/broker" } pallet-child-bounties = { version = "4.0.0-dev", default-features = false, path = "../../../frame/child-bounties" } pallet-collective = { version = "4.0.0-dev", default-features = false, path = "../../../frame/collective" } pallet-contracts = { version = "4.0.0-dev", default-features = false, path = "../../../frame/contracts" } @@ -160,6 +161,7 @@ std = [ "pallet-bags-list/std", "pallet-balances/std", "pallet-bounties/std", + "pallet-broker/std", "pallet-child-bounties/std", "pallet-collective/std", "pallet-contracts-primitives/std", @@ -254,6 +256,7 @@ runtime-benchmarks = [ "pallet-bags-list/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-bounties/runtime-benchmarks", + "pallet-broker/runtime-benchmarks", "pallet-child-bounties/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-contracts/runtime-benchmarks", @@ -324,6 +327,7 @@ try-runtime = [ "pallet-bags-list/try-runtime", "pallet-balances/try-runtime", "pallet-bounties/try-runtime", + "pallet-broker/try-runtime", "pallet-child-bounties/try-runtime", "pallet-collective/try-runtime", "pallet-contracts/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index dc9230d927f70..fe2382cce613c 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -35,7 +35,7 @@ use frame_support::{ pallet_prelude::Get, parameter_types, traits::{ - fungible::ItemOf, + fungible::{Balanced, Credit, ItemOf}, tokens::{nonfungibles_v2::Inspect, GetSalary, PayFromAccount}, AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse, EqualPrivilegeOnly, Everything, Imbalance, InstanceFilter, KeyOwnerProofSystem, @@ -56,6 +56,7 @@ use frame_system::{ pub use node_primitives::{AccountId, Signature}; use node_primitives::{AccountIndex, Balance, BlockNumber, Hash, Moment, Nonce}; use pallet_asset_conversion::{NativeOrAssetId, NativeOrAssetIdConverter}; +use pallet_broker::{CoreAssignment, CoreIndex, CoretimeInterface, PartsOf57600}; use pallet_election_provider_multi_phase::SolutionAccuracyOf; use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use pallet_nfts::PalletFeatures; @@ -1877,6 +1878,77 @@ impl pallet_statement::Config for Runtime { type MaxAllowedBytes = MaxAllowedBytes; } +parameter_types! { + pub const BrokerPalletId: PalletId = PalletId(*b"py/broke"); +} + +pub struct IntoAuthor; +impl OnUnbalanced> for IntoAuthor { + fn on_nonzero_unbalanced(credit: Credit) { + if let Some(author) = Authorship::author() { + let _ = >::resolve(&author, credit); + } + } +} + +parameter_types! { + pub storage CoreCount: Option = None; + pub storage CoretimeRevenue: Option<(BlockNumber, Balance)> = None; +} + +pub struct CoretimeProvider; +impl CoretimeInterface for CoretimeProvider { + type AccountId = AccountId; + type Balance = Balance; + type BlockNumber = BlockNumber; + fn latest() -> Self::BlockNumber { + System::block_number() + } + fn request_core_count(_count: CoreIndex) {} + fn request_revenue_info_at(_when: Self::BlockNumber) {} + fn credit_account(_who: Self::AccountId, _amount: Self::Balance) {} + fn assign_core( + _core: CoreIndex, + _begin: Self::BlockNumber, + _assignment: Vec<(CoreAssignment, PartsOf57600)>, + _end_hint: Option, + ) { + } + fn check_notify_core_count() -> Option { + let count = CoreCount::get(); + CoreCount::set(&None); + count + } + fn check_notify_revenue_info() -> Option<(Self::BlockNumber, Self::Balance)> { + let revenue = CoretimeRevenue::get(); + CoretimeRevenue::set(&None); + revenue + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_core_count(count: u16) { + CoreCount::set(&Some(count)); + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_revenue_info(when: Self::BlockNumber, revenue: Self::Balance) { + CoretimeRevenue::set(&Some((when, revenue))); + } +} + +impl pallet_broker::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type OnRevenue = IntoAuthor; + type TimeslicePeriod = ConstU32<2>; + type MaxLeasedCores = ConstU32<5>; + type MaxReservedCores = ConstU32<5>; + type Coretime = CoretimeProvider; + type ConvertBalance = traits::Identity; + type WeightInfo = (); + type PalletId = BrokerPalletId; + type AdminOrigin = EnsureRoot; + type PriceAdapter = pallet_broker::Linear; +} + construct_runtime!( pub struct Runtime { @@ -1950,6 +2022,7 @@ construct_runtime!( MessageQueue: pallet_message_queue, Pov: frame_benchmarking_pallet_pov, Statement: pallet_statement, + Broker: pallet_broker, } ); @@ -2030,6 +2103,7 @@ mod benches { [pallet_bags_list, VoterList] [pallet_balances, Balances] [pallet_bounties, Bounties] + [pallet_broker, Broker] [pallet_child_bounties, ChildBounties] [pallet_collective, Council] [pallet_conviction_voting, ConvictionVoting] diff --git a/client/consensus/grandpa/src/communication/gossip.rs b/client/consensus/grandpa/src/communication/gossip.rs index 5688aff3ea717..3a78b157d5b1b 100644 --- a/client/consensus/grandpa/src/communication/gossip.rs +++ b/client/consensus/grandpa/src/communication/gossip.rs @@ -546,9 +546,7 @@ impl Peers { who: &PeerId, update: NeighborPacket, ) -> Result>, Misbehavior> { - let Some(peer) = self.inner.get_mut(who) else { - return Ok(None) - }; + let Some(peer) = self.inner.get_mut(who) else { return Ok(None) }; let invalid_change = peer.view.set_id > update.set_id || peer.view.round > update.round && peer.view.set_id == update.set_id || diff --git a/frame/broker/Cargo.toml b/frame/broker/Cargo.toml new file mode 100644 index 0000000000000..daa2426b2c34a --- /dev/null +++ b/frame/broker/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "pallet-broker" +version = "0.1.0" +description = "Brokerage tool for managing Polkadot Core scheduling" +authors = ["Parity Technologies "] +homepage = "https://substrate.io" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/paritytech/substrate" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ "derive"] } +scale-info = { version = "2.0.0", default-features = false, features = ["derive"] } +bitvec = "1" +sp-std = { version = "8.0.0", default-features = false, path = "../../primitives/std" } +sp-arithmetic = { version = "16.0.0", default-features = false, path = "../../primitives/arithmetic" } +sp-core = { version = "21.0.0", default-features = false, path = "../../primitives/core" } +sp-runtime = { version = "24.0.0", default-features = false, path = "../../primitives/runtime" } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } + +[dev-dependencies] +sp-io = { version = "23.0.0", path = "../../primitives/io" } + +[features] +default = [ "std" ] + +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] + +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] + +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/frame/broker/README.md b/frame/broker/README.md new file mode 100644 index 0000000000000..65b6179863e3d --- /dev/null +++ b/frame/broker/README.md @@ -0,0 +1,26 @@ +# Pallet Broker + +Brokerage tool for managing Polkadot Core scheduling. + +Properly described in RFC-0001 Agile Coretime. + +## Implemnentation Specifics + +### Core Mask Bits + +This is 1/80th of a Polkadot Core per timeslice. Assuming timeslices are 80 blocks, then this +indicates usage of a single core one time over a timeslice. + +### The Sale + +```nocompile + 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 +-------------------------------------------------------- +< interlude > + < sale > + ... of which ... + < descending-price >< fixed-price > + | <-------\ +price fixed, unsold assigned to instapool, system cores reserved -/ +``` diff --git a/frame/broker/src/adapt_price.rs b/frame/broker/src/adapt_price.rs new file mode 100644 index 0000000000000..8266625687a23 --- /dev/null +++ b/frame/broker/src/adapt_price.rs @@ -0,0 +1,84 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![deny(missing_docs)] + +use crate::CoreIndex; +use sp_arithmetic::{traits::One, FixedU64}; + +/// Type for determining how to set price. +pub trait AdaptPrice { + /// Return the factor by which the regular price must be multiplied during the leadin period. + /// + /// - `when`: The amount through the leadin period; between zero and one. + fn leadin_factor_at(when: FixedU64) -> FixedU64; + /// Return the correction factor by which the regular price must be multiplied based on market + /// performance. + /// + /// - `sold`: The number of cores sold. + /// - `target`: The target number of cores to be sold (must be larger than zero). + /// - `limit`: The maximum number of cores to be sold. + fn adapt_price(sold: CoreIndex, target: CoreIndex, limit: CoreIndex) -> FixedU64; +} + +impl AdaptPrice for () { + fn leadin_factor_at(_: FixedU64) -> FixedU64 { + FixedU64::one() + } + fn adapt_price(_: CoreIndex, _: CoreIndex, _: CoreIndex) -> FixedU64 { + FixedU64::one() + } +} + +/// Simple implementation of `AdaptPrice` giving a monotonic leadin and a linear price change based +/// on cores sold. +pub struct Linear; +impl AdaptPrice for Linear { + fn leadin_factor_at(when: FixedU64) -> FixedU64 { + FixedU64::from(2) - when + } + fn adapt_price(sold: CoreIndex, target: CoreIndex, limit: CoreIndex) -> FixedU64 { + if sold <= target { + FixedU64::from_rational(sold.into(), target.into()) + } else { + FixedU64::one() + + FixedU64::from_rational((sold - target).into(), (limit - target).into()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn linear_no_panic() { + for limit in 0..10 { + for target in 1..10 { + for sold in 0..=limit { + let price = Linear::adapt_price(sold, target, limit); + + if sold > target { + assert!(price > FixedU64::one()); + } else { + assert!(price <= FixedU64::one()); + } + } + } + } + } +} diff --git a/frame/broker/src/benchmarking.rs b/frame/broker/src/benchmarking.rs new file mode 100644 index 0000000000000..663bf2f466cf3 --- /dev/null +++ b/frame/broker/src/benchmarking.rs @@ -0,0 +1,858 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use crate::{CoreAssignment::Task, Pallet as Broker}; +use frame_benchmarking::v2::*; +use frame_support::{ + storage::bounded_vec::BoundedVec, + traits::{ + fungible::{Inspect, Mutate}, + EnsureOrigin, Hooks, + }, +}; +use frame_system::{Pallet as System, RawOrigin}; +use sp_arithmetic::{traits::Zero, Perbill}; +use sp_core::Get; +use sp_runtime::Saturating; +use sp_std::{vec, vec::Vec}; + +const SEED: u32 = 0; +const MAX_CORE_COUNT: u16 = 1_000; + +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn new_config_record() -> ConfigRecordOf { + ConfigRecord { + advance_notice: 2u32.into(), + interlude_length: 1u32.into(), + leadin_length: 1u32.into(), + ideal_bulk_proportion: Default::default(), + limit_cores_offered: None, + region_length: 3, + renewal_bump: Perbill::from_percent(10), + contribution_timeout: 5, + } +} + +fn new_schedule() -> Schedule { + // Max items for worst case + let mut items = Vec::new(); + for i in 0..CORE_MASK_BITS { + items.push(ScheduleItem { + assignment: Task(i.try_into().unwrap()), + mask: CoreMask::complete(), + }); + } + Schedule::truncate_from(items) +} + +fn setup_reservations(n: u32) { + let schedule = new_schedule(); + + Reservations::::put(BoundedVec::try_from(vec![schedule.clone(); n as usize]).unwrap()); +} + +fn setup_leases(n: u32, task: u32, until: u32) { + Leases::::put( + BoundedVec::try_from(vec![LeaseRecordItem { task, until: until.into() }; n as usize]) + .unwrap(), + ); +} + +fn advance_to(b: u32) { + while System::::block_number() < b.into() { + System::::set_block_number(System::::block_number().saturating_add(1u32.into())); + Broker::::on_initialize(System::::block_number()); + } +} + +fn setup_and_start_sale() -> Result { + Configuration::::put(new_config_record::()); + + // Assume Reservations to be filled for worst case + setup_reservations::(T::MaxReservedCores::get()); + + // Assume Leases to be filled for worst case + setup_leases::(T::MaxLeasedCores::get(), 1, 10); + + Broker::::do_start_sales(10u32.into(), MAX_CORE_COUNT.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + Ok(T::MaxReservedCores::get() + .saturating_add(T::MaxLeasedCores::get()) + .try_into() + .unwrap()) +} + +#[benchmarks] +mod benches { + use super::*; + use crate::Finality::*; + + #[benchmark] + fn configure() -> Result<(), BenchmarkError> { + let config = new_config_record::(); + + let origin = + T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, config.clone()); + + assert_eq!(Configuration::::get(), Some(config)); + + Ok(()) + } + + #[benchmark] + fn reserve() -> Result<(), BenchmarkError> { + let schedule = new_schedule(); + + // Assume Reservations to be almost filled for worst case + setup_reservations::(T::MaxReservedCores::get().saturating_sub(1)); + + let origin = + T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, schedule); + + assert_eq!(Reservations::::get().len(), T::MaxReservedCores::get() as usize); + + Ok(()) + } + + #[benchmark] + fn unreserve() -> Result<(), BenchmarkError> { + // Assume Reservations to be filled for worst case + setup_reservations::(T::MaxReservedCores::get()); + + let origin = + T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, 0); + + assert_eq!( + Reservations::::get().len(), + T::MaxReservedCores::get().saturating_sub(1) as usize + ); + + Ok(()) + } + + #[benchmark] + fn set_lease() -> Result<(), BenchmarkError> { + let task = 1u32; + let until = 10u32.into(); + + // Assume Leases to be almost filled for worst case + setup_leases::(T::MaxLeasedCores::get().saturating_sub(1), task, until); + + let origin = + T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, task, until); + + assert_eq!(Leases::::get().len(), T::MaxLeasedCores::get() as usize); + + Ok(()) + } + + #[benchmark] + fn start_sales(n: Linear<0, { MAX_CORE_COUNT.into() }>) -> Result<(), BenchmarkError> { + Configuration::::put(new_config_record::()); + + // Assume Reservations to be filled for worst case + setup_reservations::(T::MaxReservedCores::get()); + + // Assume Leases to be filled for worst case + setup_leases::(T::MaxLeasedCores::get(), 1, 10); + + let initial_price = 10u32.into(); + + let origin = + T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, initial_price, n.try_into().unwrap()); + + assert!(SaleInfo::::get().is_some()); + assert_last_event::( + Event::SaleInitialized { + sale_start: 2u32.into(), + leadin_length: 1u32.into(), + start_price: 20u32.into(), + regular_price: 10u32.into(), + region_begin: 4, + region_end: 7, + ideal_cores_sold: 0, + cores_offered: n + .saturating_sub(T::MaxReservedCores::get()) + .saturating_sub(T::MaxLeasedCores::get()) + .try_into() + .unwrap(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn purchase() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), 10u32.into()); + + assert_eq!(SaleInfo::::get().unwrap().sellout_price, Some(10u32.into())); + assert_last_event::( + Event::Purchased { + who: caller, + region_id: RegionId { begin: 4, core, mask: CoreMask::complete() }, + price: 10u32.into(), + duration: 3u32.into(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn renew() -> Result<(), BenchmarkError> { + setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(20u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + Broker::::do_assign(region, None, 1001, Final) + .map_err(|_| BenchmarkError::Weightless)?; + + advance_to::(6); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region.core); + + let id = AllowedRenewalId { core: region.core, when: 10 }; + assert!(AllowedRenewals::::get(id).is_some()); + + Ok(()) + } + + #[benchmark] + fn transfer() -> Result<(), BenchmarkError> { + setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + let recipient: T::AccountId = account("recipient", 0, SEED); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), region, recipient.clone()); + + assert_last_event::( + Event::Transferred { + region_id: region, + old_owner: caller, + owner: recipient, + duration: 3u32.into(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn partition() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region, 2); + + assert_last_event::( + Event::Partitioned { + old_region_id: RegionId { begin: 4, core, mask: CoreMask::complete() }, + new_region_ids: ( + RegionId { begin: 4, core, mask: CoreMask::complete() }, + RegionId { begin: 6, core, mask: CoreMask::complete() }, + ), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn interlace() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region, 0x00000_fffff_fffff_00000.into()); + + assert_last_event::( + Event::Interlaced { + old_region_id: RegionId { begin: 4, core, mask: CoreMask::complete() }, + new_region_ids: ( + RegionId { begin: 4, core, mask: 0x00000_fffff_fffff_00000.into() }, + RegionId { + begin: 4, + core, + mask: CoreMask::complete() ^ 0x00000_fffff_fffff_00000.into(), + }, + ), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn assign() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region, 1000, Provisional); + + let workplan_key = (region.begin, region.core); + assert!(Workplan::::get(workplan_key).is_some()); + + assert!(Regions::::get(region).is_some()); + + assert_last_event::( + Event::Assigned { + region_id: RegionId { begin: 4, core, mask: CoreMask::complete() }, + task: 1000, + duration: 3u32.into(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn pool() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + let recipient: T::AccountId = account("recipient", 0, SEED); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region, recipient, Final); + + let workplan_key = (region.begin, region.core); + assert!(Workplan::::get(workplan_key).is_some()); + + assert_last_event::( + Event::Pooled { + region_id: RegionId { begin: 4, core, mask: CoreMask::complete() }, + duration: 3u32.into(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn claim_revenue( + m: Linear<1, { new_config_record::().region_length }>, + ) -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + T::Currency::set_balance( + &Broker::::account_id(), + T::Currency::minimum_balance().saturating_add(200u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + let recipient: T::AccountId = account("recipient", 0, SEED); + T::Currency::set_balance(&recipient.clone(), T::Currency::minimum_balance()); + + Broker::::do_pool(region, None, recipient.clone(), Final) + .map_err(|_| BenchmarkError::Weightless)?; + + let revenue = 10u32.into(); + InstaPoolHistory::::insert( + region.begin, + InstaPoolHistoryRecord { + private_contributions: 4u32.into(), + system_contributions: 3u32.into(), + maybe_payout: Some(revenue), + }, + ); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region, m); + + assert!(InstaPoolHistory::::get(region.begin).is_none()); + assert_last_event::( + Event::RevenueClaimPaid { + who: recipient, + amount: 200u32.into(), + next: if m < new_config_record::().region_length { + Some(RegionId { begin: 4.saturating_add(m), core, mask: CoreMask::complete() }) + } else { + None + }, + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn purchase_credit() -> Result<(), BenchmarkError> { + setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(30u32.into()), + ); + T::Currency::set_balance(&Broker::::account_id(), T::Currency::minimum_balance()); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + let recipient: T::AccountId = account("recipient", 0, SEED); + + Broker::::do_pool(region, None, recipient, Final) + .map_err(|_| BenchmarkError::Weightless)?; + + let beneficiary: RelayAccountIdOf = account("beneficiary", 0, SEED); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), 20u32.into(), beneficiary.clone()); + + assert_last_event::( + Event::CreditPurchased { who: caller, beneficiary, amount: 20u32.into() }.into(), + ); + + Ok(()) + } + + #[benchmark] + fn drop_region() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + advance_to::(12); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region); + + assert_last_event::( + Event::RegionDropped { + region_id: RegionId { begin: 4, core, mask: CoreMask::complete() }, + duration: 3u32.into(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn drop_contribution() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(10u32.into()), + ); + + let region = Broker::::do_purchase(caller.clone(), 10u32.into()) + .map_err(|_| BenchmarkError::Weightless)?; + + let recipient: T::AccountId = account("recipient", 0, SEED); + + Broker::::do_pool(region, None, recipient, Final) + .map_err(|_| BenchmarkError::Weightless)?; + + advance_to::(26); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), region); + + assert_last_event::( + Event::ContributionDropped { + region_id: RegionId { begin: 4, core, mask: CoreMask::complete() }, + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn drop_history() -> Result<(), BenchmarkError> { + setup_and_start_sale::()?; + let when = 5u32.into(); + let revenue = 10u32.into(); + + advance_to::(25); + + let caller: T::AccountId = whitelisted_caller(); + InstaPoolHistory::::insert( + when, + InstaPoolHistoryRecord { + private_contributions: 4u32.into(), + system_contributions: 3u32.into(), + maybe_payout: Some(revenue), + }, + ); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), when); + + assert!(InstaPoolHistory::::get(when).is_none()); + assert_last_event::(Event::HistoryDropped { when, revenue }.into()); + + Ok(()) + } + + #[benchmark] + fn drop_renewal() -> Result<(), BenchmarkError> { + let core = setup_and_start_sale::()?; + let when = 5u32.into(); + + advance_to::(10); + + let id = AllowedRenewalId { core, when }; + let record = AllowedRenewalRecord { + price: 1u32.into(), + completion: CompletionStatus::Complete(new_schedule()), + }; + AllowedRenewals::::insert(id, record); + + let caller: T::AccountId = whitelisted_caller(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller), core, when); + + assert!(AllowedRenewals::::get(id).is_none()); + assert_last_event::(Event::AllowedRenewalDropped { core, when }.into()); + + Ok(()) + } + + #[benchmark] + fn request_core_count(n: Linear<0, { MAX_CORE_COUNT.into() }>) -> Result<(), BenchmarkError> { + let admin_origin = + T::AdminOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + #[extrinsic_call] + _(admin_origin as T::RuntimeOrigin, n.try_into().unwrap()); + + assert_last_event::( + Event::CoreCountRequested { core_count: n.try_into().unwrap() }.into(), + ); + + Ok(()) + } + + #[benchmark] + fn process_core_count(n: Linear<0, { MAX_CORE_COUNT.into() }>) -> Result<(), BenchmarkError> { + setup_and_start_sale::()?; + + let core_count = n.try_into().unwrap(); + + ::ensure_notify_core_count(core_count); + + let mut status = Status::::get().ok_or(BenchmarkError::Weightless)?; + + #[block] + { + Broker::::process_core_count(&mut status); + } + + assert_last_event::(Event::CoreCountChanged { core_count }.into()); + + Ok(()) + } + + #[benchmark] + fn process_revenue() -> Result<(), BenchmarkError> { + setup_and_start_sale::()?; + + advance_to::(2); + + let caller: T::AccountId = whitelisted_caller(); + T::Currency::set_balance( + &caller.clone(), + T::Currency::minimum_balance().saturating_add(30u32.into()), + ); + T::Currency::set_balance(&Broker::::account_id(), T::Currency::minimum_balance()); + + ::ensure_notify_revenue_info(10u32.into(), 10u32.into()); + + InstaPoolHistory::::insert( + 4u32, + InstaPoolHistoryRecord { + private_contributions: 1u32.into(), + system_contributions: 9u32.into(), + maybe_payout: None, + }, + ); + + #[block] + { + Broker::::process_revenue(); + } + + assert_last_event::( + Event::ClaimsReady { + when: 4u32.into(), + system_payout: 9u32.into(), + private_payout: 1u32.into(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn rotate_sale(n: Linear<0, { MAX_CORE_COUNT.into() }>) { + let core_count = n.try_into().unwrap(); + let config = new_config_record::(); + + let now = frame_system::Pallet::::block_number(); + let price = 10u32.into(); + let commit_timeslice = Broker::::latest_timeslice_ready_to_commit(&config); + let sale = SaleInfoRecordOf:: { + sale_start: now, + leadin_length: Zero::zero(), + price, + sellout_price: None, + region_begin: commit_timeslice, + region_end: commit_timeslice.saturating_add(config.region_length), + first_core: 0, + ideal_cores_sold: 0, + cores_offered: 0, + cores_sold: 0, + }; + + let status = StatusRecord { + core_count, + private_pool_size: 0, + system_pool_size: 0, + last_committed_timeslice: commit_timeslice.saturating_sub(1), + last_timeslice: Broker::::current_timeslice(), + }; + + // Assume Reservations to be filled for worst case + setup_reservations::(T::MaxReservedCores::get()); + + // Assume Leases to be filled for worst case + setup_leases::(T::MaxLeasedCores::get(), 1, 10); + + #[block] + { + Broker::::rotate_sale(sale, &config, &status); + } + + assert!(SaleInfo::::get().is_some()); + assert_last_event::( + Event::SaleInitialized { + sale_start: 2u32.into(), + leadin_length: 1u32.into(), + start_price: 20u32.into(), + regular_price: 10u32.into(), + region_begin: 4, + region_end: 7, + ideal_cores_sold: 0, + cores_offered: n + .saturating_sub(T::MaxReservedCores::get()) + .saturating_sub(T::MaxLeasedCores::get()) + .try_into() + .unwrap(), + } + .into(), + ); + } + + #[benchmark] + fn process_pool() { + let when = 10u32.into(); + let private_pool_size = 5u32.into(); + let system_pool_size = 4u32.into(); + + let config = new_config_record::(); + let commit_timeslice = Broker::::latest_timeslice_ready_to_commit(&config); + let mut status = StatusRecord { + core_count: 5u16.into(), + private_pool_size, + system_pool_size, + last_committed_timeslice: commit_timeslice.saturating_sub(1), + last_timeslice: Broker::::current_timeslice(), + }; + + #[block] + { + Broker::::process_pool(when, &mut status); + } + + assert!(InstaPoolHistory::::get(when).is_some()); + assert_last_event::( + Event::HistoryInitialized { when, private_pool_size, system_pool_size }.into(), + ); + } + + #[benchmark] + fn process_core_schedule() { + let timeslice = 10u32.into(); + let core = 5u16.into(); + let rc_begin = 1u32.into(); + + Workplan::::insert((timeslice, core), new_schedule()); + + #[block] + { + Broker::::process_core_schedule(timeslice, rc_begin, core); + } + + assert_eq!(Workload::::get(core).len(), CORE_MASK_BITS); + + let mut assignment: Vec<(CoreAssignment, PartsOf57600)> = vec![]; + for i in 0..CORE_MASK_BITS { + assignment.push((CoreAssignment::Task(i.try_into().unwrap()), 57600)); + } + assert_last_event::(Event::CoreAssigned { core, when: rc_begin, assignment }.into()); + } + + #[benchmark] + fn request_revenue_info_at() { + let current_timeslice = Broker::::current_timeslice(); + let rc_block = T::TimeslicePeriod::get() * current_timeslice.into(); + + #[block] + { + T::Coretime::request_revenue_info_at(rc_block); + } + } + + // Implements a test for each benchmark. Execute with: + // `cargo test -p pallet-broker --features runtime-benchmarks`. + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/frame/broker/src/core_mask.rs b/frame/broker/src/core_mask.rs new file mode 100644 index 0000000000000..b8d045077d828 --- /dev/null +++ b/frame/broker/src/core_mask.rs @@ -0,0 +1,227 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use codec::{Decode, Encode, MaxEncodedLen}; +use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; +use scale_info::TypeInfo; +use sp_core::RuntimeDebug; + +/// The number of bits in the `CoreMask`. +pub const CORE_MASK_BITS: usize = 80; + +// TODO: Use BitArr instead; for this, we'll need to ensure Codec is impl'ed for `BitArr`. +#[derive( + Encode, Decode, Default, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, +)] +pub struct CoreMask([u8; 10]); +impl CoreMask { + pub fn void() -> Self { + Self([0u8; 10]) + } + pub fn complete() -> Self { + Self([255u8; 10]) + } + pub fn is_void(&self) -> bool { + &self.0 == &[0u8; 10] + } + pub fn is_complete(&self) -> bool { + &self.0 == &[255u8; 10] + } + pub fn set(&mut self, i: u32) -> Self { + if i < 80 { + self.0[(i / 8) as usize] |= 128 >> (i % 8); + } + *self + } + pub fn clear(&mut self, i: u32) -> Self { + if i < 80 { + self.0[(i / 8) as usize] &= !(128 >> (i % 8)); + } + *self + } + pub fn count_zeros(&self) -> u32 { + self.0.iter().map(|i| i.count_zeros()).sum() + } + pub fn count_ones(&self) -> u32 { + self.0.iter().map(|i| i.count_ones()).sum() + } + pub fn from_chunk(from: u32, to: u32) -> Self { + let mut v = [0u8; 10]; + for i in (from.min(80) as usize)..(to.min(80) as usize) { + v[i / 8] |= 128 >> (i % 8); + } + Self(v) + } +} +impl From for CoreMask { + fn from(x: u128) -> Self { + let mut v = [0u8; 10]; + v.iter_mut().rev().fold(x, |a, i| { + *i = a as u8; + a >> 8 + }); + Self(v) + } +} +impl From for u128 { + fn from(x: CoreMask) -> Self { + x.0.into_iter().fold(0u128, |a, i| a << 8 | i as u128) + } +} +impl BitAnd for CoreMask { + type Output = Self; + fn bitand(mut self, rhs: Self) -> Self { + self.bitand_assign(rhs); + self + } +} +impl BitAndAssign for CoreMask { + fn bitand_assign(&mut self, rhs: Self) { + for i in 0..10 { + self.0[i].bitand_assign(rhs.0[i]); + } + } +} +impl BitOr for CoreMask { + type Output = Self; + fn bitor(mut self, rhs: Self) -> Self { + self.bitor_assign(rhs); + self + } +} +impl BitOrAssign for CoreMask { + fn bitor_assign(&mut self, rhs: Self) { + for i in 0..10 { + self.0[i].bitor_assign(rhs.0[i]); + } + } +} +impl BitXor for CoreMask { + type Output = Self; + fn bitxor(mut self, rhs: Self) -> Self { + self.bitxor_assign(rhs); + self + } +} +impl BitXorAssign for CoreMask { + fn bitxor_assign(&mut self, rhs: Self) { + for i in 0..10 { + self.0[i].bitxor_assign(rhs.0[i]); + } + } +} +impl Not for CoreMask { + type Output = Self; + fn not(self) -> Self { + let mut result = [0u8; 10]; + for i in 0..10 { + result[i] = self.0[i].not(); + } + Self(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn complete_works() { + assert_eq!(CoreMask::complete(), CoreMask([0xff; 10])); + assert!(CoreMask([0xff; 10]).is_complete()); + for i in 0..80 { + assert!(!CoreMask([0xff; 10]).clear(i).is_complete()); + } + } + + #[test] + fn void_works() { + assert_eq!(CoreMask::void(), CoreMask([0; 10])); + assert!(CoreMask([0; 10]).is_void()); + for i in 0..80 { + assert!(!(CoreMask([0; 10]).set(i).is_void())); + } + } + + #[test] + fn from_works() { + assert!(CoreMask::from(0xfffff_fffff_fffff_fffff).is_complete()); + assert_eq!( + CoreMask::from(0x12345_67890_abcde_f0123), + CoreMask([0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x01, 0x23]), + ); + } + + #[test] + fn into_works() { + assert_eq!(u128::from(CoreMask::complete()), 0xfffff_fffff_fffff_fffff); + assert_eq!( + 0x12345_67890_abcde_f0123u128, + CoreMask([0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x01, 0x23]).into(), + ); + } + + #[test] + fn chunk_works() { + assert_eq!(CoreMask::from_chunk(40, 60), CoreMask::from(0x00000_00000_fffff_00000),); + } + + #[test] + fn bit_or_works() { + assert_eq!( + CoreMask::from(0x02040_a0c0e_d0a0b_0ffff) | CoreMask::from(0x10305_0b0d0_0e0d0_e0000), + CoreMask::from(0x12345_abcde_deadb_effff), + ); + } + + #[test] + fn bit_or_assign_works() { + let mut a = CoreMask::from(0x02040_a0c0e_d0a0b_0ffff); + a |= CoreMask::from(0x10305_0b0d0_0e0d0_e0000); + assert_eq!(a, CoreMask::from(0x12345_abcde_deadb_effff)); + } + + #[test] + fn bit_and_works() { + assert_eq!( + CoreMask::from(0x00000_abcde_deadb_efff0) & CoreMask::from(0x02040_00000_d0a0b_0ff0f), + CoreMask::from(0x00000_00000_d0a0b_0ff00), + ); + } + + #[test] + fn bit_and_assign_works() { + let mut a = CoreMask::from(0x00000_abcde_deadb_efff0); + a &= CoreMask::from(0x02040_00000_d0a0b_0ff0f); + assert_eq!(a, CoreMask::from(0x00000_00000_d0a0b_0ff00)); + } + + #[test] + fn bit_xor_works() { + assert_eq!( + CoreMask::from(0x10010_10010_10010_10010) ^ CoreMask::from(0x01110_01110_01110_01110), + CoreMask::from(0x11100_11100_11100_11100), + ); + } + + #[test] + fn bit_xor_assign_works() { + let mut a = CoreMask::from(0x10010_10010_10010_10010); + a ^= CoreMask::from(0x01110_01110_01110_01110); + assert_eq!(a, CoreMask::from(0x11100_11100_11100_11100)); + } +} diff --git a/frame/broker/src/coretime_interface.rs b/frame/broker/src/coretime_interface.rs new file mode 100644 index 0000000000000..fec40b9fdd7b3 --- /dev/null +++ b/frame/broker/src/coretime_interface.rs @@ -0,0 +1,168 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![deny(missing_docs)] + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::Parameter; +use scale_info::TypeInfo; +use sp_arithmetic::traits::AtLeast32BitUnsigned; +use sp_core::RuntimeDebug; +use sp_std::{fmt::Debug, vec::Vec}; + +/// Index of a Polkadot Core. +pub type CoreIndex = u16; + +/// A Task Id. In general this is called a ParachainId. +pub type TaskId = u32; + +/// Fraction expressed as a nominator with an assumed denominator of 57,600. +pub type PartsOf57600 = u16; + +/// An element to which a core can be assigned. +#[derive( + Encode, Decode, Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, MaxEncodedLen, +)] +pub enum CoreAssignment { + /// Core need not be used for anything. + Idle, + /// Core should be used for the Instantaneous Coretime Pool. + Pool, + /// Core should be used to process the given task. + Task(TaskId), +} + +/// Type able to accept Coretime scheduling instructions and provide certain usage information. +/// Generally implemented by the Relay-chain or some means of communicating with it. +/// +/// The trait representation of RFC#5 ``. +pub trait CoretimeInterface { + /// A (Relay-chain-side) account ID. + type AccountId: Parameter; + + /// A (Relay-chain-side) balance. + type Balance: AtLeast32BitUnsigned; + + /// A (Relay-chain-side) block number. + type BlockNumber: AtLeast32BitUnsigned + + Copy + + TypeInfo + + Encode + + Decode + + MaxEncodedLen + + Debug; + + /// Return the latest block number on the Relay-chain. + fn latest() -> Self::BlockNumber; + + /// Requests the Relay-chain to alter the number of schedulable cores to `count`. Under normal + /// operation, the Relay-chain SHOULD send a `notify_core_count(count)` message back. + fn request_core_count(count: CoreIndex); + + /// Requests that the Relay-chain send a `notify_revenue` message back at or soon after + /// Relay-chain block number `when` whose `until` parameter is equal to `when`. + /// + /// `when` may never be greater than the result of `Self::latest()`. + /// The period in to the past which `when` is allowed to be may be limited; if so the limit + /// should be understood on a channel outside of this proposal. In the case that the request + /// cannot be serviced because `when` is too old a block then a `notify_revenue` message must + /// still be returned, but its `revenue` field may be `None`. + fn request_revenue_info_at(when: Self::BlockNumber); + + /// Instructs the Relay-chain to add the `amount` of DOT to the Instantaneous Coretime Market + /// Credit account of `who`. + /// + /// It is expected that Instantaneous Coretime Market Credit on the Relay-chain is NOT + /// transferrable and only redeemable when used to assign cores in the Instantaneous Coretime + /// Pool. + fn credit_account(who: Self::AccountId, amount: Self::Balance); + + /// Instructs the Relay-chain to ensure that the core indexed as `core` is utilised for a number + /// of assignments in specific ratios given by `assignment` starting as soon after `begin` as + /// possible. Core assignments take the form of a `CoreAssignment` value which can either task + /// the core to a `ParaId` value or indicate that the core should be used in the Instantaneous + /// Pool. Each assignment comes with a ratio value, represented as the numerator of the fraction + /// with a denominator of 57,600. + /// + /// If `end_hint` is `Some` and the inner is greater than the current block number, then the + /// Relay-chain should optimize in the expectation of receiving a new `assign_core(core, ...)` + /// message at or prior to the block number of the inner value. Specific functionality should + /// remain unchanged regardless of the `end_hint` value. + fn assign_core( + core: CoreIndex, + begin: Self::BlockNumber, + assignment: Vec<(CoreAssignment, PartsOf57600)>, + end_hint: Option, + ); + + /// Indicate that from this block onwards, the range of acceptable values of the `core` + /// parameter of `assign_core` message is `[0, count)`. `assign_core` will be a no-op if + /// provided with a value for `core` outside of this range. + fn check_notify_core_count() -> Option; + + /// Provide the amount of revenue accumulated from Instantaneous Coretime Sales from Relay-chain + /// block number `last_until` to `until`, not including `until` itself. `last_until` is defined + /// as being the `until` argument of the last `notify_revenue` message sent, or zero for the + /// first call. If `revenue` is `None`, this indicates that the information is no longer + /// available. + /// + /// This explicitly disregards the possibility of multiple parachains requesting and being + /// notified of revenue information. The Relay-chain must be configured to ensure that only a + /// single revenue information destination exists. + fn check_notify_revenue_info() -> Option<(Self::BlockNumber, Self::Balance)>; + + /// Ensure that core count is updated to the provided value. + /// + /// This is only used for benchmarking. + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_core_count(count: u16); + + /// Ensure that revenue information is updated to the provided value. + /// + /// This is only used for benchmarking. + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_revenue_info(when: Self::BlockNumber, revenue: Self::Balance); +} + +impl CoretimeInterface for () { + type AccountId = (); + type Balance = u64; + type BlockNumber = u32; + fn latest() -> Self::BlockNumber { + 0 + } + fn request_core_count(_count: CoreIndex) {} + fn request_revenue_info_at(_when: Self::BlockNumber) {} + fn credit_account(_who: Self::AccountId, _amount: Self::Balance) {} + fn assign_core( + _core: CoreIndex, + _begin: Self::BlockNumber, + _assignment: Vec<(CoreAssignment, PartsOf57600)>, + _end_hint: Option, + ) { + } + fn check_notify_core_count() -> Option { + None + } + fn check_notify_revenue_info() -> Option<(Self::BlockNumber, Self::Balance)> { + None + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_core_count(_count: u16) {} + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_revenue_info(_when: Self::BlockNumber, _revenue: Self::Balance) {} +} diff --git a/frame/broker/src/dispatchable_impls.rs b/frame/broker/src/dispatchable_impls.rs new file mode 100644 index 0000000000000..7c1d5a786b7cf --- /dev/null +++ b/frame/broker/src/dispatchable_impls.rs @@ -0,0 +1,436 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use frame_support::{ + pallet_prelude::{DispatchResult, *}, + traits::{fungible::Mutate, tokens::Preservation::Expendable, DefensiveResult}, +}; +use sp_arithmetic::traits::{CheckedDiv, Saturating, Zero}; +use sp_runtime::traits::Convert; +use CompletionStatus::{Complete, Partial}; + +impl Pallet { + pub(crate) fn do_configure(config: ConfigRecordOf) -> DispatchResult { + config.validate().map_err(|()| Error::::InvalidConfig)?; + Configuration::::put(config); + Ok(()) + } + + pub(crate) fn do_request_core_count(core_count: CoreIndex) -> DispatchResult { + T::Coretime::request_core_count(core_count); + Self::deposit_event(Event::::CoreCountRequested { core_count }); + Ok(()) + } + + pub(crate) fn do_reserve(workload: Schedule) -> DispatchResult { + let mut r = Reservations::::get(); + let index = r.len() as u32; + r.try_push(workload.clone()).map_err(|_| Error::::TooManyReservations)?; + Reservations::::put(r); + Self::deposit_event(Event::::ReservationMade { index, workload }); + Ok(()) + } + + pub(crate) fn do_unreserve(index: u32) -> DispatchResult { + let mut r = Reservations::::get(); + ensure!(index < r.len() as u32, Error::::UnknownReservation); + let workload = r.remove(index as usize); + Reservations::::put(r); + Self::deposit_event(Event::::ReservationCancelled { index, workload }); + Ok(()) + } + + pub(crate) fn do_set_lease(task: TaskId, until: Timeslice) -> DispatchResult { + let mut r = Leases::::get(); + ensure!(until > Self::current_timeslice(), Error::::AlreadyExpired); + r.try_push(LeaseRecordItem { until, task }) + .map_err(|_| Error::::TooManyLeases)?; + Leases::::put(r); + Self::deposit_event(Event::::Leased { until, task }); + Ok(()) + } + + pub(crate) fn do_start_sales(price: BalanceOf, core_count: CoreIndex) -> DispatchResult { + let config = Configuration::::get().ok_or(Error::::Uninitialized)?; + let commit_timeslice = Self::latest_timeslice_ready_to_commit(&config); + let status = StatusRecord { + core_count, + private_pool_size: 0, + system_pool_size: 0, + last_committed_timeslice: commit_timeslice.saturating_sub(1), + last_timeslice: Self::current_timeslice(), + }; + let now = frame_system::Pallet::::block_number(); + let dummy_sale = SaleInfoRecord { + sale_start: now, + leadin_length: Zero::zero(), + price, + sellout_price: None, + region_begin: commit_timeslice, + region_end: commit_timeslice.saturating_add(config.region_length), + first_core: 0, + ideal_cores_sold: 0, + cores_offered: 0, + cores_sold: 0, + }; + Self::deposit_event(Event::::SalesStarted { price, core_count }); + Self::rotate_sale(dummy_sale, &config, &status); + Status::::put(&status); + Ok(()) + } + + pub(crate) fn do_purchase( + who: T::AccountId, + price_limit: BalanceOf, + ) -> Result { + let status = Status::::get().ok_or(Error::::Uninitialized)?; + let mut sale = SaleInfo::::get().ok_or(Error::::NoSales)?; + ensure!(sale.first_core < status.core_count, Error::::Unavailable); + ensure!(sale.cores_sold < sale.cores_offered, Error::::SoldOut); + let now = frame_system::Pallet::::block_number(); + ensure!(now > sale.sale_start, Error::::TooEarly); + let price = Self::sale_price(&sale, now); + ensure!(price_limit >= price, Error::::Overpriced); + + Self::charge(&who, price)?; + let core = sale.first_core.saturating_add(sale.cores_sold); + sale.cores_sold.saturating_inc(); + if sale.cores_sold <= sale.ideal_cores_sold || sale.sellout_price.is_none() { + sale.sellout_price = Some(price); + } + SaleInfo::::put(&sale); + let id = Self::issue(core, sale.region_begin, sale.region_end, who.clone(), Some(price)); + let duration = sale.region_end.saturating_sub(sale.region_begin); + Self::deposit_event(Event::Purchased { who, region_id: id, price, duration }); + Ok(id) + } + + /// Must be called on a core in `AllowedRenewals` whose value is a timeslice equal to the + /// current sale status's `region_end`. + pub(crate) fn do_renew(who: T::AccountId, core: CoreIndex) -> Result { + let config = Configuration::::get().ok_or(Error::::Uninitialized)?; + let status = Status::::get().ok_or(Error::::Uninitialized)?; + let mut sale = SaleInfo::::get().ok_or(Error::::NoSales)?; + ensure!(sale.first_core < status.core_count, Error::::Unavailable); + ensure!(sale.cores_sold < sale.cores_offered, Error::::SoldOut); + + let renewal_id = AllowedRenewalId { core, when: sale.region_begin }; + let record = AllowedRenewals::::get(renewal_id).ok_or(Error::::NotAllowed)?; + let workload = + record.completion.drain_complete().ok_or(Error::::IncompleteAssignment)?; + + let old_core = core; + let core = sale.first_core.saturating_add(sale.cores_sold); + Self::charge(&who, record.price)?; + Self::deposit_event(Event::Renewed { + who, + old_core, + core, + price: record.price, + begin: sale.region_begin, + duration: sale.region_end.saturating_sub(sale.region_begin), + workload: workload.clone(), + }); + + sale.cores_sold.saturating_inc(); + + Workplan::::insert((sale.region_begin, core), &workload); + + let begin = sale.region_end; + let price_cap = record.price + config.renewal_bump * record.price; + let now = frame_system::Pallet::::block_number(); + let price = Self::sale_price(&sale, now).min(price_cap); + let new_record = AllowedRenewalRecord { price, completion: Complete(workload) }; + AllowedRenewals::::remove(renewal_id); + AllowedRenewals::::insert(AllowedRenewalId { core, when: begin }, &new_record); + SaleInfo::::put(&sale); + if let Some(workload) = new_record.completion.drain_complete() { + Self::deposit_event(Event::Renewable { core, price, begin, workload }); + } + Ok(core) + } + + pub(crate) fn do_transfer( + region_id: RegionId, + maybe_check_owner: Option, + new_owner: T::AccountId, + ) -> Result<(), Error> { + let mut region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; + + if let Some(check_owner) = maybe_check_owner { + ensure!(check_owner == region.owner, Error::::NotOwner); + } + + let old_owner = region.owner; + region.owner = new_owner; + Regions::::insert(®ion_id, ®ion); + let duration = region.end.saturating_sub(region_id.begin); + Self::deposit_event(Event::Transferred { + region_id, + old_owner, + owner: region.owner, + duration, + }); + + Ok(()) + } + + pub(crate) fn do_partition( + region_id: RegionId, + maybe_check_owner: Option, + pivot_offset: Timeslice, + ) -> Result<(RegionId, RegionId), Error> { + let mut region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; + + if let Some(check_owner) = maybe_check_owner { + ensure!(check_owner == region.owner, Error::::NotOwner); + } + let pivot = region_id.begin.saturating_add(pivot_offset); + ensure!(pivot < region.end, Error::::PivotTooLate); + ensure!(pivot > region_id.begin, Error::::PivotTooEarly); + + region.paid = None; + let new_region_ids = (region_id, RegionId { begin: pivot, ..region_id }); + + Regions::::insert(&new_region_ids.0, &RegionRecord { end: pivot, ..region.clone() }); + Regions::::insert(&new_region_ids.1, ®ion); + Self::deposit_event(Event::Partitioned { old_region_id: region_id, new_region_ids }); + + Ok(new_region_ids) + } + + pub(crate) fn do_interlace( + region_id: RegionId, + maybe_check_owner: Option, + pivot: CoreMask, + ) -> Result<(RegionId, RegionId), Error> { + let region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; + + if let Some(check_owner) = maybe_check_owner { + ensure!(check_owner == region.owner, Error::::NotOwner); + } + + ensure!((pivot & !region_id.mask).is_void(), Error::::ExteriorPivot); + ensure!(!pivot.is_void(), Error::::VoidPivot); + ensure!(pivot != region_id.mask, Error::::CompletePivot); + + let one = RegionId { mask: pivot, ..region_id }; + Regions::::insert(&one, ®ion); + let other = RegionId { mask: region_id.mask ^ pivot, ..region_id }; + Regions::::insert(&other, ®ion); + + let new_region_ids = (one, other); + Self::deposit_event(Event::Interlaced { old_region_id: region_id, new_region_ids }); + Ok(new_region_ids) + } + + pub(crate) fn do_assign( + region_id: RegionId, + maybe_check_owner: Option, + target: TaskId, + finality: Finality, + ) -> Result<(), Error> { + let config = Configuration::::get().ok_or(Error::::Uninitialized)?; + if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? { + let workplan_key = (region_id.begin, region_id.core); + let mut workplan = Workplan::::get(&workplan_key).unwrap_or_default(); + // Ensure no previous allocations exist. + workplan.retain(|i| (i.mask & region_id.mask).is_void()); + if workplan + .try_push(ScheduleItem { + mask: region_id.mask, + assignment: CoreAssignment::Task(target), + }) + .is_ok() + { + Workplan::::insert(&workplan_key, &workplan); + } + + let duration = region.end.saturating_sub(region_id.begin); + if duration == config.region_length && finality == Finality::Final { + if let Some(price) = region.paid { + let renewal_id = AllowedRenewalId { core: region_id.core, when: region.end }; + let assigned = match AllowedRenewals::::get(renewal_id) { + Some(AllowedRenewalRecord { completion: Partial(w), price: p }) + if price == p => + w, + _ => CoreMask::void(), + } | region_id.mask; + let workload = + if assigned.is_complete() { Complete(workplan) } else { Partial(assigned) }; + let record = AllowedRenewalRecord { price, completion: workload }; + AllowedRenewals::::insert(&renewal_id, &record); + if let Some(workload) = record.completion.drain_complete() { + Self::deposit_event(Event::Renewable { + core: region_id.core, + price, + begin: region.end, + workload, + }); + } + } + } + Self::deposit_event(Event::Assigned { region_id, task: target, duration }); + } + Ok(()) + } + + pub(crate) fn do_pool( + region_id: RegionId, + maybe_check_owner: Option, + payee: T::AccountId, + finality: Finality, + ) -> Result<(), Error> { + if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? { + let workplan_key = (region_id.begin, region_id.core); + let mut workplan = Workplan::::get(&workplan_key).unwrap_or_default(); + let duration = region.end.saturating_sub(region_id.begin); + if workplan + .try_push(ScheduleItem { mask: region_id.mask, assignment: CoreAssignment::Pool }) + .is_ok() + { + Workplan::::insert(&workplan_key, &workplan); + let size = region_id.mask.count_ones() as i32; + InstaPoolIo::::mutate(region_id.begin, |a| a.private.saturating_accrue(size)); + InstaPoolIo::::mutate(region.end, |a| a.private.saturating_reduce(size)); + let record = ContributionRecord { length: duration, payee }; + InstaPoolContribution::::insert(®ion_id, record); + } + + Self::deposit_event(Event::Pooled { region_id, duration }); + } + Ok(()) + } + + pub(crate) fn do_claim_revenue( + mut region: RegionId, + max_timeslices: Timeslice, + ) -> DispatchResult { + let mut contribution = + InstaPoolContribution::::take(region).ok_or(Error::::UnknownContribution)?; + let contributed_parts = region.mask.count_ones(); + + Self::deposit_event(Event::RevenueClaimBegun { region, max_timeslices }); + + let mut payout = BalanceOf::::zero(); + let last = region.begin + contribution.length.min(max_timeslices); + for r in region.begin..last { + region.begin = r + 1; + contribution.length.saturating_dec(); + + let Some(mut pool_record) = InstaPoolHistory::::get(r) else { + continue; + }; + let Some(total_payout) = pool_record.maybe_payout else { + break; + }; + let p = total_payout + .saturating_mul(contributed_parts.into()) + .checked_div(&pool_record.private_contributions.into()) + .unwrap_or_default(); + + payout.saturating_accrue(p); + pool_record.private_contributions.saturating_reduce(contributed_parts); + + let remaining_payout = total_payout.saturating_sub(p); + if !remaining_payout.is_zero() && pool_record.private_contributions > 0 { + pool_record.maybe_payout = Some(remaining_payout); + InstaPoolHistory::::insert(r, &pool_record); + } else { + InstaPoolHistory::::remove(r); + } + if !p.is_zero() { + Self::deposit_event(Event::RevenueClaimItem { when: r, amount: p }); + } + } + + if contribution.length > 0 { + InstaPoolContribution::::insert(region, &contribution); + } + T::Currency::transfer(&Self::account_id(), &contribution.payee, payout, Expendable) + .defensive_ok(); + let next = if last < region.begin + contribution.length { Some(region) } else { None }; + Self::deposit_event(Event::RevenueClaimPaid { + who: contribution.payee, + amount: payout, + next, + }); + Ok(()) + } + + pub(crate) fn do_purchase_credit( + who: T::AccountId, + amount: BalanceOf, + beneficiary: RelayAccountIdOf, + ) -> DispatchResult { + T::Currency::transfer(&who, &Self::account_id(), amount, Expendable)?; + let rc_amount = T::ConvertBalance::convert(amount); + T::Coretime::credit_account(beneficiary.clone(), rc_amount); + Self::deposit_event(Event::::CreditPurchased { who, beneficiary, amount }); + Ok(()) + } + + pub(crate) fn do_drop_region(region_id: RegionId) -> DispatchResult { + let status = Status::::get().ok_or(Error::::Uninitialized)?; + let region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; + ensure!(status.last_committed_timeslice >= region.end, Error::::StillValid); + + Regions::::remove(®ion_id); + let duration = region.end.saturating_sub(region_id.begin); + Self::deposit_event(Event::RegionDropped { region_id, duration }); + Ok(()) + } + + pub(crate) fn do_drop_contribution(region_id: RegionId) -> DispatchResult { + let config = Configuration::::get().ok_or(Error::::Uninitialized)?; + let status = Status::::get().ok_or(Error::::Uninitialized)?; + let contrib = + InstaPoolContribution::::get(®ion_id).ok_or(Error::::UnknownContribution)?; + let end = region_id.begin.saturating_add(contrib.length); + ensure!( + status.last_timeslice >= end.saturating_add(config.contribution_timeout), + Error::::StillValid + ); + InstaPoolContribution::::remove(region_id); + Self::deposit_event(Event::ContributionDropped { region_id }); + Ok(()) + } + + pub(crate) fn do_drop_history(when: Timeslice) -> DispatchResult { + let config = Configuration::::get().ok_or(Error::::Uninitialized)?; + let status = Status::::get().ok_or(Error::::Uninitialized)?; + ensure!(status.last_timeslice > when + config.contribution_timeout, Error::::StillValid); + let record = InstaPoolHistory::::take(when).ok_or(Error::::NoHistory)?; + if let Some(payout) = record.maybe_payout { + let _ = Self::charge(&Self::account_id(), payout); + } + let revenue = record.maybe_payout.unwrap_or_default(); + Self::deposit_event(Event::HistoryDropped { when, revenue }); + Ok(()) + } + + pub(crate) fn do_drop_renewal(core: CoreIndex, when: Timeslice) -> DispatchResult { + let status = Status::::get().ok_or(Error::::Uninitialized)?; + ensure!(status.last_committed_timeslice >= when, Error::::StillValid); + let id = AllowedRenewalId { core, when }; + ensure!(AllowedRenewals::::contains_key(id), Error::::UnknownRenewal); + AllowedRenewals::::remove(id); + Self::deposit_event(Event::AllowedRenewalDropped { core, when }); + Ok(()) + } +} diff --git a/frame/broker/src/lib.rs b/frame/broker/src/lib.rs new file mode 100644 index 0000000000000..4abd041f5f397 --- /dev/null +++ b/frame/broker/src/lib.rs @@ -0,0 +1,784 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] +#![doc = include_str!("../README.md")] + +pub use pallet::*; + +mod adapt_price; +mod benchmarking; +mod core_mask; +mod coretime_interface; +mod dispatchable_impls; +#[cfg(test)] +mod mock; +mod nonfungible_impl; +#[cfg(test)] +mod test_fungibles; +#[cfg(test)] +mod tests; +mod tick_impls; +mod types; +mod utility_impls; + +pub mod weights; +pub use weights::WeightInfo; + +pub use adapt_price::*; +pub use core_mask::*; +pub use coretime_interface::*; +pub use nonfungible_impl::*; +pub use types::*; +pub use utility_impls::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + pallet_prelude::{DispatchResult, DispatchResultWithPostInfo, *}, + traits::{ + fungible::{Balanced, Credit, Mutate}, + EnsureOrigin, OnUnbalanced, + }, + PalletId, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::{Convert, ConvertBack}; + use sp_std::vec::Vec; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Weight information for all calls of this pallet. + type WeightInfo: WeightInfo; + + /// Currency used to pay for Coretime. + type Currency: Mutate + Balanced; + + /// The origin test needed for administrating this pallet. + type AdminOrigin: EnsureOrigin; + + /// What to do with any revenues collected from the sale of Coretime. + type OnRevenue: OnUnbalanced>; + + /// Relay chain's Coretime API used to interact with and instruct the low-level scheduling + /// system. + type Coretime: CoretimeInterface; + + /// The algorithm to determine the next price on the basis of market performance. + type PriceAdapter: AdaptPrice; + + /// Reversible conversion from local balance to Relay-chain balance. This will typically be + /// the `Identity`, but provided just in case the chains use different representations. + type ConvertBalance: Convert, RelayBalanceOf> + + ConvertBack, RelayBalanceOf>; + + /// Identifier from which the internal Pot is generated. + #[pallet::constant] + type PalletId: Get; + + /// Number of Relay-chain blocks per timeslice. + #[pallet::constant] + type TimeslicePeriod: Get>; + + /// Maximum number of legacy leases. + #[pallet::constant] + type MaxLeasedCores: Get; + + /// Maximum number of system cores. + #[pallet::constant] + type MaxReservedCores: Get; + } + + /// The current configuration of this pallet. + #[pallet::storage] + pub type Configuration = StorageValue<_, ConfigRecordOf, OptionQuery>; + + /// The Polkadot Core reservations (generally tasked with the maintenance of System Chains). + #[pallet::storage] + pub type Reservations = StorageValue<_, ReservationsRecordOf, ValueQuery>; + + /// The Polkadot Core legacy leases. + #[pallet::storage] + pub type Leases = StorageValue<_, LeasesRecordOf, ValueQuery>; + + /// The current status of miscellaneous subsystems of this pallet. + #[pallet::storage] + pub type Status = StorageValue<_, StatusRecord, OptionQuery>; + + /// The details of the current sale, including its properties and status. + #[pallet::storage] + pub type SaleInfo = StorageValue<_, SaleInfoRecordOf, OptionQuery>; + + /// Records of allowed renewals. + #[pallet::storage] + pub type AllowedRenewals = + StorageMap<_, Twox64Concat, AllowedRenewalId, AllowedRenewalRecordOf, OptionQuery>; + + /// The current (unassigned) Regions. + #[pallet::storage] + pub type Regions = StorageMap<_, Blake2_128Concat, RegionId, RegionRecordOf, OptionQuery>; + + /// The work we plan on having each core do at a particular time in the future. + #[pallet::storage] + pub type Workplan = + StorageMap<_, Twox64Concat, (Timeslice, CoreIndex), Schedule, OptionQuery>; + + /// The current workload of each core. This gets updated with workplan as timeslices pass. + #[pallet::storage] + pub type Workload = StorageMap<_, Twox64Concat, CoreIndex, Schedule, ValueQuery>; + + /// Record of a single contribution to the Instantaneous Coretime Pool. + #[pallet::storage] + pub type InstaPoolContribution = + StorageMap<_, Blake2_128Concat, RegionId, ContributionRecordOf, OptionQuery>; + + /// Record of Coretime entering or leaving the Instantaneous Coretime Pool. + #[pallet::storage] + pub type InstaPoolIo = StorageMap<_, Blake2_128Concat, Timeslice, PoolIoRecord, ValueQuery>; + + /// Total InstaPool rewards for each Timeslice and the number of core parts which contributed. + #[pallet::storage] + pub type InstaPoolHistory = + StorageMap<_, Blake2_128Concat, Timeslice, InstaPoolHistoryRecordOf>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A Region of Bulk Coretime has been purchased. + Purchased { + /// The identity of the purchaser. + who: T::AccountId, + /// The identity of the Region. + region_id: RegionId, + /// The price paid for this Region. + price: BalanceOf, + /// The duration of the Region. + duration: Timeslice, + }, + /// The workload of a core has become renewable. + Renewable { + /// The core whose workload can be renewed. + core: CoreIndex, + /// The price at which the workload can be renewed. + price: BalanceOf, + /// The time at which the workload would recommence of this renewal. The call to renew + /// cannot happen before the beginning of the interlude prior to the sale for regions + /// which begin at this time. + begin: Timeslice, + /// The actual workload which can be renewed. + workload: Schedule, + }, + /// A workload has been renewed. + Renewed { + /// The identity of the renewer. + who: T::AccountId, + /// The price paid for this renewal. + price: BalanceOf, + /// The index of the core on which the `workload` was previously scheduled. + old_core: CoreIndex, + /// The index of the core on which the renewed `workload` has been scheduled. + core: CoreIndex, + /// The time at which the `workload` will begin on the `core`. + begin: Timeslice, + /// The number of timeslices for which this `workload` is newly scheduled. + duration: Timeslice, + /// The workload which was renewed. + workload: Schedule, + }, + /// Ownership of a Region has been transferred. + Transferred { + /// The Region which has been transferred. + region_id: RegionId, + /// The duration of the Region. + duration: Timeslice, + /// The old owner of the Region. + old_owner: T::AccountId, + /// The new owner of the Region. + owner: T::AccountId, + }, + /// A Region has been split into two non-overlapping Regions. + Partitioned { + /// The Region which was split. + old_region_id: RegionId, + /// The new Regions into which it became. + new_region_ids: (RegionId, RegionId), + }, + /// A Region has been converted into two overlapping Regions each of lesser regularity. + Interlaced { + /// The Region which was interlaced. + old_region_id: RegionId, + /// The new Regions into which it became. + new_region_ids: (RegionId, RegionId), + }, + /// A Region has been assigned to a particular task. + Assigned { + /// The Region which was assigned. + region_id: RegionId, + /// The duration of the assignment. + duration: Timeslice, + /// The task to which the Region was assigned. + task: TaskId, + }, + /// A Region has been added to the Instantaneous Coretime Pool. + Pooled { + /// The Region which was added to the Instantaneous Coretime Pool. + region_id: RegionId, + /// The duration of the Region. + duration: Timeslice, + }, + /// A new number of cores has been requested. + CoreCountRequested { + /// The number of cores requested. + core_count: CoreIndex, + }, + /// The number of cores available for scheduling has changed. + CoreCountChanged { + /// The new number of cores available for scheduling. + core_count: CoreIndex, + }, + /// There is a new reservation for a workload. + ReservationMade { + /// The index of the reservation. + index: u32, + /// The workload of the reservation. + workload: Schedule, + }, + /// A reservation for a workload has been cancelled. + ReservationCancelled { + /// The index of the reservation which was cancelled. + index: u32, + /// The workload of the now cancelled reservation. + workload: Schedule, + }, + /// A new sale has been initialized. + SaleInitialized { + /// The local block number at which the sale will/did start. + sale_start: BlockNumberFor, + /// The length in blocks of the Leadin Period (where the price is decreasing). + leadin_length: BlockNumberFor, + /// The price of Bulk Coretime at the beginning of the Leadin Period. + start_price: BalanceOf, + /// The price of Bulk Coretime after the Leadin Period. + regular_price: BalanceOf, + /// The first timeslice of the Regions which are being sold in this sale. + region_begin: Timeslice, + /// The timeslice on which the Regions which are being sold in the sale terminate. + /// (i.e. One after the last timeslice which the Regions control.) + region_end: Timeslice, + /// The number of cores we want to sell, ideally. Selling this amount would result in + /// no change to the price for the next sale. + ideal_cores_sold: CoreIndex, + /// Number of cores which are/have been offered for sale. + cores_offered: CoreIndex, + }, + /// A new lease has been created. + Leased { + /// The task to which a core will be assigned. + task: TaskId, + /// The timeslice contained in the sale period after which this lease will + /// self-terminate (and therefore the earliest timeslice at which the lease may no + /// longer apply). + until: Timeslice, + }, + /// A lease is about to end. + LeaseEnding { + /// The task to which a core was assigned. + task: TaskId, + /// The timeslice at which the task will no longer be scheduled. + when: Timeslice, + }, + /// The sale rotation has been started and a new sale is imminent. + SalesStarted { + /// The nominal price of an Region of Bulk Coretime. + price: BalanceOf, + /// The maximum number of cores which this pallet will attempt to assign. + core_count: CoreIndex, + }, + /// The act of claiming revenue has begun. + RevenueClaimBegun { + /// The region to be claimed for. + region: RegionId, + /// The maximum number of timeslices which should be searched for claimed. + max_timeslices: Timeslice, + }, + /// A particular timeslice has a non-zero claim. + RevenueClaimItem { + /// The timeslice whose claim is being processed. + when: Timeslice, + /// The amount which was claimed at this timeslice. + amount: BalanceOf, + }, + /// A revenue claim has (possibly only in part) been paid. + RevenueClaimPaid { + /// The account to whom revenue has been paid. + who: T::AccountId, + /// The total amount of revenue claimed and paid. + amount: BalanceOf, + /// The next region which should be claimed for the continuation of this contribution. + next: Option, + }, + /// Some Instantaneous Coretime Pool credit has been purchased. + CreditPurchased { + /// The account which purchased the credit. + who: T::AccountId, + /// The Relay-chain account to which the credit will be made. + beneficiary: RelayAccountIdOf, + /// The amount of credit purchased. + amount: BalanceOf, + }, + /// A Region has been dropped due to being out of date. + RegionDropped { + /// The Region which no longer exists. + region_id: RegionId, + /// The duration of the Region. + duration: Timeslice, + }, + /// Some historical Instantaneous Core Pool contribution record has been dropped. + ContributionDropped { + /// The Region whose contribution is no longer exists. + region_id: RegionId, + }, + /// Some historical Instantaneous Core Pool payment record has been initialized. + HistoryInitialized { + /// The timeslice whose history has been initialized. + when: Timeslice, + /// The amount of privately contributed Coretime to the Instantaneous Coretime Pool. + private_pool_size: CoreMaskBitCount, + /// The amount of Coretime contributed to the Instantaneous Coretime Pool by the + /// Polkadot System. + system_pool_size: CoreMaskBitCount, + }, + /// Some historical Instantaneous Core Pool payment record has been dropped. + HistoryDropped { + /// The timeslice whose history is no longer available. + when: Timeslice, + /// The amount of revenue the system has taken. + revenue: BalanceOf, + }, + /// Some historical Instantaneous Core Pool payment record has been ignored because the + /// timeslice was already known. Governance may need to intervene. + HistoryIgnored { + /// The timeslice whose history is was ignored. + when: Timeslice, + /// The amount of revenue which was ignored. + revenue: BalanceOf, + }, + /// Some historical Instantaneous Core Pool Revenue is ready for payout claims. + ClaimsReady { + /// The timeslice whose history is available. + when: Timeslice, + /// The amount of revenue the Polkadot System has already taken. + system_payout: BalanceOf, + /// The total amount of revenue remaining to be claimed. + private_payout: BalanceOf, + }, + /// A Core has been assigned to one or more tasks and/or the Pool on the Relay-chain. + CoreAssigned { + /// The index of the Core which has been assigned. + core: CoreIndex, + /// The Relay-chain block at which this assignment should take effect. + when: RelayBlockNumberOf, + /// The workload to be done on the Core. + assignment: Vec<(CoreAssignment, PartsOf57600)>, + }, + /// Some historical Instantaneous Core Pool payment record has been dropped. + AllowedRenewalDropped { + /// The timeslice whose renewal is no longer available. + when: Timeslice, + /// The core whose workload is no longer available to be renewed for `when`. + core: CoreIndex, + }, + } + + #[pallet::error] + #[derive(PartialEq)] + pub enum Error { + /// The given region identity is not known. + UnknownRegion, + /// The owner of the region is not the origin. + NotOwner, + /// The pivot point of the partition at or after the end of the region. + PivotTooLate, + /// The pivot point of the partition at the beginning of the region. + PivotTooEarly, + /// The pivot mask for the interlacing is not contained within the region's interlace mask. + ExteriorPivot, + /// The pivot mask for the interlacing is void (and therefore unschedulable). + VoidPivot, + /// The pivot mask for the interlacing is complete (and therefore not a strict subset). + CompletePivot, + /// The workplan of the pallet's state is invalid. This indicates a state corruption. + CorruptWorkplan, + /// There is no sale happening currently. + NoSales, + /// The price limit is exceeded. + Overpriced, + /// There are no cores available. + Unavailable, + /// The sale limit has been reached. + SoldOut, + /// The renewal operation is not valid at the current time (it may become valid in the next + /// sale). + WrongTime, + /// Invalid attempt to renew. + NotAllowed, + /// This pallet has not yet been initialized. + Uninitialized, + /// The purchase cannot happen yet as the sale period is yet to begin. + TooEarly, + /// There is no work to be done. + NothingToDo, + /// The maximum amount of reservations has already been reached. + TooManyReservations, + /// The maximum amount of leases has already been reached. + TooManyLeases, + /// The revenue for the Instantaneous Core Sales of this period is not (yet) known and thus + /// this operation cannot proceed. + UnknownRevenue, + /// The identified contribution to the Instantaneous Core Pool is unknown. + UnknownContribution, + /// The workload assigned for renewal is incomplete. This is unexpected and indicates a + /// logic error. + IncompleteAssignment, + /// An item cannot be dropped because it is still valid. + StillValid, + /// The history item does not exist. + NoHistory, + /// No reservation of the given index exists. + UnknownReservation, + /// The renewal record cannot be found. + UnknownRenewal, + /// The lease expiry time has already passed. + AlreadyExpired, + /// The configuration could not be applied because it is invalid. + InvalidConfig, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_now: BlockNumberFor) -> Weight { + Self::do_tick() + } + } + + #[pallet::call(weight(::WeightInfo))] + impl Pallet { + /// Configure the pallet. + /// + /// - `origin`: Must be Root or pass `AdminOrigin`. + /// - `config`: The configuration for this pallet. + #[pallet::call_index(0)] + pub fn configure( + origin: OriginFor, + config: ConfigRecordOf, + ) -> DispatchResultWithPostInfo { + T::AdminOrigin::ensure_origin_or_root(origin)?; + Self::do_configure(config)?; + Ok(Pays::No.into()) + } + + /// Reserve a core for a workload. + /// + /// - `origin`: Must be Root or pass `AdminOrigin`. + /// - `workload`: The workload which should be permanently placed on a core. + #[pallet::call_index(1)] + pub fn reserve(origin: OriginFor, workload: Schedule) -> DispatchResultWithPostInfo { + T::AdminOrigin::ensure_origin_or_root(origin)?; + Self::do_reserve(workload)?; + Ok(Pays::No.into()) + } + + /// Cancel a reservation for a workload. + /// + /// - `origin`: Must be Root or pass `AdminOrigin`. + /// - `item_index`: The index of the reservation. Usually this will also be the index of the + /// core on which the reservation has been scheduled. However, it is possible that if + /// other cores are reserved or unreserved in the same sale rotation that they won't + /// correspond, so it's better to look up the core properly in the `Reservations` storage. + #[pallet::call_index(2)] + pub fn unreserve(origin: OriginFor, item_index: u32) -> DispatchResultWithPostInfo { + T::AdminOrigin::ensure_origin_or_root(origin)?; + Self::do_unreserve(item_index)?; + Ok(Pays::No.into()) + } + + /// Reserve a core for a single task workload for a limited period. + /// + /// In the interlude and sale period where Bulk Coretime is sold for the period immediately + /// after `until`, then the same workload may be renewed. + /// + /// - `origin`: Must be Root or pass `AdminOrigin`. + /// - `task`: The workload which should be placed on a core. + /// - `until`: The timeslice now earlier than which `task` should be placed as a workload on + /// a core. + #[pallet::call_index(3)] + pub fn set_lease( + origin: OriginFor, + task: TaskId, + until: Timeslice, + ) -> DispatchResultWithPostInfo { + T::AdminOrigin::ensure_origin_or_root(origin)?; + Self::do_set_lease(task, until)?; + Ok(Pays::No.into()) + } + + /// Begin the Bulk Coretime sales rotation. + /// + /// - `origin`: Must be Root or pass `AdminOrigin`. + /// - `initial_price`: The price of Bulk Coretime in the first sale. + /// - `core_count`: The number of cores which can be allocated. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::start_sales((*core_count).into()))] + pub fn start_sales( + origin: OriginFor, + initial_price: BalanceOf, + core_count: CoreIndex, + ) -> DispatchResultWithPostInfo { + T::AdminOrigin::ensure_origin_or_root(origin)?; + Self::do_start_sales(initial_price, core_count)?; + Ok(Pays::No.into()) + } + + /// Purchase Bulk Coretime in the ongoing Sale. + /// + /// - `origin`: Must be a Signed origin with at least enough funds to pay the current price + /// of Bulk Coretime. + /// - `price_limit`: An amount no more than which should be paid. + #[pallet::call_index(5)] + pub fn purchase( + origin: OriginFor, + price_limit: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::do_purchase(who, price_limit)?; + Ok(Pays::No.into()) + } + + /// Renew Bulk Coretime in the ongoing Sale or its prior Interlude Period. + /// + /// - `origin`: Must be a Signed origin with at least enough funds to pay the renewal price + /// of the core. + /// - `core`: The core which should be renewed. + #[pallet::call_index(6)] + pub fn renew(origin: OriginFor, core: CoreIndex) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::do_renew(who, core)?; + Ok(Pays::No.into()) + } + + /// Transfer a Bulk Coretime Region to a new owner. + /// + /// - `origin`: Must be a Signed origin of the account which owns the Region `region_id`. + /// - `region_id`: The Region whose ownership should change. + /// - `new_owner`: The new owner for the Region. + #[pallet::call_index(7)] + pub fn transfer( + origin: OriginFor, + region_id: RegionId, + new_owner: T::AccountId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_transfer(region_id, Some(who), new_owner)?; + Ok(()) + } + + /// Split a Bulk Coretime Region into two non-overlapping Regions at a particular time into + /// the region. + /// + /// - `origin`: Must be a Signed origin of the account which owns the Region `region_id`. + /// - `region_id`: The Region which should be partitioned into two non-overlapping Regions. + /// - `pivot`: The offset in time into the Region at which to make the split. + #[pallet::call_index(8)] + pub fn partition( + origin: OriginFor, + region_id: RegionId, + pivot: Timeslice, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_partition(region_id, Some(who), pivot)?; + Ok(()) + } + + /// Split a Bulk Coretime Region into two wholly-overlapping Regions with complementary + /// interlace masks which together make up the original Region's interlace mask. + /// + /// - `origin`: Must be a Signed origin of the account which owns the Region `region_id`. + /// - `region_id`: The Region which should become two interlaced Regions of incomplete + /// regularity. + /// - `pivot`: The interlace mask of on of the two new regions (the other it its partial + /// complement). + #[pallet::call_index(9)] + pub fn interlace( + origin: OriginFor, + region_id: RegionId, + pivot: CoreMask, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_interlace(region_id, Some(who), pivot)?; + Ok(()) + } + + /// Assign a Bulk Coretime Region to a task. + /// + /// - `origin`: Must be a Signed origin of the account which owns the Region `region_id`. + /// - `region_id`: The Region which should be assigned to the task. + /// - `task`: The task to assign. + /// - `finality`: Indication of whether this assignment is final (in which case it may be + /// eligible for renewal) or provisional (in which case it may be manipulated and/or + /// reassigned at a later stage). + #[pallet::call_index(10)] + pub fn assign( + origin: OriginFor, + region_id: RegionId, + task: TaskId, + finality: Finality, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::do_assign(region_id, Some(who), task, finality)?; + Ok(if finality == Finality::Final { Pays::No } else { Pays::Yes }.into()) + } + + /// Place a Bulk Coretime Region into the Instantaneous Coretime Pool. + /// + /// - `origin`: Must be a Signed origin of the account which owns the Region `region_id`. + /// - `region_id`: The Region which should be assigned to the Pool. + /// - `payee`: The account which is able to collect any revenue due for the usage of this + /// Coretime. + #[pallet::call_index(11)] + pub fn pool( + origin: OriginFor, + region_id: RegionId, + payee: T::AccountId, + finality: Finality, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::do_pool(region_id, Some(who), payee, finality)?; + Ok(if finality == Finality::Final { Pays::No } else { Pays::Yes }.into()) + } + + /// Claim the revenue owed from inclusion in the Instantaneous Coretime Pool. + /// + /// - `origin`: Must be a Signed origin of the account which owns the Region `region_id`. + /// - `region_id`: The Region which was assigned to the Pool. + /// - `max_timeslices`: The maximum number of timeslices which should be processed. This may + /// effect the weight of the call but should be ideally made equivalant to the length of + /// the Region `region_id`. If it is less than this, then further dispatches will be + /// required with the `region_id` which makes up any remainders of the region to be + /// collected. + #[pallet::call_index(12)] + #[pallet::weight(T::WeightInfo::claim_revenue(*max_timeslices))] + pub fn claim_revenue( + origin: OriginFor, + region_id: RegionId, + max_timeslices: Timeslice, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + Self::do_claim_revenue(region_id, max_timeslices)?; + Ok(Pays::No.into()) + } + + /// Purchase credit for use in the Instantaneous Coretime Pool. + /// + /// - `origin`: Must be a Signed origin able to pay at least `amount`. + /// - `amount`: The amount of credit to purchase. + /// - `beneficiary`: The account on the Relay-chain which controls the credit (generally + /// this will be the collator's hot wallet). + #[pallet::call_index(13)] + pub fn purchase_credit( + origin: OriginFor, + amount: BalanceOf, + beneficiary: RelayAccountIdOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_purchase_credit(who, amount, beneficiary)?; + Ok(()) + } + + /// Drop an expired Region from the chain. + /// + /// - `origin`: Must be a Signed origin. + /// - `region_id`: The Region which has expired. + #[pallet::call_index(14)] + pub fn drop_region( + origin: OriginFor, + region_id: RegionId, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + Self::do_drop_region(region_id)?; + Ok(Pays::No.into()) + } + + /// Drop an expired Instantaneous Pool Contribution record from the chain. + /// + /// - `origin`: Must be a Signed origin. + /// - `region_id`: The Region identifying the Pool Contribution which has expired. + #[pallet::call_index(15)] + pub fn drop_contribution( + origin: OriginFor, + region_id: RegionId, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + Self::do_drop_contribution(region_id)?; + Ok(Pays::No.into()) + } + + /// Drop an expired Instantaneous Pool History record from the chain. + /// + /// - `origin`: Must be a Signed origin. + /// - `region_id`: The time of the Pool History record which has expired. + #[pallet::call_index(16)] + pub fn drop_history(origin: OriginFor, when: Timeslice) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + Self::do_drop_history(when)?; + Ok(Pays::No.into()) + } + + /// Drop an expired Allowed Renewal record from the chain. + /// + /// - `origin`: Must be a Signed origin of the account which owns the Region `region_id`. + /// - `core`: The core to which the expired renewal refers. + /// - `when`: The timeslice to which the expired renewal refers. This must have passed. + #[pallet::call_index(17)] + pub fn drop_renewal( + origin: OriginFor, + core: CoreIndex, + when: Timeslice, + ) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + Self::do_drop_renewal(core, when)?; + Ok(Pays::No.into()) + } + + /// Request a change to the number of cores available for scheduling work. + /// + /// - `origin`: Must be Root or pass `AdminOrigin`. + /// - `core_count`: The desired number of cores to be made available. + #[pallet::call_index(18)] + #[pallet::weight(T::WeightInfo::request_core_count((*core_count).into()))] + pub fn request_core_count(origin: OriginFor, core_count: CoreIndex) -> DispatchResult { + T::AdminOrigin::ensure_origin_or_root(origin)?; + Self::do_request_core_count(core_count)?; + Ok(()) + } + } +} diff --git a/frame/broker/src/mock.rs b/frame/broker/src/mock.rs new file mode 100644 index 0000000000000..cab6b7389c064 --- /dev/null +++ b/frame/broker/src/mock.rs @@ -0,0 +1,322 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(test)] + +use crate::{test_fungibles::TestFungibles, *}; +use frame_support::{ + assert_ok, ensure, ord_parameter_types, parameter_types, + traits::{ + fungible::{Balanced, Credit, Inspect, ItemOf, Mutate}, + nonfungible::Inspect as NftInspect, + EitherOfDiverse, Hooks, OnUnbalanced, + }, + PalletId, +}; +use frame_system::{EnsureRoot, EnsureSignedBy}; +use sp_arithmetic::Perbill; +use sp_core::{ConstU16, ConstU32, ConstU64, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, Identity, IdentityLookup}, + BuildStorage, Saturating, +}; +use sp_std::collections::btree_map::BTreeMap; + +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Broker: crate, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = ConstU16<42>; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum CoretimeTraceItem { + AssignCore { + core: CoreIndex, + begin: u32, + assignment: Vec<(CoreAssignment, PartsOf57600)>, + end_hint: Option, + }, +} +use CoretimeTraceItem::*; + +parameter_types! { + pub static CoretimeTrace: Vec<(u32, CoretimeTraceItem)> = Default::default(); + pub static CoretimeCredit: BTreeMap = Default::default(); + pub static CoretimeSpending: Vec<(u32, u64)> = Default::default(); + pub static CoretimeWorkplan: BTreeMap<(u32, CoreIndex), Vec<(CoreAssignment, PartsOf57600)>> = Default::default(); + pub static CoretimeUsage: BTreeMap> = Default::default(); + pub static CoretimeInPool: CoreMaskBitCount = 0; + pub static NotifyCoreCount: Vec = Default::default(); + pub static NotifyRevenueInfo: Vec<(u32, u64)> = Default::default(); +} + +pub struct TestCoretimeProvider; +impl CoretimeInterface for TestCoretimeProvider { + type AccountId = u64; + type Balance = u64; + type BlockNumber = u32; + fn latest() -> Self::BlockNumber { + System::block_number() as u32 + } + fn request_core_count(count: CoreIndex) { + NotifyCoreCount::mutate(|s| s.insert(0, count)); + } + fn request_revenue_info_at(when: Self::BlockNumber) { + if when > Self::latest() { + panic!("Asking for revenue info in the future {:?} {:?}", when, Self::latest()); + } + + let mut total = 0; + CoretimeSpending::mutate(|s| { + s.retain(|(n, a)| { + if *n < when { + total += a; + false + } else { + true + } + }) + }); + NotifyRevenueInfo::mutate(|s| s.insert(0, (when, total))); + } + fn credit_account(who: Self::AccountId, amount: Self::Balance) { + CoretimeCredit::mutate(|c| c.entry(who).or_default().saturating_accrue(amount)); + } + fn assign_core( + core: CoreIndex, + begin: Self::BlockNumber, + assignment: Vec<(CoreAssignment, PartsOf57600)>, + end_hint: Option, + ) { + CoretimeWorkplan::mutate(|p| p.insert((begin, core), assignment.clone())); + let item = (Self::latest(), AssignCore { core, begin, assignment, end_hint }); + CoretimeTrace::mutate(|v| v.push(item)); + } + fn check_notify_core_count() -> Option { + NotifyCoreCount::mutate(|s| s.pop()) + } + fn check_notify_revenue_info() -> Option<(Self::BlockNumber, Self::Balance)> { + NotifyRevenueInfo::mutate(|s| s.pop()) + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_core_count(count: u16) { + NotifyCoreCount::mutate(|s| s.insert(0, count)); + } + #[cfg(feature = "runtime-benchmarks")] + fn ensure_notify_revenue_info(when: Self::BlockNumber, revenue: Self::Balance) { + NotifyRevenueInfo::mutate(|s| s.push((when, revenue))); + } +} +impl TestCoretimeProvider { + pub fn spend_instantaneous(who: u64, price: u64) -> Result<(), ()> { + let mut c = CoretimeCredit::get(); + ensure!(CoretimeInPool::get() > 0, ()); + c.insert(who, c.get(&who).ok_or(())?.checked_sub(price).ok_or(())?); + CoretimeCredit::set(c); + CoretimeSpending::mutate(|v| v.push((Self::latest(), price))); + Ok(()) + } + pub fn bump() { + let mut pool_size = CoretimeInPool::get(); + let mut workplan = CoretimeWorkplan::get(); + let mut usage = CoretimeUsage::get(); + let now = Self::latest(); + workplan.retain(|(when, core), assignment| { + if *when <= now { + if let Some(old_assignment) = usage.get(core) { + if let Some(a) = old_assignment.iter().find(|i| i.0 == CoreAssignment::Pool) { + pool_size -= (a.1 / 720) as CoreMaskBitCount; + } + } + if let Some(a) = assignment.iter().find(|i| i.0 == CoreAssignment::Pool) { + pool_size += (a.1 / 720) as CoreMaskBitCount; + } + usage.insert(*core, assignment.clone()); + false + } else { + true + } + }); + CoretimeInPool::set(pool_size); + CoretimeWorkplan::set(workplan); + CoretimeUsage::set(usage); + } +} + +parameter_types! { + pub const TestBrokerId: PalletId = PalletId(*b"TsBroker"); +} + +pub struct IntoZero; +impl OnUnbalanced::Currency>> for IntoZero { + fn on_nonzero_unbalanced(credit: Credit::Currency>) { + let _ = <::Currency as Balanced<_>>::resolve(&0, credit); + } +} + +ord_parameter_types! { + pub const One: u64 = 1; +} +type EnsureOneOrRoot = EitherOfDiverse, EnsureSignedBy>; + +impl crate::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = ItemOf, ()>, (), u64>; + type OnRevenue = IntoZero; + type TimeslicePeriod = ConstU32<2>; + type MaxLeasedCores = ConstU32<5>; + type MaxReservedCores = ConstU32<5>; + type Coretime = TestCoretimeProvider; + type ConvertBalance = Identity; + type WeightInfo = (); + type PalletId = TestBrokerId; + type AdminOrigin = EnsureOneOrRoot; + type PriceAdapter = Linear; +} + +pub fn advance_to(b: u64) { + while System::block_number() < b { + System::set_block_number(System::block_number() + 1); + TestCoretimeProvider::bump(); + Broker::on_initialize(System::block_number()); + } +} + +pub fn pot() -> u64 { + balance(Broker::account_id()) +} + +pub fn revenue() -> u64 { + balance(0) +} + +pub fn balance(who: u64) -> u64 { + <::Currency as Inspect<_>>::total_balance(&who) +} + +pub fn attribute(nft: RegionId, attribute: impl codec::Encode) -> T { + >::typed_attribute::<_, T>(&nft.into(), &attribute).unwrap() +} + +pub fn new_config() -> ConfigRecordOf { + ConfigRecord { + advance_notice: 2, + interlude_length: 1, + leadin_length: 1, + ideal_bulk_proportion: Default::default(), + limit_cores_offered: None, + region_length: 3, + renewal_bump: Perbill::from_percent(10), + contribution_timeout: 5, + } +} + +pub struct TestExt(ConfigRecordOf); +#[allow(dead_code)] +impl TestExt { + pub fn new() -> Self { + Self(new_config()) + } + + pub fn advance_notice(mut self, advance_notice: Timeslice) -> Self { + self.0.advance_notice = advance_notice; + self + } + + pub fn interlude_length(mut self, interlude_length: u64) -> Self { + self.0.interlude_length = interlude_length; + self + } + + pub fn leadin_length(mut self, leadin_length: u64) -> Self { + self.0.leadin_length = leadin_length; + self + } + + pub fn region_length(mut self, region_length: Timeslice) -> Self { + self.0.region_length = region_length; + self + } + + pub fn ideal_bulk_proportion(mut self, ideal_bulk_proportion: Perbill) -> Self { + self.0.ideal_bulk_proportion = ideal_bulk_proportion; + self + } + + pub fn limit_cores_offered(mut self, limit_cores_offered: Option) -> Self { + self.0.limit_cores_offered = limit_cores_offered; + self + } + + pub fn renewal_bump(mut self, renewal_bump: Perbill) -> Self { + self.0.renewal_bump = renewal_bump; + self + } + + pub fn contribution_timeout(mut self, contribution_timeout: Timeslice) -> Self { + self.0.contribution_timeout = contribution_timeout; + self + } + + pub fn endow(self, who: u64, amount: u64) -> Self { + assert_ok!(<::Currency as Mutate<_>>::mint_into(&who, amount)); + self + } + + pub fn execute_with(self, f: impl Fn() -> R) -> R { + new_test_ext().execute_with(|| { + assert_ok!(Broker::do_configure(self.0)); + f() + }) + } +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let c = frame_system::GenesisConfig::::default().build_storage().unwrap(); + sp_io::TestExternalities::from(c) +} diff --git a/frame/broker/src/nonfungible_impl.rs b/frame/broker/src/nonfungible_impl.rs new file mode 100644 index 0000000000000..fe95438cb1afa --- /dev/null +++ b/frame/broker/src/nonfungible_impl.rs @@ -0,0 +1,52 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use frame_support::{ + pallet_prelude::{DispatchResult, *}, + traits::nonfungible::{Inspect, Transfer}, +}; +use sp_std::vec::Vec; + +impl Inspect for Pallet { + type ItemId = u128; + + fn owner(index: &Self::ItemId) -> Option { + Regions::::get(RegionId::from(*index)).map(|r| r.owner) + } + + fn attribute(index: &Self::ItemId, key: &[u8]) -> Option> { + let id = RegionId::from(*index); + let item = Regions::::get(id)?; + match key { + b"begin" => Some(id.begin.encode()), + b"end" => Some(item.end.encode()), + b"length" => Some(item.end.saturating_sub(id.begin).encode()), + b"core" => Some(id.core.encode()), + b"part" => Some(id.mask.encode()), + b"owner" => Some(item.owner.encode()), + b"paid" => Some(item.paid.encode()), + _ => None, + } + } +} + +impl Transfer for Pallet { + fn transfer(index: &Self::ItemId, dest: &T::AccountId) -> DispatchResult { + Self::do_transfer((*index).into(), None, dest.clone()).map_err(Into::into) + } +} diff --git a/frame/broker/src/test_fungibles.rs b/frame/broker/src/test_fungibles.rs new file mode 100644 index 0000000000000..f6ac5a49dedd2 --- /dev/null +++ b/frame/broker/src/test_fungibles.rs @@ -0,0 +1,283 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use codec::{Decode, Encode}; +use frame_support::{ + parameter_types, + traits::{ + fungibles::{self, Dust}, + tokens::{ + self, DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence, + }, + }, +}; +use scale_info::TypeInfo; +use sp_arithmetic::traits::Zero; +use sp_core::{Get, TypedGet}; +use sp_runtime::{DispatchError, DispatchResult}; +use sp_std::collections::btree_map::BTreeMap; + +parameter_types! { + static TestAssetOf: BTreeMap<(u32, Vec), Vec> = Default::default(); + static TestBalanceOf: BTreeMap<(u32, Vec, Vec), Vec> = Default::default(); + static TestHoldOf: BTreeMap<(u32, Vec, Vec, Vec), Vec> = Default::default(); +} + +pub struct TestFungibles( + core::marker::PhantomData<(Instance, AccountId, AssetId, MinimumBalance, HoldReason)>, +); +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason, + > fungibles::Inspect + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ + type AssetId = AssetId; + type Balance = MinimumBalance::Type; + + fn total_issuance(asset: Self::AssetId) -> Self::Balance { + TestAssetOf::get() + .get(&(Instance::get(), asset.encode())) + .and_then(|data| Decode::decode(&mut &data[..]).ok()) + .unwrap_or_default() + } + + fn active_issuance(asset: Self::AssetId) -> Self::Balance { + Self::total_issuance(asset) + } + + /// The minimum balance any single account may have. + fn minimum_balance(_asset: Self::AssetId) -> Self::Balance { + MinimumBalance::get() + } + + fn total_balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance { + TestBalanceOf::get() + .get(&(Instance::get(), asset.encode(), who.encode())) + .and_then(|data| Decode::decode(&mut &data[..]).ok()) + .unwrap_or_default() + } + + fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance { + Self::total_balance(asset, who) + } + + fn reducible_balance( + asset: Self::AssetId, + who: &AccountId, + _preservation: Preservation, + _force: Fortitude, + ) -> Self::Balance { + Self::total_balance(asset, who) + } + + fn can_deposit( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + _provenance: Provenance, + ) -> DepositConsequence { + if !Self::asset_exists(asset) { + return DepositConsequence::UnknownAsset + } + if amount + Self::balance(asset, who) < Self::minimum_balance(asset) { + return DepositConsequence::BelowMinimum + } + DepositConsequence::Success + } + + fn can_withdraw( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + if Self::reducible_balance(asset, who, Preservation::Expendable, Fortitude::Polite) < amount + { + return WithdrawConsequence::BalanceLow + } + if Self::total_balance(asset, who) < Self::minimum_balance(asset) + amount { + return WithdrawConsequence::WouldDie + } + WithdrawConsequence::Success + } + + fn asset_exists(asset: Self::AssetId) -> bool { + TestAssetOf::get().contains_key(&(Instance::get(), asset.encode())) + } +} + +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason, + > fungibles::Unbalanced + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ + fn handle_dust(_dust: Dust) {} + + fn write_balance( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> Result, DispatchError> { + let mut tb = TestBalanceOf::get(); + let maybe_dust = if amount < MinimumBalance::get() { + tb.remove(&(Instance::get(), asset.encode(), who.encode())); + if amount.is_zero() { + None + } else { + Some(amount) + } + } else { + tb.insert((Instance::get(), asset.encode(), who.encode()), amount.encode()); + None + }; + TestBalanceOf::set(tb); + Ok(maybe_dust) + } + + fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) { + let mut ta = TestAssetOf::get(); + ta.insert((Instance::get(), asset.encode()), amount.encode()); + TestAssetOf::set(ta); + } +} + +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason, + > fungibles::Mutate + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ +} + +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason, + > fungibles::Balanced + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ + type OnDropCredit = fungibles::DecreaseIssuance; + type OnDropDebt = fungibles::IncreaseIssuance; +} + +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason: Encode + Decode + TypeInfo + 'static, + > fungibles::InspectHold + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ + type Reason = HoldReason; + + fn total_balance_on_hold(asset: Self::AssetId, who: &AccountId) -> Self::Balance { + let asset = asset.encode(); + let who = who.encode(); + TestHoldOf::get() + .iter() + .filter(|(k, _)| k.0 == Instance::get() && k.1 == asset && k.2 == who) + .filter_map(|(_, b)| Self::Balance::decode(&mut &b[..]).ok()) + .fold(Zero::zero(), |a, i| a + i) + } + + fn balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + ) -> Self::Balance { + TestHoldOf::get() + .get(&(Instance::get(), asset.encode(), who.encode(), reason.encode())) + .and_then(|data| Decode::decode(&mut &data[..]).ok()) + .unwrap_or_default() + } +} + +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason: Encode + Decode + TypeInfo + 'static, + > fungibles::UnbalancedHold + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ + fn set_balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let mut th = TestHoldOf::get(); + th.insert( + (Instance::get(), asset.encode(), who.encode(), reason.encode()), + amount.encode(), + ); + TestHoldOf::set(th); + Ok(()) + } +} + +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason: Encode + Decode + TypeInfo + 'static, + > fungibles::MutateHold + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ +} + +impl< + Instance: Get, + AccountId: Encode, + AssetId: tokens::AssetId + Copy, + MinimumBalance: TypedGet, + HoldReason: Encode + Decode + TypeInfo + 'static, + > fungibles::BalancedHold + for TestFungibles +where + MinimumBalance::Type: tokens::Balance, +{ +} diff --git a/frame/broker/src/tests.rs b/frame/broker/src/tests.rs new file mode 100644 index 0000000000000..3c326010dddfc --- /dev/null +++ b/frame/broker/src/tests.rs @@ -0,0 +1,896 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(test)] + +use crate::{core_mask::*, mock::*, *}; +use frame_support::{ + assert_noop, assert_ok, + traits::nonfungible::{Inspect as NftInspect, Transfer}, + BoundedVec, +}; +use frame_system::RawOrigin::Root; +use sp_runtime::traits::Get; +use CoreAssignment::*; +use CoretimeTraceItem::*; +use Finality::*; + +#[test] +fn basic_initialize_works() { + TestExt::new().execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + assert_eq!(CoretimeTrace::get(), vec![]); + assert_eq!(Broker::current_timeslice(), 0); + }); +} + +#[test] +fn drop_region_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_ok!(Broker::do_assign(region, Some(1), 1001, Provisional)); + advance_to(11); + assert_noop!(Broker::do_drop_region(region), Error::::StillValid); + advance_to(12); + // assignment worked. + let just_1001 = vec![(Task(1001), 57600)]; + let just_pool = vec![(Pool, 57600)]; + assert_eq!( + CoretimeTrace::get(), + vec![ + (6, AssignCore { core: 0, begin: 8, assignment: just_1001, end_hint: None }), + (12, AssignCore { core: 0, begin: 14, assignment: just_pool, end_hint: None }), + ] + ); + // `region` still exists as it was never finalized. + assert_eq!(Regions::::iter().count(), 1); + assert_ok!(Broker::do_drop_region(region)); + assert_eq!(Regions::::iter().count(), 0); + assert_noop!(Broker::do_drop_region(region), Error::::UnknownRegion); + }); +} + +#[test] +fn drop_renewal_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_ok!(Broker::do_assign(region, Some(1), 1001, Final)); + advance_to(11); + let e = Error::::StillValid; + assert_noop!(Broker::do_drop_renewal(region.core, region.begin + 3), e); + advance_to(12); + assert_ok!(Broker::do_drop_renewal(region.core, region.begin + 3)); + let e = Error::::UnknownRenewal; + assert_noop!(Broker::do_drop_renewal(region.core, region.begin + 3), e); + }); +} + +#[test] +fn drop_contribution_works() { + TestExt::new().contribution_timeout(3).endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + // Place region in pool. Active in pool timeslices 4, 5, 6 = rcblocks 8, 10, 12; we + // expect the contribution record to timeout 3 timeslices following 7 = 10 + assert_ok!(Broker::do_pool(region, Some(1), 1, Final)); + assert_eq!(InstaPoolContribution::::iter().count(), 1); + advance_to(19); + assert_noop!(Broker::do_drop_contribution(region), Error::::StillValid); + advance_to(20); + assert_ok!(Broker::do_drop_contribution(region)); + assert_eq!(InstaPoolContribution::::iter().count(), 0); + assert_noop!(Broker::do_drop_contribution(region), Error::::UnknownContribution); + }); +} + +#[test] +fn drop_history_works() { + TestExt::new() + .contribution_timeout(4) + .endow(1, 1000) + .endow(2, 30) + .execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let mut region = Broker::do_purchase(1, u64::max_value()).unwrap(); + // Place region in pool. Active in pool timeslices 4, 5, 6 = rcblocks 8, 10, 12; we + // expect to make/receive revenue reports on blocks 10, 12, 14. + assert_ok!(Broker::do_pool(region, Some(1), 1, Final)); + assert_ok!(Broker::do_purchase_credit(2, 30, 2)); + advance_to(6); + // In the stable state with no pending payouts, we expect to see 3 items in + // InstaPoolHistory here since there is a latency of 1 timeslice (for generating the + // revenue report), the forward notice period (equivalent to another timeslice) and a + // block between the revenue report being requested and the response being processed. + assert_eq!(InstaPoolHistory::::iter().count(), 3); + advance_to(7); + // One block later, the most recent report will have been processed, so the effective + // queue drops to 2 items. + assert_eq!(InstaPoolHistory::::iter().count(), 2); + advance_to(8); + assert_eq!(InstaPoolHistory::::iter().count(), 3); + assert_ok!(TestCoretimeProvider::spend_instantaneous(2, 10)); + advance_to(10); + assert_eq!(InstaPoolHistory::::iter().count(), 3); + assert_ok!(TestCoretimeProvider::spend_instantaneous(2, 10)); + advance_to(12); + assert_eq!(InstaPoolHistory::::iter().count(), 4); + assert_ok!(TestCoretimeProvider::spend_instantaneous(2, 10)); + advance_to(14); + assert_eq!(InstaPoolHistory::::iter().count(), 5); + advance_to(16); + assert_eq!(InstaPoolHistory::::iter().count(), 6); + advance_to(17); + assert_noop!(Broker::do_drop_history(region.begin), Error::::StillValid); + advance_to(18); + assert_eq!(InstaPoolHistory::::iter().count(), 6); + // Block 18 is 8 blocks ()= 4 timeslices = contribution timeout) after first region. + // Its revenue should now be droppable. + assert_ok!(Broker::do_drop_history(region.begin)); + assert_eq!(InstaPoolHistory::::iter().count(), 5); + assert_noop!(Broker::do_drop_history(region.begin), Error::::NoHistory); + advance_to(19); + region.begin += 1; + assert_noop!(Broker::do_drop_history(region.begin), Error::::StillValid); + advance_to(20); + assert_ok!(Broker::do_drop_history(region.begin)); + assert_eq!(InstaPoolHistory::::iter().count(), 4); + assert_noop!(Broker::do_drop_history(region.begin), Error::::NoHistory); + advance_to(21); + region.begin += 1; + assert_noop!(Broker::do_drop_history(region.begin), Error::::StillValid); + advance_to(22); + assert_ok!(Broker::do_drop_history(region.begin)); + assert_eq!(InstaPoolHistory::::iter().count(), 3); + assert_noop!(Broker::do_drop_history(region.begin), Error::::NoHistory); + }); +} + +#[test] +fn request_core_count_works() { + TestExt::new().execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 0)); + assert_ok!(Broker::request_core_count(RuntimeOrigin::root(), 1)); + advance_to(12); + let assignment = vec![(Pool, 57600)]; + assert_eq!( + CoretimeTrace::get(), + vec![(12, AssignCore { core: 0, begin: 14, assignment, end_hint: None })], + ); + }); +} + +#[test] +fn transfer_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_ok!(>::transfer(®ion.into(), &2)); + assert_eq!(>::owner(®ion.into()), Some(2)); + assert_noop!(Broker::do_assign(region, Some(1), 1001, Final), Error::::NotOwner); + assert_ok!(Broker::do_assign(region, Some(2), 1002, Final)); + }); +} + +#[test] +fn permanent_is_not_reassignable() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_ok!(Broker::do_assign(region, Some(1), 1001, Final)); + assert_noop!(Broker::do_assign(region, Some(1), 1002, Final), Error::::UnknownRegion); + assert_noop!(Broker::do_pool(region, Some(1), 1002, Final), Error::::UnknownRegion); + assert_noop!(Broker::do_partition(region, Some(1), 1), Error::::UnknownRegion); + assert_noop!( + Broker::do_interlace(region, Some(1), CoreMask::from_chunk(0, 40)), + Error::::UnknownRegion + ); + }); +} + +#[test] +fn provisional_is_reassignable() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_ok!(Broker::do_assign(region, Some(1), 1001, Provisional)); + let (region1, region) = Broker::do_partition(region, Some(1), 1).unwrap(); + let (region2, region3) = + Broker::do_interlace(region, Some(1), CoreMask::from_chunk(0, 40)).unwrap(); + assert_ok!(Broker::do_pool(region1, Some(1), 1, Provisional)); + assert_ok!(Broker::do_assign(region2, Some(1), 1002, Provisional)); + assert_ok!(Broker::do_assign(region3, Some(1), 1003, Provisional)); + advance_to(8); + assert_eq!( + CoretimeTrace::get(), + vec![ + ( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Pool, 57600),], + end_hint: None + } + ), + ( + 8, + AssignCore { + core: 0, + begin: 10, + assignment: vec![(Task(1002), 28800), (Task(1003), 28800),], + end_hint: None + } + ), + ] + ); + }); +} + +#[test] +fn nft_metadata_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_eq!(attribute::(region, b"begin"), 4); + assert_eq!(attribute::(region, b"length"), 3); + assert_eq!(attribute::(region, b"end"), 7); + assert_eq!(attribute::(region, b"owner"), 1); + assert_eq!(attribute::(region, b"part"), 0xfffff_fffff_fffff_fffff.into()); + assert_eq!(attribute::(region, b"core"), 0); + assert_eq!(attribute::>(region, b"paid"), Some(100)); + + assert_ok!(Broker::do_transfer(region, None, 42)); + let (_, region) = Broker::do_partition(region, None, 2).unwrap(); + let (region, _) = + Broker::do_interlace(region, None, 0x00000_fffff_fffff_00000.into()).unwrap(); + assert_eq!(attribute::(region, b"begin"), 6); + assert_eq!(attribute::(region, b"length"), 1); + assert_eq!(attribute::(region, b"end"), 7); + assert_eq!(attribute::(region, b"owner"), 42); + assert_eq!(attribute::(region, b"part"), 0x00000_fffff_fffff_00000.into()); + assert_eq!(attribute::(region, b"core"), 0); + assert_eq!(attribute::>(region, b"paid"), None); + }); +} + +#[test] +fn migration_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_set_lease(1000, 8)); + assert_ok!(Broker::do_start_sales(100, 2)); + + // Sale is for regions from TS4..7 + // Not ending in this sale period. + assert_noop!(Broker::do_renew(1, 0), Error::::NotAllowed); + + advance_to(12); + // Sale is now for regions from TS10..13 + // Ending in this sale period. + // Should now be renewable. + assert_ok!(Broker::do_renew(1, 0)); + assert_eq!(balance(1), 900); + advance_to(18); + + let just_pool = || vec![(Pool, 57600)]; + let just_1000 = || vec![(Task(1000), 57600)]; + assert_eq!( + CoretimeTrace::get(), + vec![ + (6, AssignCore { core: 0, begin: 8, assignment: just_1000(), end_hint: None }), + (6, AssignCore { core: 1, begin: 8, assignment: just_pool(), end_hint: None }), + (12, AssignCore { core: 0, begin: 14, assignment: just_1000(), end_hint: None }), + (12, AssignCore { core: 1, begin: 14, assignment: just_pool(), end_hint: None }), + (18, AssignCore { core: 0, begin: 20, assignment: just_1000(), end_hint: None }), + (18, AssignCore { core: 1, begin: 20, assignment: just_pool(), end_hint: None }), + ] + ); + }); +} + +#[test] +fn renewal_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_eq!(balance(1), 900); + assert_ok!(Broker::do_assign(region, None, 1001, Final)); + // Should now be renewable. + advance_to(6); + assert_noop!(Broker::do_purchase(1, u64::max_value()), Error::::TooEarly); + let core = Broker::do_renew(1, region.core).unwrap(); + assert_eq!(balance(1), 800); + advance_to(8); + assert_noop!(Broker::do_purchase(1, u64::max_value()), Error::::SoldOut); + advance_to(12); + assert_ok!(Broker::do_renew(1, core)); + assert_eq!(balance(1), 690); + }); +} + +#[test] +fn instapool_payouts_work() { + TestExt::new().endow(1, 1000).execute_with(|| { + let item = ScheduleItem { assignment: Pool, mask: CoreMask::complete() }; + assert_ok!(Broker::do_reserve(Schedule::truncate_from(vec![item]))); + assert_ok!(Broker::do_start_sales(100, 3)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_ok!(Broker::do_pool(region, None, 2, Final)); + assert_ok!(Broker::do_purchase_credit(1, 20, 1)); + advance_to(8); + assert_ok!(TestCoretimeProvider::spend_instantaneous(1, 10)); + advance_to(11); + assert_eq!(pot(), 14); + assert_eq!(revenue(), 106); + assert_ok!(Broker::do_claim_revenue(region, 100)); + assert_eq!(pot(), 10); + assert_eq!(balance(2), 4); + }); +} + +#[test] +fn instapool_partial_core_payouts_work() { + TestExt::new().endow(1, 1000).execute_with(|| { + let item = ScheduleItem { assignment: Pool, mask: CoreMask::complete() }; + assert_ok!(Broker::do_reserve(Schedule::truncate_from(vec![item]))); + assert_ok!(Broker::do_start_sales(100, 2)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + let (region1, region2) = + Broker::do_interlace(region, None, CoreMask::from_chunk(0, 20)).unwrap(); + assert_ok!(Broker::do_pool(region1, None, 2, Final)); + assert_ok!(Broker::do_pool(region2, None, 3, Final)); + assert_ok!(Broker::do_purchase_credit(1, 40, 1)); + advance_to(8); + assert_ok!(TestCoretimeProvider::spend_instantaneous(1, 40)); + advance_to(11); + assert_ok!(Broker::do_claim_revenue(region1, 100)); + assert_ok!(Broker::do_claim_revenue(region2, 100)); + assert_eq!(revenue(), 120); + assert_eq!(balance(2), 5); + assert_eq!(balance(3), 15); + assert_eq!(pot(), 0); + }); +} + +#[test] +fn initialize_with_system_paras_works() { + TestExt::new().execute_with(|| { + let item = ScheduleItem { assignment: Task(1u32), mask: CoreMask::complete() }; + assert_ok!(Broker::do_reserve(Schedule::truncate_from(vec![item]))); + let items = vec![ + ScheduleItem { assignment: Task(2u32), mask: 0xfffff_fffff_00000_00000.into() }, + ScheduleItem { assignment: Task(3u32), mask: 0x00000_00000_fffff_00000.into() }, + ScheduleItem { assignment: Task(4u32), mask: 0x00000_00000_00000_fffff.into() }, + ]; + assert_ok!(Broker::do_reserve(Schedule::truncate_from(items))); + assert_ok!(Broker::do_start_sales(100, 2)); + advance_to(10); + assert_eq!( + CoretimeTrace::get(), + vec![ + ( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Task(1), 57600),], + end_hint: None + } + ), + ( + 6, + AssignCore { + core: 1, + begin: 8, + assignment: vec![(Task(2), 28800), (Task(3), 14400), (Task(4), 14400),], + end_hint: None + } + ), + ] + ); + }); +} + +#[test] +fn initialize_with_leased_slots_works() { + TestExt::new().execute_with(|| { + assert_ok!(Broker::do_set_lease(1000, 6)); + assert_ok!(Broker::do_set_lease(1001, 7)); + assert_ok!(Broker::do_start_sales(100, 2)); + advance_to(18); + let end_hint = None; + assert_eq!( + CoretimeTrace::get(), + vec![ + ( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Task(1000), 57600),], + end_hint + } + ), + ( + 6, + AssignCore { + core: 1, + begin: 8, + assignment: vec![(Task(1001), 57600),], + end_hint + } + ), + ( + 12, + AssignCore { + core: 0, + begin: 14, + assignment: vec![(Task(1001), 57600),], + end_hint + } + ), + (12, AssignCore { core: 1, begin: 14, assignment: vec![(Pool, 57600),], end_hint }), + (18, AssignCore { core: 0, begin: 20, assignment: vec![(Pool, 57600),], end_hint }), + (18, AssignCore { core: 1, begin: 20, assignment: vec![(Pool, 57600),], end_hint }), + ] + ); + }); +} + +#[test] +fn purchase_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_ok!(Broker::do_assign(region, None, 1000, Final)); + advance_to(6); + assert_eq!( + CoretimeTrace::get(), + vec![( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Task(1000), 57600),], + end_hint: None + } + ),] + ); + }); +} + +#[test] +fn partition_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + let (region1, region) = Broker::do_partition(region, None, 1).unwrap(); + let (region2, region3) = Broker::do_partition(region, None, 1).unwrap(); + assert_ok!(Broker::do_assign(region1, None, 1001, Final)); + assert_ok!(Broker::do_assign(region2, None, 1002, Final)); + assert_ok!(Broker::do_assign(region3, None, 1003, Final)); + advance_to(10); + assert_eq!( + CoretimeTrace::get(), + vec![ + ( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Task(1001), 57600),], + end_hint: None + } + ), + ( + 8, + AssignCore { + core: 0, + begin: 10, + assignment: vec![(Task(1002), 57600),], + end_hint: None + } + ), + ( + 10, + AssignCore { + core: 0, + begin: 12, + assignment: vec![(Task(1003), 57600),], + end_hint: None + } + ), + ] + ); + }); +} + +#[test] +fn interlace_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + let (region1, region) = + Broker::do_interlace(region, None, CoreMask::from_chunk(0, 30)).unwrap(); + let (region2, region3) = + Broker::do_interlace(region, None, CoreMask::from_chunk(30, 60)).unwrap(); + assert_ok!(Broker::do_assign(region1, None, 1001, Final)); + assert_ok!(Broker::do_assign(region2, None, 1002, Final)); + assert_ok!(Broker::do_assign(region3, None, 1003, Final)); + advance_to(10); + assert_eq!( + CoretimeTrace::get(), + vec![( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Task(1001), 21600), (Task(1002), 21600), (Task(1003), 14400),], + end_hint: None + } + ),] + ); + }); +} + +#[test] +fn interlace_then_partition_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + let (region1, region2) = + Broker::do_interlace(region, None, CoreMask::from_chunk(0, 20)).unwrap(); + let (region1, region3) = Broker::do_partition(region1, None, 1).unwrap(); + let (region2, region4) = Broker::do_partition(region2, None, 2).unwrap(); + assert_ok!(Broker::do_assign(region1, None, 1001, Final)); + assert_ok!(Broker::do_assign(region2, None, 1002, Final)); + assert_ok!(Broker::do_assign(region3, None, 1003, Final)); + assert_ok!(Broker::do_assign(region4, None, 1004, Final)); + advance_to(10); + assert_eq!( + CoretimeTrace::get(), + vec![ + ( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Task(1001), 14400), (Task(1002), 43200),], + end_hint: None + } + ), + ( + 8, + AssignCore { + core: 0, + begin: 10, + assignment: vec![(Task(1002), 43200), (Task(1003), 14400),], + end_hint: None + } + ), + ( + 10, + AssignCore { + core: 0, + begin: 12, + assignment: vec![(Task(1003), 14400), (Task(1004), 43200),], + end_hint: None + } + ), + ] + ); + }); +} + +#[test] +fn partition_then_interlace_works() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + let (region1, region2) = Broker::do_partition(region, None, 1).unwrap(); + let (region1, region3) = + Broker::do_interlace(region1, None, CoreMask::from_chunk(0, 20)).unwrap(); + let (region2, region4) = + Broker::do_interlace(region2, None, CoreMask::from_chunk(0, 30)).unwrap(); + assert_ok!(Broker::do_assign(region1, None, 1001, Final)); + assert_ok!(Broker::do_assign(region2, None, 1002, Final)); + assert_ok!(Broker::do_assign(region3, None, 1003, Final)); + assert_ok!(Broker::do_assign(region4, None, 1004, Final)); + advance_to(10); + assert_eq!( + CoretimeTrace::get(), + vec![ + ( + 6, + AssignCore { + core: 0, + begin: 8, + assignment: vec![(Task(1001), 14400), (Task(1003), 43200),], + end_hint: None + } + ), + ( + 8, + AssignCore { + core: 0, + begin: 10, + assignment: vec![(Task(1002), 21600), (Task(1004), 36000),], + end_hint: None + } + ), + ] + ); + }); +} + +#[test] +fn reservations_are_limited() { + TestExt::new().execute_with(|| { + let schedule = Schedule::truncate_from(vec![ScheduleItem { + assignment: Pool, + mask: CoreMask::complete(), + }]); + let max_cores: u32 = ::MaxReservedCores::get(); + Reservations::::put( + BoundedVec::try_from(vec![schedule.clone(); max_cores as usize]).unwrap(), + ); + assert_noop!(Broker::do_reserve(schedule), Error::::TooManyReservations); + }); +} + +#[test] +fn cannot_unreserve_unknown() { + TestExt::new().execute_with(|| { + let schedule = Schedule::truncate_from(vec![ScheduleItem { + assignment: Pool, + mask: CoreMask::complete(), + }]); + Reservations::::put(BoundedVec::try_from(vec![schedule.clone(); 1usize]).unwrap()); + assert_noop!(Broker::do_unreserve(2), Error::::UnknownReservation); + }); +} + +#[test] +fn cannot_set_expired_lease() { + TestExt::new().execute_with(|| { + advance_to(2); + let current_timeslice = Broker::current_timeslice(); + assert_noop!( + Broker::do_set_lease(1000, current_timeslice.saturating_sub(1)), + Error::::AlreadyExpired + ); + }); +} + +#[test] +fn leases_are_limited() { + TestExt::new().execute_with(|| { + let max_leases: u32 = ::MaxLeasedCores::get(); + Leases::::put( + BoundedVec::try_from(vec![ + LeaseRecordItem { task: 1u32, until: 10u32 }; + max_leases as usize + ]) + .unwrap(), + ); + assert_noop!(Broker::do_set_lease(1000, 10), Error::::TooManyLeases); + }); +} + +#[test] +fn purchase_requires_valid_status_and_sale_info() { + TestExt::new().execute_with(|| { + assert_noop!(Broker::do_purchase(1, 100), Error::::Uninitialized); + + let status = StatusRecord { + core_count: 2, + private_pool_size: 0, + system_pool_size: 0, + last_committed_timeslice: 0, + last_timeslice: 1, + }; + Status::::put(&status); + assert_noop!(Broker::do_purchase(1, 100), Error::::NoSales); + + let mut dummy_sale = SaleInfoRecord { + sale_start: 0, + leadin_length: 0, + price: 200, + sellout_price: None, + region_begin: 0, + region_end: 3, + first_core: 3, + ideal_cores_sold: 0, + cores_offered: 1, + cores_sold: 2, + }; + SaleInfo::::put(&dummy_sale); + assert_noop!(Broker::do_purchase(1, 100), Error::::Unavailable); + + dummy_sale.first_core = 1; + SaleInfo::::put(&dummy_sale); + assert_noop!(Broker::do_purchase(1, 100), Error::::SoldOut); + + assert_ok!(Broker::do_start_sales(200, 1)); + assert_noop!(Broker::do_purchase(1, 100), Error::::TooEarly); + + advance_to(2); + assert_noop!(Broker::do_purchase(1, 100), Error::::Overpriced); + }); +} + +#[test] +fn renewal_requires_valid_status_and_sale_info() { + TestExt::new().execute_with(|| { + assert_noop!(Broker::do_renew(1, 1), Error::::Uninitialized); + + let status = StatusRecord { + core_count: 2, + private_pool_size: 0, + system_pool_size: 0, + last_committed_timeslice: 0, + last_timeslice: 1, + }; + Status::::put(&status); + assert_noop!(Broker::do_renew(1, 1), Error::::NoSales); + + let mut dummy_sale = SaleInfoRecord { + sale_start: 0, + leadin_length: 0, + price: 200, + sellout_price: None, + region_begin: 0, + region_end: 3, + first_core: 3, + ideal_cores_sold: 0, + cores_offered: 1, + cores_sold: 2, + }; + SaleInfo::::put(&dummy_sale); + assert_noop!(Broker::do_renew(1, 1), Error::::Unavailable); + + dummy_sale.first_core = 1; + SaleInfo::::put(&dummy_sale); + assert_noop!(Broker::do_renew(1, 1), Error::::SoldOut); + + assert_ok!(Broker::do_start_sales(200, 1)); + assert_noop!(Broker::do_renew(1, 1), Error::::NotAllowed); + + let record = AllowedRenewalRecord { + price: 100, + completion: CompletionStatus::Partial(CoreMask::from_chunk(0, 20)), + }; + AllowedRenewals::::insert(AllowedRenewalId { core: 1, when: 4 }, &record); + assert_noop!(Broker::do_renew(1, 1), Error::::IncompleteAssignment); + }); +} + +#[test] +fn cannot_transfer_or_partition_or_interlace_unknown() { + TestExt::new().execute_with(|| { + let region_id = RegionId { begin: 0, core: 0, mask: CoreMask::complete() }; + assert_noop!(Broker::do_transfer(region_id, None, 2), Error::::UnknownRegion); + assert_noop!(Broker::do_partition(region_id, None, 2), Error::::UnknownRegion); + assert_noop!( + Broker::do_interlace(region_id, None, CoreMask::from_chunk(0, 20)), + Error::::UnknownRegion + ); + }); +} + +#[test] +fn check_ownership_for_transfer_or_partition_or_interlace() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_noop!(Broker::do_transfer(region, Some(2), 2), Error::::NotOwner); + assert_noop!(Broker::do_partition(region, Some(2), 2), Error::::NotOwner); + assert_noop!( + Broker::do_interlace(region, Some(2), CoreMask::from_chunk(0, 20)), + Error::::NotOwner + ); + }); +} + +#[test] +fn cannot_partition_invalid_offset() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + assert_noop!(Broker::do_partition(region, None, 0), Error::::PivotTooEarly); + assert_noop!(Broker::do_partition(region, None, 5), Error::::PivotTooLate); + }); +} + +#[test] +fn cannot_interlace_invalid_pivot() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let region = Broker::do_purchase(1, u64::max_value()).unwrap(); + let (region1, _) = Broker::do_interlace(region, None, CoreMask::from_chunk(0, 20)).unwrap(); + assert_noop!( + Broker::do_interlace(region1, None, CoreMask::from_chunk(20, 40)), + Error::::ExteriorPivot + ); + assert_noop!( + Broker::do_interlace(region1, None, CoreMask::void()), + Error::::VoidPivot + ); + assert_noop!( + Broker::do_interlace(region1, None, CoreMask::from_chunk(0, 20)), + Error::::CompletePivot + ); + }); +} + +#[test] +fn assign_should_drop_invalid_region() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let mut region = Broker::do_purchase(1, u64::max_value()).unwrap(); + advance_to(10); + assert_ok!(Broker::do_assign(region, Some(1), 1001, Provisional)); + region.begin = 7; + System::assert_last_event(Event::RegionDropped { region_id: region, duration: 0 }.into()); + }); +} + +#[test] +fn pool_should_drop_invalid_region() { + TestExt::new().endow(1, 1000).execute_with(|| { + assert_ok!(Broker::do_start_sales(100, 1)); + advance_to(2); + let mut region = Broker::do_purchase(1, u64::max_value()).unwrap(); + advance_to(10); + assert_ok!(Broker::do_pool(region, Some(1), 1001, Provisional)); + region.begin = 7; + System::assert_last_event(Event::RegionDropped { region_id: region, duration: 0 }.into()); + }); +} + +#[test] +fn config_works() { + TestExt::new().execute_with(|| { + let mut cfg = new_config(); + // Good config works: + assert_ok!(Broker::configure(Root.into(), cfg.clone())); + // Bad config is a noop: + cfg.leadin_length = 0; + assert_noop!(Broker::configure(Root.into(), cfg), Error::::InvalidConfig); + }); +} diff --git a/frame/broker/src/tick_impls.rs b/frame/broker/src/tick_impls.rs new file mode 100644 index 0000000000000..0677d2793e21a --- /dev/null +++ b/frame/broker/src/tick_impls.rs @@ -0,0 +1,326 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use frame_support::{pallet_prelude::*, weights::WeightMeter}; +use sp_arithmetic::{ + traits::{One, SaturatedConversion, Saturating, Zero}, + FixedPointNumber, +}; +use sp_runtime::traits::ConvertBack; +use sp_std::{vec, vec::Vec}; +use CompletionStatus::Complete; + +impl Pallet { + /// Attempt to tick things along. + /// + /// This may do several things: + /// - Processes notifications of the core count changing + /// - Processes reports of Instantaneous Core Market Revenue + /// - Commit a timeslice + /// - Rotate the sale period + /// - Request revenue information for a previous timeslice + /// - Initialize an instantaneous core pool historical revenue record + pub(crate) fn do_tick() -> Weight { + let (mut status, config) = match (Status::::get(), Configuration::::get()) { + (Some(s), Some(c)) => (s, c), + _ => return Weight::zero(), + }; + + let mut meter = WeightMeter::max_limit(); + + if Self::process_core_count(&mut status) { + meter.consume(T::WeightInfo::process_core_count(status.core_count.into())); + } + + if Self::process_revenue() { + meter.consume(T::WeightInfo::process_revenue()); + } + + if let Some(commit_timeslice) = Self::next_timeslice_to_commit(&config, &status) { + status.last_committed_timeslice = commit_timeslice; + if let Some(sale) = SaleInfo::::get() { + if commit_timeslice >= sale.region_begin { + // Sale can be rotated. + Self::rotate_sale(sale, &config, &status); + meter.consume(T::WeightInfo::rotate_sale(status.core_count.into())); + } + } + + Self::process_pool(commit_timeslice, &mut status); + meter.consume(T::WeightInfo::process_pool()); + + let timeslice_period = T::TimeslicePeriod::get(); + let rc_begin = RelayBlockNumberOf::::from(commit_timeslice) * timeslice_period; + for core in 0..status.core_count { + Self::process_core_schedule(commit_timeslice, rc_begin, core); + meter.consume(T::WeightInfo::process_core_schedule()); + } + } + + let current_timeslice = Self::current_timeslice(); + if status.last_timeslice < current_timeslice { + status.last_timeslice.saturating_inc(); + let rc_block = T::TimeslicePeriod::get() * status.last_timeslice.into(); + T::Coretime::request_revenue_info_at(rc_block); + meter.consume(T::WeightInfo::request_revenue_info_at()); + } + + Status::::put(&status); + + meter.consumed() + } + + pub(crate) fn process_core_count(status: &mut StatusRecord) -> bool { + if let Some(core_count) = T::Coretime::check_notify_core_count() { + status.core_count = core_count; + Self::deposit_event(Event::::CoreCountChanged { core_count }); + return true + } + false + } + + pub(crate) fn process_revenue() -> bool { + let Some((until, amount)) = T::Coretime::check_notify_revenue_info() else { + return false; + }; + let when: Timeslice = + (until / T::TimeslicePeriod::get()).saturating_sub(One::one()).saturated_into(); + let mut revenue = T::ConvertBalance::convert_back(amount); + if revenue.is_zero() { + Self::deposit_event(Event::::HistoryDropped { when, revenue }); + InstaPoolHistory::::remove(when); + return true + } + let mut r = InstaPoolHistory::::get(when).unwrap_or_default(); + if r.maybe_payout.is_some() { + Self::deposit_event(Event::::HistoryIgnored { when, revenue }); + return true + } + // Payout system InstaPool Cores. + let total_contrib = r.system_contributions.saturating_add(r.private_contributions); + let system_payout = + revenue.saturating_mul(r.system_contributions.into()) / total_contrib.into(); + let _ = Self::charge(&Self::account_id(), system_payout); + revenue.saturating_reduce(system_payout); + + if !revenue.is_zero() && r.private_contributions > 0 { + r.maybe_payout = Some(revenue); + InstaPoolHistory::::insert(when, &r); + Self::deposit_event(Event::::ClaimsReady { + when, + system_payout, + private_payout: revenue, + }); + } else { + InstaPoolHistory::::remove(when); + Self::deposit_event(Event::::HistoryDropped { when, revenue }); + } + true + } + + /// Begin selling for the next sale period. + /// + /// Triggered by Relay-chain block number/timeslice. + pub(crate) fn rotate_sale( + old_sale: SaleInfoRecordOf, + config: &ConfigRecordOf, + status: &StatusRecord, + ) -> Option<()> { + let now = frame_system::Pallet::::block_number(); + + let pool_item = + ScheduleItem { assignment: CoreAssignment::Pool, mask: CoreMask::complete() }; + let just_pool = Schedule::truncate_from(vec![pool_item]); + + // Clean up the old sale - we need to use up any unused cores by putting them into the + // InstaPool. + let mut old_pooled: SignedCoreMaskBitCount = 0; + for i in old_sale.cores_sold..old_sale.cores_offered { + old_pooled.saturating_accrue(80); + Workplan::::insert((old_sale.region_begin, old_sale.first_core + i), &just_pool); + } + InstaPoolIo::::mutate(old_sale.region_begin, |r| r.system.saturating_accrue(old_pooled)); + InstaPoolIo::::mutate(old_sale.region_end, |r| r.system.saturating_reduce(old_pooled)); + + // Calculate the start price for the upcoming sale. + let price = { + let offered = old_sale.cores_offered; + let ideal = old_sale.ideal_cores_sold; + let sold = old_sale.cores_sold; + + let maybe_purchase_price = if offered == 0 { + // No cores offered for sale - no purchase price. + None + } else if sold >= ideal { + // Sold more than the ideal amount. We should look for the last purchase price + // before the sell-out. If there was no purchase at all, then we avoid having a + // price here so that we make no alterations to it (since otherwise we would + // increase it). + old_sale.sellout_price + } else { + // Sold less than the ideal - we fall back to the regular price. + Some(old_sale.price) + }; + if let Some(purchase_price) = maybe_purchase_price { + T::PriceAdapter::adapt_price(sold.min(offered), ideal, offered) + .saturating_mul_int(purchase_price) + } else { + old_sale.price + } + }; + + // Set workload for the reserved (system, probably) workloads. + let region_begin = old_sale.region_end; + let region_end = region_begin + config.region_length; + + let mut first_core = 0; + let mut total_pooled: SignedCoreMaskBitCount = 0; + for schedule in Reservations::::get().into_iter() { + let parts: u32 = schedule + .iter() + .filter(|i| matches!(i.assignment, CoreAssignment::Pool)) + .map(|i| i.mask.count_ones()) + .sum(); + total_pooled.saturating_accrue(parts as i32); + + Workplan::::insert((region_begin, first_core), &schedule); + first_core.saturating_inc(); + } + InstaPoolIo::::mutate(region_begin, |r| r.system.saturating_accrue(total_pooled)); + InstaPoolIo::::mutate(region_end, |r| r.system.saturating_reduce(total_pooled)); + + let mut leases = Leases::::get(); + // Can morph to a renewable as long as it's >=begin and ::insert((region_begin, first_core), &schedule); + let expiring = until >= region_begin && until < region_end; + if expiring { + // last time for this one - make it renewable. + let renewal_id = AllowedRenewalId { core: first_core, when: region_end }; + let record = AllowedRenewalRecord { price, completion: Complete(schedule) }; + AllowedRenewals::::insert(renewal_id, &record); + Self::deposit_event(Event::Renewable { + core: first_core, + price, + begin: region_end, + workload: record.completion.drain_complete().unwrap_or_default(), + }); + Self::deposit_event(Event::LeaseEnding { when: region_end, task }); + } + first_core.saturating_inc(); + !expiring + }); + Leases::::put(&leases); + + let max_possible_sales = status.core_count.saturating_sub(first_core); + let limit_cores_offered = config.limit_cores_offered.unwrap_or(CoreIndex::max_value()); + let cores_offered = limit_cores_offered.min(max_possible_sales); + let sale_start = now.saturating_add(config.interlude_length); + let leadin_length = config.leadin_length; + let ideal_cores_sold = (config.ideal_bulk_proportion * cores_offered as u32) as u16; + // Update SaleInfo + let new_sale = SaleInfoRecord { + sale_start, + leadin_length, + price, + sellout_price: None, + region_begin, + region_end, + first_core, + ideal_cores_sold, + cores_offered, + cores_sold: 0, + }; + SaleInfo::::put(&new_sale); + Self::deposit_event(Event::SaleInitialized { + sale_start, + leadin_length, + start_price: Self::sale_price(&new_sale, now), + regular_price: price, + region_begin, + region_end, + ideal_cores_sold, + cores_offered, + }); + + Some(()) + } + + pub(crate) fn process_pool(when: Timeslice, status: &mut StatusRecord) { + let pool_io = InstaPoolIo::::take(when); + status.private_pool_size = (status.private_pool_size as SignedCoreMaskBitCount) + .saturating_add(pool_io.private) as CoreMaskBitCount; + status.system_pool_size = (status.system_pool_size as SignedCoreMaskBitCount) + .saturating_add(pool_io.system) as CoreMaskBitCount; + let record = InstaPoolHistoryRecord { + private_contributions: status.private_pool_size, + system_contributions: status.system_pool_size, + maybe_payout: None, + }; + InstaPoolHistory::::insert(when, record); + Self::deposit_event(Event::::HistoryInitialized { + when, + private_pool_size: status.private_pool_size, + system_pool_size: status.system_pool_size, + }); + } + + /// Schedule cores for the given `timeslice`. + pub(crate) fn process_core_schedule( + timeslice: Timeslice, + rc_begin: RelayBlockNumberOf, + core: CoreIndex, + ) { + let Some(workplan) = Workplan::::take((timeslice, core)) else { + return; + }; + let workload = Workload::::get(core); + let parts_used = workplan.iter().map(|i| i.mask).fold(CoreMask::void(), |a, i| a | i); + let mut workplan = workplan.into_inner(); + workplan.extend(workload.into_iter().filter(|i| (i.mask & parts_used).is_void())); + let workplan = Schedule::truncate_from(workplan); + Workload::::insert(core, &workplan); + + let mut total_used = 0; + let mut intermediate = workplan + .into_iter() + .map(|i| (i.assignment, i.mask.count_ones() as u16 * (57_600 / 80))) + .inspect(|i| total_used.saturating_accrue(i.1)) + .collect::>(); + if total_used < 57_600 { + intermediate.push((CoreAssignment::Idle, 57_600 - total_used)); + } + intermediate.sort(); + let mut assignment: Vec<(CoreAssignment, PartsOf57600)> = + Vec::with_capacity(intermediate.len()); + for i in intermediate.into_iter() { + if let Some(ref mut last) = assignment.last_mut() { + if last.0 == i.0 { + last.1 += i.1; + continue + } + } + assignment.push(i); + } + T::Coretime::assign_core(core, rc_begin, assignment.clone(), None); + Self::deposit_event(Event::::CoreAssigned { core, when: rc_begin, assignment }); + } +} diff --git a/frame/broker/src/types.rs b/frame/broker/src/types.rs new file mode 100644 index 0000000000000..89222ca8e9527 --- /dev/null +++ b/frame/broker/src/types.rs @@ -0,0 +1,290 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + Config, CoreAssignment, CoreIndex, CoreMask, CoretimeInterface, TaskId, CORE_MASK_BITS, +}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::fungible::Inspect; +use frame_system::{pallet_prelude::BlockNumberFor, Config as SConfig}; +use scale_info::TypeInfo; +use sp_arithmetic::Perbill; +use sp_core::{ConstU32, RuntimeDebug}; +use sp_runtime::BoundedVec; + +pub type BalanceOf = <::Currency as Inspect<::AccountId>>::Balance; +pub type RelayBalanceOf = <::Coretime as CoretimeInterface>::Balance; +pub type RelayBlockNumberOf = <::Coretime as CoretimeInterface>::BlockNumber; +pub type RelayAccountIdOf = <::Coretime as CoretimeInterface>::AccountId; + +/// Relay-chain block number with a fixed divisor of Config::TimeslicePeriod. +pub type Timeslice = u32; +/// Counter for the total number of set bits over every core's `CoreMask`. `u32` so we don't +/// ever get an overflow. This is 1/80th of a Polkadot Core per timeslice. Assuming timeslices are +/// 80 blocks, then this indicates usage of a single core one time over a timeslice. +pub type CoreMaskBitCount = u32; +/// The same as `CoreMaskBitCount` but signed. +pub type SignedCoreMaskBitCount = i32; + +/// Whether a core assignment is revokable or not. +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum Finality { + /// The region remains with the same owner allowing the assignment to be altered. + Provisional, + /// The region is removed; the assignment may be eligible for renewal. + Final, +} + +/// Self-describing identity for a Region of Bulk Coretime. +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct RegionId { + /// The timeslice at which this Region begins. + pub begin: Timeslice, + /// The index of the Polakdot Core on which this Region will be scheduled. + pub core: CoreIndex, + /// The regularity parts in which this Region will be scheduled. + pub mask: CoreMask, +} +impl From for RegionId { + fn from(x: u128) -> Self { + Self { begin: (x >> 96) as u32, core: (x >> 80) as u16, mask: x.into() } + } +} +impl From for u128 { + fn from(x: RegionId) -> Self { + (x.begin as u128) << 96 | (x.core as u128) << 80 | u128::from(x.mask) + } +} +#[test] +fn region_id_converts_u128() { + let r = RegionId { begin: 0x12345678u32, core: 0xabcdu16, mask: 0xdeadbeefcafef00d0123.into() }; + let u = 0x12345678_abcd_deadbeefcafef00d0123u128; + assert_eq!(RegionId::from(u), r); + assert_eq!(u128::from(r), u); +} + +/// The rest of the information describing a Region. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct RegionRecord { + /// The end of the Region. + pub end: Timeslice, + /// The owner of the Region. + pub owner: AccountId, + /// The amount paid to Polkadot for this Region, or `None` if renewal is not allowed. + pub paid: Option, +} +pub type RegionRecordOf = RegionRecord<::AccountId, BalanceOf>; + +/// An distinct item which can be scheduled on a Polkadot Core. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ScheduleItem { + /// The regularity parts in which this Item will be scheduled on the Core. + pub mask: CoreMask, + /// The job that the Core should be doing. + pub assignment: CoreAssignment, +} +pub type Schedule = BoundedVec>; + +/// The record body of a Region which was contributed to the Instantaneous Coretime Pool. This helps +/// with making pro rata payments to contributors. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ContributionRecord { + /// The end of the Region contributed. + pub length: Timeslice, + /// The identity of the contributor. + pub payee: AccountId, +} +pub type ContributionRecordOf = ContributionRecord<::AccountId>; + +/// A per-timeslice bookkeeping record for tracking Instantaneous Coretime Pool activity and +/// making proper payments to contributors. +#[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct InstaPoolHistoryRecord { + /// The total amount of Coretime (measured in Core Mask Bits minus any contributions which have + /// already been paid out. + pub private_contributions: CoreMaskBitCount, + /// The total amount of Coretime (measured in Core Mask Bits contributed by the Polkadot System + /// in this timeslice. + pub system_contributions: CoreMaskBitCount, + /// The payout remaining for the `private_contributions`, or `None` if the revenue is not yet + /// known. + pub maybe_payout: Option, +} +pub type InstaPoolHistoryRecordOf = InstaPoolHistoryRecord>; + +/// How much of a core has been assigned or, if completely assigned, the workload itself. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum CompletionStatus { + /// The core is not fully assigned; the inner is the parts which have. + Partial(CoreMask), + /// The core is fully assigned; the inner is the workload which has been assigned. + Complete(Schedule), +} +impl CompletionStatus { + /// Return reference to the complete workload, or `None` if incomplete. + pub fn complete(&self) -> Option<&Schedule> { + match self { + Self::Complete(s) => Some(s), + Self::Partial(_) => None, + } + } + /// Return the complete workload, or `None` if incomplete. + pub fn drain_complete(self) -> Option { + match self { + Self::Complete(s) => Some(s), + Self::Partial(_) => None, + } + } +} + +/// The identity of a possible Core workload renewal. +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct AllowedRenewalId { + /// The core whose workload at the sale ending with `when` may be renewed to begin at `when`. + pub core: CoreIndex, + /// The point in time that the renewable workload on `core` ends and a fresh renewal may begin. + pub when: Timeslice, +} + +/// A record of an allowed renewal. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct AllowedRenewalRecord { + /// The price for which the next renewal can be made. + pub price: Balance, + /// The workload which will be scheduled on the Core in the case a renewal is made, or if + /// incomplete, then the parts of the core which have been scheduled. + pub completion: CompletionStatus, +} +pub type AllowedRenewalRecordOf = AllowedRenewalRecord>; + +/// General status of the system. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct StatusRecord { + /// The total number of cores which can be assigned (one plus the maximum index which can + /// be used in `Coretime::assign`). + pub core_count: CoreIndex, + /// The current size of the Instantaneous Coretime Pool, measured in + /// Core Mask Bits. + pub private_pool_size: CoreMaskBitCount, + /// The current amount of the Instantaneous Coretime Pool which is provided by the Polkadot + /// System, rather than provided as a result of privately operated Coretime. + pub system_pool_size: CoreMaskBitCount, + /// The last (Relay-chain) timeslice which we committed to the Relay-chain. + pub last_committed_timeslice: Timeslice, + /// The timeslice of the last time we ticked. + pub last_timeslice: Timeslice, +} + +/// A record of flux in the InstaPool. +#[derive( + Encode, Decode, Clone, Copy, Default, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, +)] +pub struct PoolIoRecord { + /// The total change of the portion of the pool supplied by purchased Bulk Coretime, measured + /// in Core Mask Bits. + pub private: SignedCoreMaskBitCount, + /// The total change of the portion of the pool supplied by the Polkaot System, measured in + /// Core Mask Bits. + pub system: SignedCoreMaskBitCount, +} + +/// The status of a Bulk Coretime Sale. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct SaleInfoRecord { + /// The local block number at which the sale will/did start. + pub sale_start: BlockNumber, + /// The length in blocks of the Leadin Period (where the price is decreasing). + pub leadin_length: BlockNumber, + /// The price of Bulk Coretime after the Leadin Period. + pub price: Balance, + /// The first timeslice of the Regions which are being sold in this sale. + pub region_begin: Timeslice, + /// The timeslice on which the Regions which are being sold in the sale terminate. (i.e. One + /// after the last timeslice which the Regions control.) + pub region_end: Timeslice, + /// The number of cores we want to sell, ideally. Selling this amount would result in no + /// change to the price for the next sale. + pub ideal_cores_sold: CoreIndex, + /// Number of cores which are/have been offered for sale. + pub cores_offered: CoreIndex, + /// The index of the first core which is for sale. Core of Regions which are sold have + /// incrementing indices from this. + pub first_core: CoreIndex, + /// The latest price at which Bulk Coretime was purchased until surpassing the ideal number of + /// cores were sold. + pub sellout_price: Option, + /// Number of cores which have been sold; never more than cores_offered. + pub cores_sold: CoreIndex, +} +pub type SaleInfoRecordOf = SaleInfoRecord, BlockNumberFor>; + +/// Record for Polkadot Core reservations (generally tasked with the maintenance of System +/// Chains). +pub type ReservationsRecord = BoundedVec; +pub type ReservationsRecordOf = ReservationsRecord<::MaxReservedCores>; + +/// Information on a single legacy lease. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct LeaseRecordItem { + /// The timeslice until the lease is valid. + pub until: Timeslice, + /// The task which the lease is for. + pub task: TaskId, +} + +/// Record for Polkadot Core legacy leases. +pub type LeasesRecord = BoundedVec; +pub type LeasesRecordOf = LeasesRecord<::MaxLeasedCores>; + +/// Configuration of this pallet. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ConfigRecord { + /// The number of Relay-chain blocks in advance which scheduling should be fixed and the + /// `Coretime::assign` API used to inform the Relay-chain. + pub advance_notice: RelayBlockNumber, + /// The length in blocks of the Interlude Period for forthcoming sales. + pub interlude_length: BlockNumber, + /// The length in blocks of the Leadin Period for forthcoming sales. + pub leadin_length: BlockNumber, + /// The length in timeslices of Regions which are up for sale in forthcoming sales. + pub region_length: Timeslice, + /// The proportion of cores available for sale which should be sold in order for the price + /// to remain the same in the next sale. + pub ideal_bulk_proportion: Perbill, + /// An artificial limit to the number of cores which are allowed to be sold. If `Some` then + /// no more cores will be sold than this. + pub limit_cores_offered: Option, + /// The amount by which the renewal price increases each sale period. + pub renewal_bump: Perbill, + /// The duration by which rewards for contributions to the InstaPool must be collected. + pub contribution_timeout: Timeslice, +} +pub type ConfigRecordOf = ConfigRecord, RelayBlockNumberOf>; + +impl ConfigRecord +where + BlockNumber: sp_arithmetic::traits::Zero, +{ + /// Check the config for basic validity constraints. + pub(crate) fn validate(&self) -> Result<(), ()> { + if self.leadin_length.is_zero() { + return Err(()) + } + + Ok(()) + } +} diff --git a/frame/broker/src/utility_impls.rs b/frame/broker/src/utility_impls.rs new file mode 100644 index 0000000000000..99c4de32f7767 --- /dev/null +++ b/frame/broker/src/utility_impls.rs @@ -0,0 +1,121 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use frame_support::{ + pallet_prelude::{DispatchResult, *}, + traits::{ + fungible::Balanced, + tokens::{Fortitude::Polite, Precision::Exact, Preservation::Expendable}, + OnUnbalanced, + }, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_arithmetic::{ + traits::{SaturatedConversion, Saturating}, + FixedPointNumber, FixedU64, +}; +use sp_runtime::traits::AccountIdConversion; + +impl Pallet { + pub fn current_timeslice() -> Timeslice { + let latest = T::Coretime::latest(); + let timeslice_period = T::TimeslicePeriod::get(); + (latest / timeslice_period).saturated_into() + } + + pub fn latest_timeslice_ready_to_commit(config: &ConfigRecordOf) -> Timeslice { + let latest = T::Coretime::latest(); + let advanced = latest.saturating_add(config.advance_notice); + let timeslice_period = T::TimeslicePeriod::get(); + (advanced / timeslice_period).saturated_into() + } + + pub fn next_timeslice_to_commit( + config: &ConfigRecordOf, + status: &StatusRecord, + ) -> Option { + if status.last_committed_timeslice < Self::latest_timeslice_ready_to_commit(config) { + Some(status.last_committed_timeslice + 1) + } else { + None + } + } + + pub fn account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + pub fn sale_price(sale: &SaleInfoRecordOf, now: BlockNumberFor) -> BalanceOf { + let num = now.saturating_sub(sale.sale_start).min(sale.leadin_length).saturated_into(); + let through = FixedU64::from_rational(num, sale.leadin_length.saturated_into()); + T::PriceAdapter::leadin_factor_at(through).saturating_mul_int(sale.price) + } + + pub(crate) fn charge(who: &T::AccountId, amount: BalanceOf) -> DispatchResult { + let credit = T::Currency::withdraw(&who, amount, Exact, Expendable, Polite)?; + T::OnRevenue::on_unbalanced(credit); + Ok(()) + } + + pub(crate) fn issue( + core: CoreIndex, + begin: Timeslice, + end: Timeslice, + owner: T::AccountId, + paid: Option>, + ) -> RegionId { + let id = RegionId { begin, core, mask: CoreMask::complete() }; + let record = RegionRecord { end, owner, paid }; + Regions::::insert(&id, &record); + id + } + + pub(crate) fn utilize( + mut region_id: RegionId, + maybe_check_owner: Option, + finality: Finality, + ) -> Result)>, Error> { + let status = Status::::get().ok_or(Error::::Uninitialized)?; + let region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; + + if let Some(check_owner) = maybe_check_owner { + ensure!(check_owner == region.owner, Error::::NotOwner); + } + + Regions::::remove(®ion_id); + + let last_committed_timeslice = status.last_committed_timeslice; + if region_id.begin <= last_committed_timeslice { + region_id.begin = last_committed_timeslice + 1; + if region_id.begin >= region.end { + let duration = region.end.saturating_sub(region_id.begin); + Self::deposit_event(Event::RegionDropped { region_id, duration }); + return Ok(None) + } + } else { + Workplan::::mutate_extant((region_id.begin, region_id.core), |p| { + p.retain(|i| (i.mask & region_id.mask).is_void()) + }); + } + if finality == Finality::Provisional { + Regions::::insert(®ion_id, ®ion); + } + + Ok(Some((region_id, region))) + } +} diff --git a/frame/broker/src/weights.rs b/frame/broker/src/weights.rs new file mode 100644 index 0000000000000..93b568bf2a035 --- /dev/null +++ b/frame/broker/src/weights.rs @@ -0,0 +1,794 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_broker` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-07-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-ynta1nyy-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/production/substrate-node +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/substrate/.git/.artifacts/bench.json +// --pallet=pallet_broker +// --chain=dev +// --header=./HEADER-APACHE2 +// --output=./frame/broker/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_broker`. +pub trait WeightInfo { + fn configure() -> Weight; + fn reserve() -> Weight; + fn unreserve() -> Weight; + fn set_lease() -> Weight; + fn start_sales(n: u32, ) -> Weight; + fn purchase() -> Weight; + fn renew() -> Weight; + fn transfer() -> Weight; + fn partition() -> Weight; + fn interlace() -> Weight; + fn assign() -> Weight; + fn pool() -> Weight; + fn claim_revenue(m: u32, ) -> Weight; + fn purchase_credit() -> Weight; + fn drop_region() -> Weight; + fn drop_contribution() -> Weight; + fn drop_history() -> Weight; + fn drop_renewal() -> Weight; + fn request_core_count(n: u32, ) -> Weight; + fn process_core_count(n: u32, ) -> Weight; + fn process_revenue() -> Weight; + fn rotate_sale(n: u32, ) -> Weight; + fn process_pool() -> Weight; + fn process_core_schedule() -> Weight; + fn request_revenue_info_at() -> Weight; +} + +/// Weights for `pallet_broker` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Broker::Configuration` (r:0 w:1) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + fn configure() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_448_000 picoseconds. + Weight::from_parts(3_729_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Reservations` (r:1 w:1) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + fn reserve() -> Weight { + // Proof Size summary in bytes: + // Measured: `5016` + // Estimated: `7496` + // Minimum execution time: 22_537_000 picoseconds. + Weight::from_parts(23_335_000, 7496) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Reservations` (r:1 w:1) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + fn unreserve() -> Weight { + // Proof Size summary in bytes: + // Measured: `6218` + // Estimated: `7496` + // Minimum execution time: 21_668_000 picoseconds. + Weight::from_parts(22_442_000, 7496) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Leases` (r:1 w:1) + /// Proof: `Broker::Leases` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + fn set_lease() -> Weight { + // Proof Size summary in bytes: + // Measured: `239` + // Estimated: `1526` + // Minimum execution time: 13_606_000 picoseconds. + Weight::from_parts(14_104_000, 1526) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolIo` (r:3 w:3) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::Reservations` (r:1 w:0) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + /// Storage: `Broker::Leases` (r:1 w:1) + /// Proof: `Broker::Leases` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:0 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:0 w:1) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:0 w:10) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 1000]`. + fn start_sales(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `6330` + // Estimated: `8499` + // Minimum execution time: 64_012_000 picoseconds. + Weight::from_parts(67_819_922, 8499) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(16_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:1 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Authorship::Author` (r:1 w:0) + /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `System::Digest` (r:1 w:0) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Broker::Regions` (r:0 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn purchase() -> Weight { + // Proof Size summary in bytes: + // Measured: `568` + // Estimated: `2053` + // Minimum execution time: 48_110_000 picoseconds. + Weight::from_parts(49_234_000, 2053) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:1 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Broker::AllowedRenewals` (r:1 w:2) + /// Proof: `Broker::AllowedRenewals` (`max_values`: None, `max_size`: Some(1233), added: 3708, mode: `MaxEncodedLen`) + /// Storage: `Authorship::Author` (r:1 w:0) + /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `System::Digest` (r:1 w:0) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Broker::Workplan` (r:0 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + fn renew() -> Weight { + // Proof Size summary in bytes: + // Measured: `686` + // Estimated: `4698` + // Minimum execution time: 69_580_000 picoseconds. + Weight::from_parts(70_914_000, 4698) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3550` + // Minimum execution time: 17_687_000 picoseconds. + Weight::from_parts(18_573_000, 3550) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Regions` (r:1 w:2) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn partition() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3550` + // Minimum execution time: 19_675_000 picoseconds. + Weight::from_parts(20_234_000, 3550) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Regions` (r:1 w:2) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn interlace() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3550` + // Minimum execution time: 19_426_000 picoseconds. + Weight::from_parts(20_414_000, 3550) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:1 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + fn assign() -> Weight { + // Proof Size summary in bytes: + // Measured: `740` + // Estimated: `4681` + // Minimum execution time: 31_751_000 picoseconds. + Weight::from_parts(32_966_000, 4681) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:1 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolIo` (r:2 w:2) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolContribution` (r:0 w:1) + /// Proof: `Broker::InstaPoolContribution` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `775` + // Estimated: `5996` + // Minimum execution time: 36_709_000 picoseconds. + Weight::from_parts(38_930_000, 5996) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Broker::InstaPoolContribution` (r:1 w:1) + /// Proof: `Broker::InstaPoolContribution` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolHistory` (r:3 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `m` is `[1, 3]`. + fn claim_revenue(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `720` + // Estimated: `6196 + m * (2520 ±0)` + // Minimum execution time: 55_510_000 picoseconds. + Weight::from_parts(56_665_061, 6196) + // Standard Error: 61_729 + .saturating_add(Weight::from_parts(1_724_824, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(m.into()))) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 2520).saturating_mul(m.into())) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn purchase_credit() -> Weight { + // Proof Size summary in bytes: + // Measured: `103` + // Estimated: `3593` + // Minimum execution time: 44_992_000 picoseconds. + Weight::from_parts(46_225_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn drop_region() -> Weight { + // Proof Size summary in bytes: + // Measured: `603` + // Estimated: `3550` + // Minimum execution time: 28_207_000 picoseconds. + Weight::from_parts(28_707_000, 3550) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolContribution` (r:1 w:1) + /// Proof: `Broker::InstaPoolContribution` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn drop_contribution() -> Weight { + // Proof Size summary in bytes: + // Measured: `601` + // Estimated: `3533` + // Minimum execution time: 31_813_000 picoseconds. + Weight::from_parts(32_612_000, 3533) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolHistory` (r:1 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn drop_history() -> Weight { + // Proof Size summary in bytes: + // Measured: `829` + // Estimated: `3593` + // Minimum execution time: 38_571_000 picoseconds. + Weight::from_parts(39_493_000, 3593) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::AllowedRenewals` (r:1 w:1) + /// Proof: `Broker::AllowedRenewals` (`max_values`: None, `max_size`: Some(1233), added: 3708, mode: `MaxEncodedLen`) + fn drop_renewal() -> Weight { + // Proof Size summary in bytes: + // Measured: `525` + // Estimated: `4698` + // Minimum execution time: 24_714_000 picoseconds. + Weight::from_parts(25_288_000, 4698) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:0 w:1) + /// Proof: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:0 w:1) + /// The range of component `n` is `[0, 1000]`. + fn request_core_count(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_258_000 picoseconds. + Weight::from_parts(7_925_570, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:1 w:0) + /// Proof: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:1 w:0) + /// The range of component `n` is `[0, 1000]`. + fn process_core_count(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `97` + // Estimated: `3562` + // Minimum execution time: 7_136_000 picoseconds. + Weight::from_parts(7_788_194, 3562) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Broker::InstaPoolHistory` (r:0 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + fn process_revenue() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_049_000 picoseconds. + Weight::from_parts(6_311_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::InstaPoolIo` (r:3 w:3) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::Reservations` (r:1 w:0) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + /// Storage: `Broker::Leases` (r:1 w:1) + /// Proof: `Broker::Leases` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:0 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:0 w:10) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 1000]`. + fn rotate_sale(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `6281` + // Estimated: `8499` + // Minimum execution time: 47_504_000 picoseconds. + Weight::from_parts(49_778_098, 8499) + // Standard Error: 109 + .saturating_add(Weight::from_parts(427, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(15_u64)) + } + /// Storage: `Broker::InstaPoolIo` (r:1 w:0) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolHistory` (r:0 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + fn process_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `180` + // Estimated: `3493` + // Minimum execution time: 9_573_000 picoseconds. + Weight::from_parts(10_034_000, 3493) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Workplan` (r:1 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workload` (r:1 w:1) + /// Proof: `Broker::Workload` (`max_values`: None, `max_size`: Some(1212), added: 3687, mode: `MaxEncodedLen`) + fn process_core_schedule() -> Weight { + // Proof Size summary in bytes: + // Measured: `1423` + // Estimated: `4681` + // Minimum execution time: 21_331_000 picoseconds. + Weight::from_parts(22_235_000, 4681) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + fn request_revenue_info_at() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 191_000 picoseconds. + Weight::from_parts(234_000, 0) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Broker::Configuration` (r:0 w:1) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + fn configure() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_448_000 picoseconds. + Weight::from_parts(3_729_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Reservations` (r:1 w:1) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + fn reserve() -> Weight { + // Proof Size summary in bytes: + // Measured: `5016` + // Estimated: `7496` + // Minimum execution time: 22_537_000 picoseconds. + Weight::from_parts(23_335_000, 7496) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Reservations` (r:1 w:1) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + fn unreserve() -> Weight { + // Proof Size summary in bytes: + // Measured: `6218` + // Estimated: `7496` + // Minimum execution time: 21_668_000 picoseconds. + Weight::from_parts(22_442_000, 7496) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Leases` (r:1 w:1) + /// Proof: `Broker::Leases` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + fn set_lease() -> Weight { + // Proof Size summary in bytes: + // Measured: `239` + // Estimated: `1526` + // Minimum execution time: 13_606_000 picoseconds. + Weight::from_parts(14_104_000, 1526) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolIo` (r:3 w:3) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::Reservations` (r:1 w:0) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + /// Storage: `Broker::Leases` (r:1 w:1) + /// Proof: `Broker::Leases` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:0 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:0 w:1) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:0 w:10) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 1000]`. + fn start_sales(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `6330` + // Estimated: `8499` + // Minimum execution time: 64_012_000 picoseconds. + Weight::from_parts(67_819_922, 8499) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(16_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:1 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Authorship::Author` (r:1 w:0) + /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `System::Digest` (r:1 w:0) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Broker::Regions` (r:0 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn purchase() -> Weight { + // Proof Size summary in bytes: + // Measured: `568` + // Estimated: `2053` + // Minimum execution time: 48_110_000 picoseconds. + Weight::from_parts(49_234_000, 2053) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:1 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Broker::AllowedRenewals` (r:1 w:2) + /// Proof: `Broker::AllowedRenewals` (`max_values`: None, `max_size`: Some(1233), added: 3708, mode: `MaxEncodedLen`) + /// Storage: `Authorship::Author` (r:1 w:0) + /// Proof: `Authorship::Author` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `System::Digest` (r:1 w:0) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Broker::Workplan` (r:0 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + fn renew() -> Weight { + // Proof Size summary in bytes: + // Measured: `686` + // Estimated: `4698` + // Minimum execution time: 69_580_000 picoseconds. + Weight::from_parts(70_914_000, 4698) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn transfer() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3550` + // Minimum execution time: 17_687_000 picoseconds. + Weight::from_parts(18_573_000, 3550) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Regions` (r:1 w:2) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn partition() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3550` + // Minimum execution time: 19_675_000 picoseconds. + Weight::from_parts(20_234_000, 3550) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Regions` (r:1 w:2) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn interlace() -> Weight { + // Proof Size summary in bytes: + // Measured: `495` + // Estimated: `3550` + // Minimum execution time: 19_426_000 picoseconds. + Weight::from_parts(20_414_000, 3550) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:1 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + fn assign() -> Weight { + // Proof Size summary in bytes: + // Measured: `740` + // Estimated: `4681` + // Minimum execution time: 31_751_000 picoseconds. + Weight::from_parts(32_966_000, 4681) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:1 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolIo` (r:2 w:2) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolContribution` (r:0 w:1) + /// Proof: `Broker::InstaPoolContribution` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `775` + // Estimated: `5996` + // Minimum execution time: 36_709_000 picoseconds. + Weight::from_parts(38_930_000, 5996) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Broker::InstaPoolContribution` (r:1 w:1) + /// Proof: `Broker::InstaPoolContribution` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolHistory` (r:3 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `m` is `[1, 3]`. + fn claim_revenue(m: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `720` + // Estimated: `6196 + m * (2520 ±0)` + // Minimum execution time: 55_510_000 picoseconds. + Weight::from_parts(56_665_061, 6196) + // Standard Error: 61_729 + .saturating_add(Weight::from_parts(1_724_824, 0).saturating_mul(m.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(m.into()))) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(Weight::from_parts(0, 2520).saturating_mul(m.into())) + } + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn purchase_credit() -> Weight { + // Proof Size summary in bytes: + // Measured: `103` + // Estimated: `3593` + // Minimum execution time: 44_992_000 picoseconds. + Weight::from_parts(46_225_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::Regions` (r:1 w:1) + /// Proof: `Broker::Regions` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn drop_region() -> Weight { + // Proof Size summary in bytes: + // Measured: `603` + // Estimated: `3550` + // Minimum execution time: 28_207_000 picoseconds. + Weight::from_parts(28_707_000, 3550) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolContribution` (r:1 w:1) + /// Proof: `Broker::InstaPoolContribution` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn drop_contribution() -> Weight { + // Proof Size summary in bytes: + // Measured: `601` + // Estimated: `3533` + // Minimum execution time: 31_813_000 picoseconds. + Weight::from_parts(32_612_000, 3533) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Configuration` (r:1 w:0) + /// Proof: `Broker::Configuration` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolHistory` (r:1 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn drop_history() -> Weight { + // Proof Size summary in bytes: + // Measured: `829` + // Estimated: `3593` + // Minimum execution time: 38_571_000 picoseconds. + Weight::from_parts(39_493_000, 3593) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Status` (r:1 w:0) + /// Proof: `Broker::Status` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + /// Storage: `Broker::AllowedRenewals` (r:1 w:1) + /// Proof: `Broker::AllowedRenewals` (`max_values`: None, `max_size`: Some(1233), added: 3708, mode: `MaxEncodedLen`) + fn drop_renewal() -> Weight { + // Proof Size summary in bytes: + // Measured: `525` + // Estimated: `4698` + // Minimum execution time: 24_714_000 picoseconds. + Weight::from_parts(25_288_000, 4698) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:0 w:1) + /// Proof: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:0 w:1) + /// The range of component `n` is `[0, 1000]`. + fn request_core_count(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 7_258_000 picoseconds. + Weight::from_parts(7_925_570, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:1 w:0) + /// Proof: UNKNOWN KEY `0x18194fcb5c1fcace44d2d0a004272614` (r:1 w:0) + /// The range of component `n` is `[0, 1000]`. + fn process_core_count(_n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `97` + // Estimated: `3562` + // Minimum execution time: 7_136_000 picoseconds. + Weight::from_parts(7_788_194, 3562) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Broker::InstaPoolHistory` (r:0 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + fn process_revenue() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 6_049_000 picoseconds. + Weight::from_parts(6_311_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::InstaPoolIo` (r:3 w:3) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::Reservations` (r:1 w:0) + /// Proof: `Broker::Reservations` (`max_values`: Some(1), `max_size`: Some(6011), added: 6506, mode: `MaxEncodedLen`) + /// Storage: `Broker::Leases` (r:1 w:1) + /// Proof: `Broker::Leases` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Storage: `Broker::SaleInfo` (r:0 w:1) + /// Proof: `Broker::SaleInfo` (`max_values`: Some(1), `max_size`: Some(57), added: 552, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workplan` (r:0 w:10) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 1000]`. + fn rotate_sale(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `6281` + // Estimated: `8499` + // Minimum execution time: 47_504_000 picoseconds. + Weight::from_parts(49_778_098, 8499) + // Standard Error: 109 + .saturating_add(Weight::from_parts(427, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(15_u64)) + } + /// Storage: `Broker::InstaPoolIo` (r:1 w:0) + /// Proof: `Broker::InstaPoolIo` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Broker::InstaPoolHistory` (r:0 w:1) + /// Proof: `Broker::InstaPoolHistory` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + fn process_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `180` + // Estimated: `3493` + // Minimum execution time: 9_573_000 picoseconds. + Weight::from_parts(10_034_000, 3493) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Broker::Workplan` (r:1 w:1) + /// Proof: `Broker::Workplan` (`max_values`: None, `max_size`: Some(1216), added: 3691, mode: `MaxEncodedLen`) + /// Storage: `Broker::Workload` (r:1 w:1) + /// Proof: `Broker::Workload` (`max_values`: None, `max_size`: Some(1212), added: 3687, mode: `MaxEncodedLen`) + fn process_core_schedule() -> Weight { + // Proof Size summary in bytes: + // Measured: `1423` + // Estimated: `4681` + // Minimum execution time: 21_331_000 picoseconds. + Weight::from_parts(22_235_000, 4681) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + fn request_revenue_info_at() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 191_000 picoseconds. + Weight::from_parts(234_000, 0) + } +} diff --git a/frame/core-fellowship/src/lib.rs b/frame/core-fellowship/src/lib.rs index 5eb7bf8caba0e..ace614d2bddb9 100644 --- a/frame/core-fellowship/src/lib.rs +++ b/frame/core-fellowship/src/lib.rs @@ -328,7 +328,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::set_params())] #[pallet::call_index(1)] pub fn set_params(origin: OriginFor, params: Box>) -> DispatchResult { - T::ParamsOrigin::try_origin(origin).map(|_| ()).or_else(|o| ensure_root(o))?; + T::ParamsOrigin::ensure_origin_or_root(origin)?; Params::::put(params.as_ref()); Self::deposit_event(Event::::ParamsChanged { params: *params }); Ok(()) diff --git a/frame/glutton/src/lib.rs b/frame/glutton/src/lib.rs index 5950bd676b378..c76cc30017cf0 100644 --- a/frame/glutton/src/lib.rs +++ b/frame/glutton/src/lib.rs @@ -227,7 +227,7 @@ pub mod pallet { new_count: u32, witness_count: Option, ) -> DispatchResult { - T::AdminOrigin::try_origin(origin).map(|_| ()).or_else(|o| ensure_root(o))?; + T::AdminOrigin::ensure_origin_or_root(origin)?; let current_count = TrashDataCount::::get(); ensure!( @@ -252,7 +252,7 @@ pub mod pallet { /// Only callable by Root or `AdminOrigin`. #[pallet::call_index(1)] pub fn set_compute(origin: OriginFor, compute: FixedU64) -> DispatchResult { - T::AdminOrigin::try_origin(origin).map(|_| ()).or_else(|o| ensure_root(o))?; + T::AdminOrigin::ensure_origin_or_root(origin)?; ensure!(compute <= RESOURCE_HARD_LIMIT, Error::::InsaneLimit); Compute::::set(compute); @@ -262,7 +262,7 @@ pub mod pallet { } /// Set how much of the remaining `proof_size` weight should be consumed by `on_idle`. - // + /// /// `1.0` means that all remaining `proof_size` will be consumed. The PoV benchmarking /// results that are used here are likely an over-estimation. 100% intended consumption will /// therefore translate to less than 100% actual consumption. @@ -270,7 +270,7 @@ pub mod pallet { /// Only callable by Root or `AdminOrigin`. #[pallet::call_index(2)] pub fn set_storage(origin: OriginFor, storage: FixedU64) -> DispatchResult { - T::AdminOrigin::try_origin(origin).map(|_| ()).or_else(|o| ensure_root(o))?; + T::AdminOrigin::ensure_origin_or_root(origin)?; ensure!(storage <= RESOURCE_HARD_LIMIT, Error::::InsaneLimit); Storage::::set(storage); diff --git a/frame/support/src/storage/mod.rs b/frame/support/src/storage/mod.rs index 36e2c47383d19..d52908fa366c6 100644 --- a/frame/support/src/storage/mod.rs +++ b/frame/support/src/storage/mod.rs @@ -111,6 +111,15 @@ pub trait StorageValue { /// Mutate the value fn mutate R>(f: F) -> R; + /// Mutate the value under a key if the value already exists. Do nothing and return the default + /// value if not. + fn mutate_extant R>(f: F) -> R { + Self::mutate_exists(|maybe_v| match maybe_v { + Some(ref mut value) => f(value), + None => R::default(), + }) + } + /// Mutate the value if closure returns `Ok` fn try_mutate Result>(f: F) -> Result; diff --git a/frame/support/src/storage/types/value.rs b/frame/support/src/storage/types/value.rs index 14be6bb402e38..3c7f24715ac94 100644 --- a/frame/support/src/storage/types/value.rs +++ b/frame/support/src/storage/types/value.rs @@ -135,6 +135,11 @@ where >::mutate(f) } + /// Mutate the value under a key iff it exists. Do nothing and return the default value if not. + pub fn mutate_extant R>(f: F) -> R { + >::mutate_extant(f) + } + /// Mutate the value if closure returns `Ok` pub fn try_mutate Result>( f: F, diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index 9ea58479a0dfe..d0cedb708cf1d 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -37,9 +37,35 @@ pub trait EnsureOrigin { Self::try_origin(o).map_err(|_| BadOrigin) } + /// The same as `ensure_origin` except that Root origin will always pass. This can only be + /// used if `Success` has a sensible impl of `Default` since that will be used in the result. + fn ensure_origin_or_root(o: OuterOrigin) -> Result, BadOrigin> + where + OuterOrigin: OriginTrait, + { + if o.caller().is_root() { + return Ok(None) + } else { + Self::ensure_origin(o).map(Some) + } + } + /// Perform the origin check. fn try_origin(o: OuterOrigin) -> Result; + /// The same as `try_origin` except that Root origin will always pass. This can only be + /// used if `Success` has a sensible impl of `Default` since that will be used in the result. + fn try_origin_or_root(o: OuterOrigin) -> Result, OuterOrigin> + where + OuterOrigin: OriginTrait, + { + if o.caller().is_root() { + return Ok(None) + } else { + Self::try_origin(o).map(Some) + } + } + /// Attempt to get an outer origin capable of passing `try_origin` check. May return `Err` if it /// is impossible. /// diff --git a/frame/support/src/traits/tokens/fungible/hold.rs b/frame/support/src/traits/tokens/fungible/hold.rs index 3a0ff62e0965a..aa15e9df63a48 100644 --- a/frame/support/src/traits/tokens/fungible/hold.rs +++ b/frame/support/src/traits/tokens/fungible/hold.rs @@ -52,8 +52,10 @@ pub trait Inspect: super::Inspect { /// restrictions on the minimum amount of the account. Note: This cannot bring the account into /// an inconsistent state with regards any required existential deposit. /// - /// Always less than `total_balance_on_hold()`. - fn reducible_total_balance_on_hold(who: &AccountId, force: Fortitude) -> Self::Balance; + /// Never more than `total_balance_on_hold()`. + fn reducible_total_balance_on_hold(who: &AccountId, _force: Fortitude) -> Self::Balance { + Self::total_balance_on_hold(who) + } /// Amount of funds on hold (for the given reason) of `who`. fn balance_on_hold(reason: &Self::Reason, who: &AccountId) -> Self::Balance; @@ -65,7 +67,9 @@ pub trait Inspect: super::Inspect { /// NOTE: This does not take into account changes which could be made to the account of `who` /// (such as removing a provider reference) after this call is made. Any usage of this should /// therefore ensure the account is already in the appropriate state prior to calling it. - fn hold_available(reason: &Self::Reason, who: &AccountId) -> bool; + fn hold_available(_reason: &Self::Reason, _who: &AccountId) -> bool { + true + } /// Check to see if some `amount` of funds of `who` may be placed on hold with the given /// `reason`. Reasons why this may not be true: diff --git a/frame/support/src/traits/tokens/fungibles/hold.rs b/frame/support/src/traits/tokens/fungibles/hold.rs index 55f9d51c65564..c751a836d1f43 100644 --- a/frame/support/src/traits/tokens/fungibles/hold.rs +++ b/frame/support/src/traits/tokens/fungibles/hold.rs @@ -52,12 +52,14 @@ pub trait Inspect: super::Inspect { /// restrictions on the minimum amount of the account. Note: This cannot bring the account into /// an inconsistent state with regards any required existential deposit. /// - /// Always less than `total_balance_on_hold()`. + /// Never more than `total_balance_on_hold()`. fn reducible_total_balance_on_hold( asset: Self::AssetId, who: &AccountId, - force: Fortitude, - ) -> Self::Balance; + _force: Fortitude, + ) -> Self::Balance { + Self::total_balance_on_hold(asset, who) + } /// Amount of funds on hold (for the given reason) of `who`. fn balance_on_hold( @@ -73,7 +75,9 @@ pub trait Inspect: super::Inspect { /// NOTE: This does not take into account changes which could be made to the account of `who` /// (such as removing a provider reference) after this call is made. Any usage of this should /// therefore ensure the account is already in the appropriate state prior to calling it. - fn hold_available(asset: Self::AssetId, reason: &Self::Reason, who: &AccountId) -> bool; + fn hold_available(_asset: Self::AssetId, _reason: &Self::Reason, _who: &AccountId) -> bool { + true + } /// Check to see if some `amount` of funds of `who` may be placed on hold with the given /// `reason`. Reasons why this may not be true: diff --git a/primitives/arithmetic/src/fixed_point.rs b/primitives/arithmetic/src/fixed_point.rs index d3e75f6f781cd..ce14d2957b5e2 100644 --- a/primitives/arithmetic/src/fixed_point.rs +++ b/primitives/arithmetic/src/fixed_point.rs @@ -45,7 +45,7 @@ pub trait FixedPointOperand: + Bounded + Zero + Saturating - + PartialOrd + + PartialOrd + UniqueSaturatedInto + TryFrom + CheckedNeg @@ -58,7 +58,7 @@ impl FixedPointOperand for T where + Bounded + Zero + Saturating - + PartialOrd + + PartialOrd + UniqueSaturatedInto + TryFrom + CheckedNeg