feat: Rebrand Polkadot/Substrate references to PezkuwiChain

This commit systematically rebrands various references from Parity Technologies'
Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk.

Key changes include:
- Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks.
- Modified internal documentation and code comments to reflect PezkuwiChain naming and structure.
- Replaced direct references to  with  or specific paths within the  for XCM, Pezkuwi, and other modules.
- Cleaned up deprecated  issue and PR references in various  and  files, particularly in  and  modules.
- Adjusted image and logo URLs in documentation to point to PezkuwiChain assets.
- Removed or rephrased comments related to external Polkadot/Substrate PRs and issues.

This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -0,0 +1,309 @@
// This file is part of Bizinikiwi.
// 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, SaleInfoRecord};
use pezsp_arithmetic::{traits::One, FixedU64};
use pezsp_core::{Get, RuntimeDebug};
use pezsp_runtime::{FixedPointNumber, FixedPointOperand, Saturating};
/// Performance of a past sale.
#[derive(Copy, Clone)]
pub struct SalePerformance<Balance> {
/// The price at which the last core was sold.
///
/// Will be `None` if no cores have been offered.
pub sellout_price: Option<Balance>,
/// The minimum price that was achieved in this sale.
pub end_price: Balance,
/// The number of cores we want to sell, ideally.
pub ideal_cores_sold: CoreIndex,
/// Number of cores which are/have been offered for sale.
pub cores_offered: CoreIndex,
/// Number of cores which have been sold; never more than cores_offered.
pub cores_sold: CoreIndex,
}
/// Result of `AdaptPrice::adapt_price`.
#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)]
pub struct AdaptedPrices<Balance> {
/// New minimum price to use.
pub end_price: Balance,
/// Price the controller is optimizing for.
///
/// This is the price "expected" by the controller based on the previous sale. We assume that
/// sales in this period will be around this price, assuming stable market conditions.
///
/// Think of it as the expected market price. This can be used for determining what to charge
/// for renewals, that don't yet have any price information for example. E.g. for expired
/// legacy leases.
pub target_price: Balance,
}
impl<Balance: Copy> SalePerformance<Balance> {
/// Construct performance via data from a `SaleInfoRecord`.
pub fn from_sale<BlockNumber>(record: &SaleInfoRecord<Balance, BlockNumber>) -> Self {
Self {
sellout_price: record.sellout_price,
end_price: record.end_price,
ideal_cores_sold: record.ideal_cores_sold,
cores_offered: record.cores_offered,
cores_sold: record.cores_sold,
}
}
#[cfg(test)]
fn new(sellout_price: Option<Balance>, end_price: Balance) -> Self {
Self { sellout_price, end_price, ideal_cores_sold: 0, cores_offered: 0, cores_sold: 0 }
}
}
/// Type for determining how to set price.
pub trait AdaptPrice<Balance> {
/// 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 adapted prices for next sale.
///
/// Based on the previous sale's performance.
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance>;
}
impl<Balance: Copy> AdaptPrice<Balance> for () {
fn leadin_factor_at(_: FixedU64) -> FixedU64 {
FixedU64::one()
}
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
let price = performance.sellout_price.unwrap_or(performance.end_price);
AdaptedPrices { end_price: price, target_price: price }
}
}
/// Simple implementation of `AdaptPrice` with two linear phases.
///
/// One steep one downwards to the target price, which is 1/10 of the maximum price and a more flat
/// one down to the minimum price, which is 1/100 of the maximum price.
pub struct CenterTargetPrice<Balance>(core::marker::PhantomData<Balance>);
impl<Balance: FixedPointOperand> AdaptPrice<Balance> for CenterTargetPrice<Balance> {
fn leadin_factor_at(when: FixedU64) -> FixedU64 {
if when <= FixedU64::from_rational(1, 2) {
FixedU64::from(100).saturating_sub(when.saturating_mul(180.into()))
} else {
FixedU64::from(19).saturating_sub(when.saturating_mul(18.into()))
}
}
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
let Some(sellout_price) = performance.sellout_price else {
return AdaptedPrices {
end_price: performance.end_price,
target_price: FixedU64::from(10).saturating_mul_int(performance.end_price),
};
};
let price = FixedU64::from_rational(1, 10).saturating_mul_int(sellout_price);
let price = if price == Balance::zero() {
// We could not recover from a price equal 0 ever.
sellout_price
} else {
price
};
AdaptedPrices { end_price: price, target_price: sellout_price }
}
}
/// `AdaptPrice` like `CenterTargetPrice`, but with a minimum price.
///
/// This price adapter behaves exactly like `CenterTargetPrice`, except that it takes a minimum
/// price and makes sure that the returned `end_price` is never lower than that.
///
/// Target price will also get adjusted if necessary (it will never be less than the end_price).
pub struct MinimumPrice<Balance, MinPrice>(core::marker::PhantomData<(Balance, MinPrice)>);
impl<Balance: FixedPointOperand, MinPrice: Get<Balance>> AdaptPrice<Balance>
for MinimumPrice<Balance, MinPrice>
{
fn leadin_factor_at(when: FixedU64) -> FixedU64 {
CenterTargetPrice::<Balance>::leadin_factor_at(when)
}
fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
let mut proposal = CenterTargetPrice::<Balance>::adapt_price(performance);
let min_price = MinPrice::get();
if proposal.end_price < min_price {
proposal.end_price = min_price;
}
// Fix target price if necessary:
if proposal.target_price < proposal.end_price {
proposal.target_price = proposal.end_price;
}
proposal
}
}
#[cfg(test)]
mod tests {
use pezsp_core::ConstU64;
use super::*;
#[test]
fn linear_no_panic() {
for sellout in 0..11 {
for price in 0..10 {
let sellout_price = if sellout == 11 { None } else { Some(sellout) };
CenterTargetPrice::adapt_price(SalePerformance::new(sellout_price, price));
}
}
}
#[test]
fn leadin_price_bound_check() {
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from(0)),
FixedU64::from(100)
);
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_rational(1, 4)),
FixedU64::from(55)
);
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_float(0.5)),
FixedU64::from(10)
);
assert_eq!(
CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_rational(3, 4)),
FixedU64::from_float(5.5)
);
assert_eq!(CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::one()), FixedU64::one());
}
#[test]
fn no_op_sale_is_good() {
let prices = CenterTargetPrice::adapt_price(SalePerformance::new(None, 1));
assert_eq!(prices.target_price, 10);
assert_eq!(prices.end_price, 1);
}
#[test]
fn price_stays_stable_on_optimal_sale() {
// Check price stays stable if sold at the optimal price:
let mut performance = SalePerformance::new(Some(1000), 100);
for _ in 0..10 {
let prices = CenterTargetPrice::adapt_price(performance);
performance.sellout_price = Some(1000);
performance.end_price = prices.end_price;
assert!(prices.end_price <= 101);
assert!(prices.end_price >= 99);
assert!(prices.target_price <= 1001);
assert!(prices.target_price >= 999);
}
}
#[test]
fn price_adjusts_correctly_upwards() {
let performance = SalePerformance::new(Some(10_000), 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 10_000);
assert_eq!(prices.end_price, 1000);
}
#[test]
fn price_adjusts_correctly_downwards() {
let performance = SalePerformance::new(Some(100), 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 100);
assert_eq!(prices.end_price, 10);
}
#[test]
fn price_never_goes_to_zero_and_recovers() {
// Check price stays stable if sold at the optimal price:
let sellout_price = 1;
let mut performance = SalePerformance::new(Some(sellout_price), 1);
for _ in 0..11 {
let prices = CenterTargetPrice::adapt_price(performance);
performance.sellout_price = Some(sellout_price);
performance.end_price = prices.end_price;
assert!(prices.end_price <= sellout_price);
assert!(prices.end_price > 0);
}
}
#[test]
fn renewal_price_is_correct_on_no_sale() {
let performance = SalePerformance::new(None, 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 1000);
assert_eq!(prices.end_price, 100);
}
#[test]
fn renewal_price_is_sell_out() {
let performance = SalePerformance::new(Some(1000), 100);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices.target_price, 1000);
}
#[test]
fn minimum_price_works() {
let performance = SalePerformance::new(Some(10), 10);
let prices = MinimumPrice::<u64, ConstU64<10>>::adapt_price(performance);
assert_eq!(prices.end_price, 10);
assert_eq!(prices.target_price, 10);
}
#[test]
fn minimum_price_does_not_affect_valid_target_price() {
let performance = SalePerformance::new(Some(12), 10);
let prices = MinimumPrice::<u64, ConstU64<10>>::adapt_price(performance);
assert_eq!(prices.end_price, 10);
assert_eq!(prices.target_price, 12);
}
#[test]
fn no_minimum_price_works_as_center_target_price() {
let performances = [
(Some(100), 10),
(None, 20),
(Some(1000), 10),
(Some(10), 10),
(Some(1), 1),
(Some(0), 10),
];
for (sellout, end) in performances {
let performance = SalePerformance::new(sellout, end);
let prices_minimum = MinimumPrice::<u64, ConstU64<0>>::adapt_price(performance);
let prices = CenterTargetPrice::adapt_price(performance);
assert_eq!(prices, prices_minimum);
}
}
}
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
// This file is part of Bizinikiwi.
// 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, DecodeWithMemTracking, Encode, MaxEncodedLen};
use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not};
use scale_info::TypeInfo;
use pezsp_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,
DecodeWithMemTracking,
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<u128> 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<CoreMask> for u128 {
fn from(x: CoreMask) -> Self {
x.0.into_iter().fold(0u128, |a, i| (a << 8) | i as u128)
}
}
impl BitAnd<Self> for CoreMask {
type Output = Self;
fn bitand(mut self, rhs: Self) -> Self {
self.bitand_assign(rhs);
self
}
}
impl BitAndAssign<Self> for CoreMask {
fn bitand_assign(&mut self, rhs: Self) {
for i in 0..10 {
self.0[i].bitand_assign(rhs.0[i]);
}
}
}
impl BitOr<Self> for CoreMask {
type Output = Self;
fn bitor(mut self, rhs: Self) -> Self {
self.bitor_assign(rhs);
self
}
}
impl BitOrAssign<Self> for CoreMask {
fn bitor_assign(&mut self, rhs: Self) {
for i in 0..10 {
self.0[i].bitor_assign(rhs.0[i]);
}
}
}
impl BitXor<Self> for CoreMask {
type Output = Self;
fn bitxor(mut self, rhs: Self) -> Self {
self.bitxor_assign(rhs);
self
}
}
impl BitXorAssign<Self> 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));
}
}
@@ -0,0 +1,144 @@
// This file is part of Bizinikiwi.
// 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 alloc::vec::Vec;
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use core::fmt::Debug;
use pezframe_support::Parameter;
use scale_info::TypeInfo;
use pezsp_arithmetic::traits::AtLeast32BitUnsigned;
use pezsp_core::RuntimeDebug;
use pezsp_runtime::traits::BlockNumberProvider;
use crate::Timeslice;
/// Index of a Pezkuwi Core.
pub type CoreIndex = u16;
/// A Task Id. In general this is called a TeyrchainId.
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,
DecodeWithMemTracking,
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),
}
/// Relay chain block number of `T` that implements [`CoretimeInterface`].
pub type RCBlockNumberOf<T> = <RCBlockNumberProviderOf<T> as BlockNumberProvider>::BlockNumber;
/// Relay chain block number provider of `T` that implements [`CoretimeInterface`].
pub type RCBlockNumberProviderOf<T> = <T as CoretimeInterface>::RelayChainBlockNumberProvider;
/// 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 `<https://github.com/polkadot-fellows/RFCs/pull/5>`.
pub trait CoretimeInterface {
/// A (Relay-chain-side) account ID.
type AccountId: Parameter;
/// A (Relay-chain-side) balance.
type Balance: AtLeast32BitUnsigned + Encode + Decode + MaxEncodedLen + TypeInfo + Debug;
/// A provider for the relay chain block number.
type RelayChainBlockNumberProvider: BlockNumberProvider;
/// 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: RCBlockNumberOf<Self>);
/// Instructs the Relay-chain to add the `amount` of HEZ to the Instantaneous Coretime Market
/// Credit account of `who`.
///
/// It is expected that Instantaneous Coretime Market Credit on the Relay-chain is NOT
/// transferable 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: RCBlockNumberOf<Self>,
assignment: Vec<(CoreAssignment, PartsOf57600)>,
end_hint: Option<RCBlockNumberOf<Self>>,
);
/// A hook supposed to be called right after a new timeslice has begun. Likely to be used for
/// batching different matters happened during the timeslice that may benefit from batched
/// processing.
fn on_new_timeslice(_timeslice: Timeslice) {}
}
impl CoretimeInterface for () {
type AccountId = ();
type Balance = u64;
type RelayChainBlockNumberProvider = ();
fn request_core_count(_count: CoreIndex) {}
fn request_revenue_info_at(_when: RCBlockNumberOf<Self>) {}
fn credit_account(_who: Self::AccountId, _amount: Self::Balance) {}
fn assign_core(
_core: CoreIndex,
_begin: RCBlockNumberOf<Self>,
_assignment: Vec<(CoreAssignment, PartsOf57600)>,
_end_hint: Option<RCBlockNumberOf<Self>>,
) {
}
}
@@ -0,0 +1,640 @@
// This file is part of Bizinikiwi.
// 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 core::cmp;
use super::*;
use pezframe_support::{
pezpallet_prelude::*,
traits::{fungible::Mutate, tokens::Preservation::Expendable, DefensiveResult},
};
use pezsp_arithmetic::traits::{CheckedDiv, Saturating, Zero};
use pezsp_runtime::traits::{BlockNumberProvider, Convert};
use CompletionStatus::{Complete, Partial};
impl<T: Config> Pallet<T> {
pub(crate) fn do_configure(config: ConfigRecordOf<T>) -> DispatchResult {
config.validate().map_err(|()| Error::<T>::InvalidConfig)?;
Configuration::<T>::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::<T>::CoreCountRequested { core_count });
Ok(())
}
pub(crate) fn do_notify_core_count(core_count: CoreIndex) -> DispatchResult {
CoreCountInbox::<T>::put(core_count);
Ok(())
}
pub(crate) fn do_reserve(workload: Schedule) -> DispatchResult {
let mut r = Reservations::<T>::get();
let index = r.len() as u32;
r.try_push(workload.clone()).map_err(|_| Error::<T>::TooManyReservations)?;
Reservations::<T>::put(r);
Self::deposit_event(Event::<T>::ReservationMade { index, workload });
Ok(())
}
pub(crate) fn do_unreserve(index: u32) -> DispatchResult {
let mut r = Reservations::<T>::get();
ensure!(index < r.len() as u32, Error::<T>::UnknownReservation);
let workload = r.remove(index as usize);
Reservations::<T>::put(r);
Self::deposit_event(Event::<T>::ReservationCancelled { index, workload });
Ok(())
}
pub(crate) fn do_force_reserve(workload: Schedule, core: CoreIndex) -> DispatchResult {
// Sales must have started, otherwise reserve is equivalent.
let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
// Reserve - starts at second sale period boundary from now.
Self::do_reserve(workload.clone())?;
// Add to workload - grants one region from the next sale boundary.
Workplan::<T>::insert((sale.region_begin, core), &workload);
// Assign now until the next sale boundary unless the next timeslice is already the sale
// boundary.
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let timeslice = status.last_committed_timeslice.saturating_add(1);
if timeslice < sale.region_begin {
Workplan::<T>::insert((timeslice, core), &workload);
}
Ok(())
}
pub(crate) fn do_set_lease(task: TaskId, until: Timeslice) -> DispatchResult {
let mut r = Leases::<T>::get();
ensure!(until > Self::current_timeslice(), Error::<T>::AlreadyExpired);
r.try_push(LeaseRecordItem { until, task })
.map_err(|_| Error::<T>::TooManyLeases)?;
Leases::<T>::put(r);
Self::deposit_event(Event::<T>::Leased { until, task });
Ok(())
}
pub(crate) fn do_remove_lease(task: TaskId) -> DispatchResult {
let mut r = Leases::<T>::get();
let i = r.iter().position(|lease| lease.task == task).ok_or(Error::<T>::LeaseNotFound)?;
r.remove(i);
Leases::<T>::put(r);
Self::deposit_event(Event::<T>::LeaseRemoved { task });
Ok(())
}
pub(crate) fn do_start_sales(
end_price: BalanceOf<T>,
extra_cores: CoreIndex,
) -> DispatchResult {
let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
// Determine the core count
let core_count = Leases::<T>::decode_len().unwrap_or(0) as CoreIndex +
Reservations::<T>::decode_len().unwrap_or(0) as CoreIndex +
extra_cores;
Self::do_request_core_count(core_count)?;
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 = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
// Imaginary old sale for bootstrapping the first actual sale:
let old_sale = SaleInfoRecord {
sale_start: now,
leadin_length: Zero::zero(),
end_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::<T>::SalesStarted { price: end_price, core_count });
Self::rotate_sale(old_sale, &config, &status);
Status::<T>::put(&status);
Ok(())
}
pub(crate) fn do_purchase(
who: T::AccountId,
price_limit: BalanceOf<T>,
) -> Result<RegionId, DispatchError> {
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let mut sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
Self::ensure_cores_for_sale(&status, &sale)?;
let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
ensure!(now > sale.sale_start, Error::<T>::TooEarly);
let price = Self::sale_price(&sale, now);
ensure!(price_limit >= price, Error::<T>::Overpriced);
let core = Self::purchase_core(&who, price, &mut sale)?;
SaleInfo::<T>::put(&sale);
let id = Self::issue(
core,
sale.region_begin,
CoreMask::complete(),
sale.region_end,
Some(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 `PotentialRenewals` 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<CoreIndex, DispatchError> {
let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let mut sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
Self::ensure_cores_for_sale(&status, &sale)?;
let renewal_id = PotentialRenewalId { core, when: sale.region_begin };
let record = PotentialRenewals::<T>::get(renewal_id).ok_or(Error::<T>::NotAllowed)?;
let workload =
record.completion.drain_complete().ok_or(Error::<T>::IncompleteAssignment)?;
let old_core = core;
let core = Self::purchase_core(&who, record.price, &mut sale)?;
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(),
});
Workplan::<T>::insert((sale.region_begin, core), &workload);
let begin = sale.region_end;
let end_price = sale.end_price;
// Renewals should never be priced lower than the current `end_price`:
let price_cap = cmp::max(record.price + config.renewal_bump * record.price, end_price);
let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
let price = Self::sale_price(&sale, now).min(price_cap);
log::debug!(
"Renew with: sale price: {:?}, price cap: {:?}, old price: {:?}",
price,
price_cap,
record.price
);
let new_record = PotentialRenewalRecord { price, completion: Complete(workload) };
PotentialRenewals::<T>::remove(renewal_id);
PotentialRenewals::<T>::insert(PotentialRenewalId { core, when: begin }, &new_record);
SaleInfo::<T>::put(&sale);
if let Some(workload) = new_record.completion.drain_complete() {
log::debug!("Recording renewable price for next run: {:?}", price);
Self::deposit_event(Event::Renewable { core, price, begin, workload });
}
Ok(core)
}
pub(crate) fn do_transfer(
region_id: RegionId,
maybe_check_owner: Option<T::AccountId>,
new_owner: T::AccountId,
) -> Result<(), Error<T>> {
let mut region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
if let Some(check_owner) = maybe_check_owner {
ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
}
let old_owner = region.owner;
region.owner = Some(new_owner);
Regions::<T>::insert(&region_id, &region);
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<T::AccountId>,
pivot_offset: Timeslice,
) -> Result<(RegionId, RegionId), Error<T>> {
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let mut region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
if let Some(check_owner) = maybe_check_owner {
ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
}
let pivot = region_id.begin.saturating_add(pivot_offset);
ensure!(pivot < region.end, Error::<T>::PivotTooLate);
ensure!(pivot > region_id.begin, Error::<T>::PivotTooEarly);
region.paid = None;
let new_region_ids = (region_id, RegionId { begin: pivot, ..region_id });
// Remove this region from the pool in case it has been assigned provisionally. If we get
// this far then it is still in `Regions` and thus could only have been pooled
// provisionally.
Self::force_unpool_region(region_id, &region, &status);
// Overwrite the previous region with its new end and create a new region for the second
// part of the partition.
Regions::<T>::insert(&new_region_ids.0, &RegionRecord { end: pivot, ..region.clone() });
Regions::<T>::insert(&new_region_ids.1, &region);
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<T::AccountId>,
pivot: CoreMask,
) -> Result<(RegionId, RegionId), Error<T>> {
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
if let Some(check_owner) = maybe_check_owner {
ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
}
ensure!((pivot & !region_id.mask).is_void(), Error::<T>::ExteriorPivot);
ensure!(!pivot.is_void(), Error::<T>::VoidPivot);
ensure!(pivot != region_id.mask, Error::<T>::CompletePivot);
// Remove this region from the pool in case it has been assigned provisionally. If we get
// this far then it is still in `Regions` and thus could only have been pooled
// provisionally.
Self::force_unpool_region(region_id, &region, &status);
// The old region should be removed.
Regions::<T>::remove(&region_id);
let one = RegionId { mask: pivot, ..region_id };
Regions::<T>::insert(&one, &region);
let other = RegionId { mask: region_id.mask ^ pivot, ..region_id };
Regions::<T>::insert(&other, &region);
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<T::AccountId>,
target: TaskId,
finality: Finality,
) -> Result<(), Error<T>> {
let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let status = Status::<T>::get().ok_or(Error::<T>::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::<T>::get(&workplan_key).unwrap_or_default();
// Remove this region from the pool in case it has been assigned provisionally. If we
// get this far then it is still in `Regions` and thus could only have been pooled
// provisionally.
Self::force_unpool_region(region_id, &region, &status);
// 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::<T>::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 = PotentialRenewalId { core: region_id.core, when: region.end };
let assigned = match PotentialRenewals::<T>::get(renewal_id) {
Some(PotentialRenewalRecord { 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 = PotentialRenewalRecord { price, completion: workload };
// Note: This entry alone does not yet actually allow renewals (the completion
// status has to be complete for `do_renew` to accept it).
PotentialRenewals::<T>::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_remove_assignment(region_id: RegionId) -> DispatchResult {
let workplan_key = (region_id.begin, region_id.core);
ensure!(Workplan::<T>::contains_key(&workplan_key), Error::<T>::AssignmentNotFound);
Workplan::<T>::remove(&workplan_key);
Self::deposit_event(Event::<T>::AssignmentRemoved { region_id });
Ok(())
}
pub(crate) fn do_pool(
region_id: RegionId,
maybe_check_owner: Option<T::AccountId>,
payee: T::AccountId,
finality: Finality,
) -> Result<(), Error<T>> {
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::<T>::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::<T>::insert(&workplan_key, &workplan);
let size = region_id.mask.count_ones() as i32;
InstaPoolIo::<T>::mutate(region_id.begin, |a| a.private.saturating_accrue(size));
InstaPoolIo::<T>::mutate(region.end, |a| a.private.saturating_reduce(size));
let record = ContributionRecord { length: duration, payee };
InstaPoolContribution::<T>::insert(&region_id, record);
}
Self::deposit_event(Event::Pooled { region_id, duration });
}
Ok(())
}
pub(crate) fn do_claim_revenue(
mut region: RegionId,
max_timeslices: Timeslice,
) -> DispatchResult {
ensure!(max_timeslices > 0, Error::<T>::NoClaimTimeslices);
let mut contribution =
InstaPoolContribution::<T>::take(region).ok_or(Error::<T>::UnknownContribution)?;
let contributed_parts = region.mask.count_ones();
Self::deposit_event(Event::RevenueClaimBegun { region, max_timeslices });
let mut payout = BalanceOf::<T>::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::<T>::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::<T>::insert(r, &pool_record);
} else {
InstaPoolHistory::<T>::remove(r);
}
if !p.is_zero() {
Self::deposit_event(Event::RevenueClaimItem { when: r, amount: p });
}
}
if contribution.length > 0 {
InstaPoolContribution::<T>::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<T>,
beneficiary: RelayAccountIdOf<T>,
) -> DispatchResult {
ensure!(amount >= T::MinimumCreditPurchase::get(), Error::<T>::CreditPurchaseTooSmall);
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::<T>::CreditPurchased { who, beneficiary, amount });
Ok(())
}
pub(crate) fn do_drop_region(region_id: RegionId) -> DispatchResult {
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
ensure!(status.last_committed_timeslice >= region.end, Error::<T>::StillValid);
Regions::<T>::remove(&region_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::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let contrib =
InstaPoolContribution::<T>::get(&region_id).ok_or(Error::<T>::UnknownContribution)?;
let end = region_id.begin.saturating_add(contrib.length);
ensure!(
status.last_timeslice >= end.saturating_add(config.contribution_timeout),
Error::<T>::StillValid
);
InstaPoolContribution::<T>::remove(region_id);
Self::deposit_event(Event::ContributionDropped { region_id });
Ok(())
}
pub(crate) fn do_drop_history(when: Timeslice) -> DispatchResult {
let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
ensure!(
status.last_timeslice > when.saturating_add(config.contribution_timeout),
Error::<T>::StillValid
);
let record = InstaPoolHistory::<T>::take(when).ok_or(Error::<T>::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::<T>::get().ok_or(Error::<T>::Uninitialized)?;
ensure!(status.last_committed_timeslice >= when, Error::<T>::StillValid);
let id = PotentialRenewalId { core, when };
ensure!(PotentialRenewals::<T>::contains_key(id), Error::<T>::UnknownRenewal);
PotentialRenewals::<T>::remove(id);
Self::deposit_event(Event::PotentialRenewalDropped { core, when });
Ok(())
}
pub(crate) fn do_notify_revenue(revenue: OnDemandRevenueRecordOf<T>) -> DispatchResult {
RevenueInbox::<T>::put(revenue);
Ok(())
}
pub(crate) fn do_swap_leases(id: TaskId, other: TaskId) -> DispatchResult {
let mut id_leases_count = 0;
let mut other_leases_count = 0;
Leases::<T>::mutate(|leases| {
leases.iter_mut().for_each(|lease| {
if lease.task == id {
lease.task = other;
id_leases_count += 1;
} else if lease.task == other {
lease.task = id;
other_leases_count += 1;
}
})
});
Ok(())
}
pub(crate) fn do_enable_auto_renew(
sovereign_account: T::AccountId,
core: CoreIndex,
task: TaskId,
workload_end_hint: Option<Timeslice>,
) -> DispatchResult {
let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
// Check if the core is expiring in the next bulk period; if so, we will renew it now.
//
// In case we renew it now, we don't need to check the workload end since we know it is
// eligible for renewal.
if PotentialRenewals::<T>::get(PotentialRenewalId { core, when: sale.region_begin })
.is_some()
{
Self::do_renew(sovereign_account.clone(), core)?;
} else if let Some(workload_end) = workload_end_hint {
ensure!(
PotentialRenewals::<T>::get(PotentialRenewalId { core, when: workload_end })
.is_some(),
Error::<T>::NotAllowed
);
} else {
return Err(Error::<T>::NotAllowed.into());
}
// We are sorting auto renewals by `CoreIndex`.
AutoRenewals::<T>::try_mutate(|renewals| {
let pos = renewals
.binary_search_by(|r: &AutoRenewalRecord| r.core.cmp(&core))
.unwrap_or_else(|e| e);
renewals.try_insert(
pos,
AutoRenewalRecord {
core,
task,
next_renewal: workload_end_hint.unwrap_or(sale.region_end),
},
)
})
.map_err(|_| Error::<T>::TooManyAutoRenewals)?;
Self::deposit_event(Event::AutoRenewalEnabled { core, task });
Ok(())
}
pub(crate) fn do_disable_auto_renew(core: CoreIndex, task: TaskId) -> DispatchResult {
AutoRenewals::<T>::try_mutate(|renewals| -> DispatchResult {
let pos = renewals
.binary_search_by(|r: &AutoRenewalRecord| r.core.cmp(&core))
.map_err(|_| Error::<T>::AutoRenewalNotEnabled)?;
let renewal_record = renewals.get(pos).ok_or(Error::<T>::AutoRenewalNotEnabled)?;
ensure!(
renewal_record.core == core && renewal_record.task == task,
Error::<T>::NoPermission
);
renewals.remove(pos);
Ok(())
})?;
Self::deposit_event(Event::AutoRenewalDisabled { core, task });
Ok(())
}
pub(crate) fn ensure_cores_for_sale(
status: &StatusRecord,
sale: &SaleInfoRecordOf<T>,
) -> Result<(), DispatchError> {
ensure!(sale.first_core < status.core_count, Error::<T>::Unavailable);
ensure!(sale.cores_sold < sale.cores_offered, Error::<T>::SoldOut);
Ok(())
}
/// If there is an ongoing sale returns the current price of a core.
pub fn current_price() -> Result<BalanceOf<T>, DispatchError> {
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
Self::ensure_cores_for_sale(&status, &sale)?;
let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
Ok(Self::sale_price(&sale, now))
}
}
File diff suppressed because it is too large Load Diff
+436
View File
@@ -0,0 +1,436 @@
// This file is part of Bizinikiwi.
// 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 crate::types::RegionRecord;
use codec::{Decode, Encode};
use core::marker::PhantomData;
use pezframe_support::traits::{Get, UncheckedOnRuntimeUpgrade};
use pezsp_runtime::Saturating;
#[cfg(feature = "try-runtime")]
use alloc::vec::Vec;
#[cfg(feature = "try-runtime")]
use pezframe_support::ensure;
mod v1 {
use super::*;
/// V0 region record.
#[derive(Encode, Decode)]
struct RegionRecordV0<AccountId, Balance> {
/// The end of the Region.
pub end: Timeslice,
/// The owner of the Region.
pub owner: AccountId,
/// The amount paid to Pezkuwi for this Region, or `None` if renewal is not allowed.
pub paid: Option<Balance>,
}
pub struct MigrateToV1Impl<T>(PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for MigrateToV1Impl<T> {
fn on_runtime_upgrade() -> pezframe_support::weights::Weight {
let mut count: u64 = 0;
<Regions<T>>::translate::<RegionRecordV0<T::AccountId, BalanceOf<T>>, _>(|_, v0| {
count.saturating_inc();
Some(RegionRecord { end: v0.end, owner: Some(v0.owner), paid: v0.paid })
});
log::info!(
target: LOG_TARGET,
"Storage migration v1 for pezpallet-broker finished.",
);
// calculate and return migration weights
T::DbWeight::get().reads_writes(count as u64 + 1, count as u64 + 1)
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::TryRuntimeError> {
Ok((Regions::<T>::iter_keys().count() as u32).encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
let old_count = u32::decode(&mut &state[..]).expect("Known good");
let new_count = Regions::<T>::iter_values().count() as u32;
ensure!(old_count == new_count, "Regions count should not change");
Ok(())
}
}
}
mod v2 {
use super::*;
use pezframe_support::{
pezpallet_prelude::{OptionQuery, Twox64Concat},
storage_alias,
};
#[storage_alias]
pub type AllowedRenewals<T: Config> = StorageMap<
Pallet<T>,
Twox64Concat,
PotentialRenewalId,
PotentialRenewalRecordOf<T>,
OptionQuery,
>;
pub struct MigrateToV2Impl<T>(PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for MigrateToV2Impl<T> {
fn on_runtime_upgrade() -> pezframe_support::weights::Weight {
let mut count = 0;
for (renewal_id, renewal) in AllowedRenewals::<T>::drain() {
PotentialRenewals::<T>::insert(renewal_id, renewal);
count += 1;
}
log::info!(
target: LOG_TARGET,
"Storage migration v2 for pezpallet-broker finished.",
);
// calculate and return migration weights
T::DbWeight::get().reads_writes(count as u64 + 1, count as u64 + 1)
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::TryRuntimeError> {
Ok((AllowedRenewals::<T>::iter_keys().count() as u32).encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
let old_count = u32::decode(&mut &state[..]).expect("Known good");
let new_count = PotentialRenewals::<T>::iter_values().count() as u32;
ensure!(old_count == new_count, "Renewal count should not change");
Ok(())
}
}
}
mod v3 {
use super::*;
use codec::MaxEncodedLen;
use pezframe_support::{
pezpallet_prelude::{OptionQuery, RuntimeDebug, TypeInfo},
storage_alias,
};
use pezframe_system::Pallet as System;
use pezsp_arithmetic::Perbill;
pub struct MigrateToV3Impl<T>(PhantomData<T>);
impl<T: Config> UncheckedOnRuntimeUpgrade for MigrateToV3Impl<T> {
fn on_runtime_upgrade() -> pezframe_support::weights::Weight {
let acc = Pallet::<T>::account_id();
System::<T>::inc_providers(&acc);
// calculate and return migration weights
T::DbWeight::get().writes(1)
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::TryRuntimeError> {
Ok(System::<T>::providers(&Pallet::<T>::account_id()).encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
let old_providers = u32::decode(&mut &state[..]).expect("Known good");
let new_providers = System::<T>::providers(&Pallet::<T>::account_id()) as u32;
ensure!(new_providers == old_providers + 1, "Providers count should increase by one");
Ok(())
}
}
#[storage_alias]
pub type Configuration<T: Config> = StorageValue<Pallet<T>, ConfigRecordOf<T>, OptionQuery>;
pub type ConfigRecordOf<T> =
ConfigRecord<pezframe_system::pezpallet_prelude::BlockNumberFor<T>, RelayBlockNumberOf<T>>;
// types added here for v4 migration
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ConfigRecord<BlockNumber, RelayBlockNumber> {
/// 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<CoreIndex>,
/// 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,
}
#[storage_alias]
pub type SaleInfo<T: Config> = StorageValue<Pallet<T>, SaleInfoRecordOf<T>, OptionQuery>;
pub type SaleInfoRecordOf<T> =
SaleInfoRecord<BalanceOf<T>, pezframe_system::pezpallet_prelude::BlockNumberFor<T>>;
/// The status of a Bulk Coretime Sale.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct SaleInfoRecord<Balance, BlockNumber> {
/// The relay block number at which the sale will/did start.
pub sale_start: BlockNumber,
/// The length in relay chain 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<Balance>,
/// Number of cores which have been sold; never more than cores_offered.
pub cores_sold: CoreIndex,
}
}
pub mod v4 {
use super::*;
type BlockNumberFor<T> = pezframe_system::pezpallet_prelude::BlockNumberFor<T>;
pub trait BlockToRelayHeightConversion<T: Config> {
/// Converts absolute value of teyrchain block number to relay chain block number
fn convert_block_number_to_relay_height(
block_number: BlockNumberFor<T>,
) -> RelayBlockNumberOf<T>;
/// Converts teyrchain block length into equivalent relay chain block length
fn convert_block_length_to_relay_length(
block_number: BlockNumberFor<T>,
) -> RelayBlockNumberOf<T>;
}
pub struct MigrateToV4Impl<T, BlockConversion>(PhantomData<T>, PhantomData<BlockConversion>);
impl<T: Config, BlockConversion: BlockToRelayHeightConversion<T>> UncheckedOnRuntimeUpgrade
for MigrateToV4Impl<T, BlockConversion>
{
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::TryRuntimeError> {
let (interlude_length, configuration_leadin_length) =
if let Some(config_record) = v3::Configuration::<T>::get() {
(config_record.interlude_length, config_record.leadin_length)
} else {
((0 as u32).into(), (0 as u32).into())
};
let updated_interlude_length: RelayBlockNumberOf<T> =
BlockConversion::convert_block_length_to_relay_length(interlude_length);
let updated_leadin_length: RelayBlockNumberOf<T> =
BlockConversion::convert_block_length_to_relay_length(configuration_leadin_length);
log::info!(target: LOG_TARGET, "Configuration Pre-Migration: Interlude Length {:?}->{:?} Leadin Length {:?}->{:?}", interlude_length, updated_interlude_length, configuration_leadin_length, updated_leadin_length);
let (sale_start, sale_info_leadin_length) =
if let Some(sale_info_record) = v3::SaleInfo::<T>::get() {
(sale_info_record.sale_start, sale_info_record.leadin_length)
} else {
((0 as u32).into(), (0 as u32).into())
};
let updated_sale_start: RelayBlockNumberOf<T> =
BlockConversion::convert_block_number_to_relay_height(sale_start);
let updated_sale_info_leadin_length: RelayBlockNumberOf<T> =
BlockConversion::convert_block_length_to_relay_length(sale_info_leadin_length);
log::info!(target: LOG_TARGET, "SaleInfo Pre-Migration: Sale Start {:?}->{:?} Interlude Length {:?}->{:?}", sale_start, updated_sale_start, sale_info_leadin_length, updated_sale_info_leadin_length);
Ok((interlude_length, configuration_leadin_length, sale_start, sale_info_leadin_length)
.encode())
}
fn on_runtime_upgrade() -> pezframe_support::weights::Weight {
let mut weight = T::DbWeight::get().reads(1);
if let Some(config_record) = v3::Configuration::<T>::take() {
log::info!(target: LOG_TARGET, "migrating Configuration record");
let updated_interlude_length: RelayBlockNumberOf<T> =
BlockConversion::convert_block_length_to_relay_length(
config_record.interlude_length,
);
let updated_leadin_length: RelayBlockNumberOf<T> =
BlockConversion::convert_block_length_to_relay_length(
config_record.leadin_length,
);
let updated_config_record = ConfigRecord {
interlude_length: updated_interlude_length,
leadin_length: updated_leadin_length,
advance_notice: config_record.advance_notice,
region_length: config_record.region_length,
ideal_bulk_proportion: config_record.ideal_bulk_proportion,
limit_cores_offered: config_record.limit_cores_offered,
renewal_bump: config_record.renewal_bump,
contribution_timeout: config_record.contribution_timeout,
};
Configuration::<T>::put(updated_config_record);
}
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
if let Some(sale_info) = v3::SaleInfo::<T>::take() {
log::info!(target: LOG_TARGET, "migrating SaleInfo record");
let updated_sale_start: RelayBlockNumberOf<T> =
BlockConversion::convert_block_number_to_relay_height(sale_info.sale_start);
let updated_leadin_length: RelayBlockNumberOf<T> =
BlockConversion::convert_block_length_to_relay_length(sale_info.leadin_length);
let updated_sale_info = SaleInfoRecord {
sale_start: updated_sale_start,
leadin_length: updated_leadin_length,
end_price: sale_info.price,
region_begin: sale_info.region_begin,
region_end: sale_info.region_end,
ideal_cores_sold: sale_info.ideal_cores_sold,
cores_offered: sale_info.cores_offered,
first_core: sale_info.first_core,
sellout_price: sale_info.sellout_price,
cores_sold: sale_info.cores_sold,
};
SaleInfo::<T>::put(updated_sale_info);
}
weight.saturating_add(T::DbWeight::get().reads_writes(1, 2))
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
let (
old_interlude_length,
old_configuration_leadin_length,
old_sale_start,
old_sale_info_leadin_length,
): (BlockNumberFor<T>, BlockNumberFor<T>, BlockNumberFor<T>, BlockNumberFor<T>) =
Decode::decode(&mut &state[..]).expect("pre_upgrade provides a valid state; qed");
if let Some(config_record) = Configuration::<T>::get() {
ensure!(
Self::verify_updated_block_length(
old_configuration_leadin_length,
config_record.leadin_length
),
"must migrate configuration leadin_length"
);
ensure!(
Self::verify_updated_block_length(
old_interlude_length,
config_record.interlude_length
),
"must migrate configuration interlude_length"
);
}
if let Some(sale_info) = SaleInfo::<T>::get() {
ensure!(
Self::verify_updated_block_time(old_sale_start, sale_info.sale_start),
"must migrate sale info sale_start"
);
ensure!(
Self::verify_updated_block_length(
old_sale_info_leadin_length,
sale_info.leadin_length
),
"must migrate sale info leadin_length"
);
}
Ok(())
}
}
#[cfg(feature = "try-runtime")]
impl<T: Config, BlockConversion: BlockToRelayHeightConversion<T>>
MigrateToV4Impl<T, BlockConversion>
{
fn verify_updated_block_time(
old_value: BlockNumberFor<T>,
new_value: RelayBlockNumberOf<T>,
) -> bool {
BlockConversion::convert_block_number_to_relay_height(old_value) == new_value
}
fn verify_updated_block_length(
old_value: BlockNumberFor<T>,
new_value: RelayBlockNumberOf<T>,
) -> bool {
BlockConversion::convert_block_length_to_relay_length(old_value) == new_value
}
}
}
/// Migrate the pallet storage from `0` to `1`.
pub type MigrateV0ToV1<T> = pezframe_support::migrations::VersionedMigration<
0,
1,
v1::MigrateToV1Impl<T>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
pub type MigrateV1ToV2<T> = pezframe_support::migrations::VersionedMigration<
1,
2,
v2::MigrateToV2Impl<T>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
pub type MigrateV2ToV3<T> = pezframe_support::migrations::VersionedMigration<
2,
3,
v3::MigrateToV3Impl<T>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
pub type MigrateV3ToV4<T, BlockConversion> = pezframe_support::migrations::VersionedMigration<
3,
4,
v4::MigrateToV4Impl<T, BlockConversion>,
Pallet<T>,
<T as pezframe_system::Config>::DbWeight,
>;
+355
View File
@@ -0,0 +1,355 @@
// This file is part of Bizinikiwi.
// 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 alloc::collections::btree_map::BTreeMap;
use pezframe_support::{
assert_ok, derive_impl, ensure, ord_parameter_types, parameter_types,
traits::{
fungible::{Balanced, Credit, Inspect, ItemOf, Mutate},
nonfungible::Inspect as NftInspect,
tokens::{Fortitude, Precision, Preservation},
EitherOfDiverse, Hooks, OnUnbalanced,
},
PalletId,
};
use pezframe_system::{EnsureRoot, EnsureSignedBy};
use pezsp_arithmetic::Perbill;
use pezsp_core::{ConstU32, ConstU64, Get};
use pezsp_runtime::{
traits::{BlockNumberProvider, Identity, MaybeConvert},
BuildStorage, Saturating,
};
type Block = pezframe_system::mocking::MockBlock<Test>;
// Configure a mock runtime to test the pallet.
pezframe_support::construct_runtime!(
pub enum Test
{
System: pezframe_system,
Broker: crate,
}
);
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for Test {
type Block = Block;
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CoretimeTraceItem {
AssignCore {
core: CoreIndex,
begin: u32,
assignment: Vec<(CoreAssignment, PartsOf57600)>,
end_hint: Option<u32>,
},
}
use CoretimeTraceItem::*;
parameter_types! {
pub static CoretimeTrace: Vec<(u32, CoretimeTraceItem)> = Default::default();
pub static CoretimeCredit: BTreeMap<u64, u64> = 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<CoreIndex, Vec<(CoreAssignment, PartsOf57600)>> = Default::default();
pub static CoretimeInPool: CoreMaskBitCount = 0;
}
pub struct TestCoretimeProvider;
impl CoretimeInterface for TestCoretimeProvider {
type AccountId = u64;
type Balance = u64;
type RelayChainBlockNumberProvider = System;
fn request_core_count(count: CoreIndex) {
CoreCountInbox::<Test>::put(count);
}
fn request_revenue_info_at(when: RCBlockNumberOf<Self>) {
if when > RCBlockNumberProviderOf::<Self>::current_block_number() {
panic!(
"Asking for revenue info in the future {:?} {:?}",
when,
RCBlockNumberProviderOf::<Self>::current_block_number()
);
}
let mut total = 0;
CoretimeSpending::mutate(|s| {
s.retain(|(n, a)| {
if *n < when as u32 {
total += a;
false
} else {
true
}
})
});
// When the credit is spent, we mint this amount back into the pot (on a real network this
// will be a teleport).
mint_to_pot(total);
RevenueInbox::<Test>::put(OnDemandRevenueRecord { until: when, amount: total });
}
fn credit_account(who: Self::AccountId, amount: Self::Balance) {
// When the account is credited, we burn the associated funds in their account (on a real
// network this will be a teleport).
CoretimeCredit::mutate(|c| c.entry(who).or_default().saturating_accrue(amount));
burn_from_pot(amount);
}
fn assign_core(
core: CoreIndex,
begin: RCBlockNumberOf<Self>,
assignment: Vec<(CoreAssignment, PartsOf57600)>,
end_hint: Option<RCBlockNumberOf<Self>>,
) {
CoretimeWorkplan::mutate(|p| p.insert((begin as u32, core), assignment.clone()));
let item = (
RCBlockNumberProviderOf::<Self>::current_block_number() as u32,
AssignCore {
core,
begin: begin as u32,
assignment,
end_hint: end_hint.map(|v| v as u32),
},
);
CoretimeTrace::mutate(|v| v.push(item));
}
}
impl TestCoretimeProvider {
pub fn spend_instantaneous(who: u64, price: u64) -> Result<(), &'static str> {
let mut c = CoretimeCredit::get();
ensure!(CoretimeInPool::get() > 0, "None in pool");
c.insert(
who,
c.get(&who)
.ok_or("Account not there")?
.checked_sub(price)
.ok_or("Checked sub failed")?,
);
CoretimeCredit::set(c);
CoretimeSpending::mutate(|v| {
v.push((RCBlockNumberProviderOf::<Self>::current_block_number() as u32, price))
});
Ok(())
}
pub fn bump() {
let mut pool_size = CoretimeInPool::get();
let mut workplan = CoretimeWorkplan::get();
let mut usage = CoretimeUsage::get();
let now = RCBlockNumberProviderOf::<Self>::current_block_number() as u32;
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<Credit<u64, <Test as Config>::Currency>> for IntoZero {
fn on_nonzero_unbalanced(credit: Credit<u64, <Test as Config>::Currency>) {
let _ = <<Test as Config>::Currency as Balanced<_>>::resolve(&0, credit);
}
}
ord_parameter_types! {
pub const One: u64 = 1;
pub const MinimumCreditPurchase: u64 = 20;
}
type EnsureOneOrRoot = EitherOfDiverse<EnsureRoot<u64>, EnsureSignedBy<One, u64>>;
// Dummy implementation which converts `TaskId` to `AccountId`.
pub struct SovereignAccountOf;
impl MaybeConvert<TaskId, u64> for SovereignAccountOf {
fn maybe_convert(task: TaskId) -> Option<u64> {
Some(task.into())
}
}
impl crate::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = ItemOf<TestFungibles<(), u64, (), ConstU64<0>, ()>, (), u64>;
type OnRevenue = IntoZero;
type TimeslicePeriod = ConstU64<2>;
type MaxLeasedCores = ConstU32<5>;
type MaxReservedCores = ConstU32<5>;
type Coretime = TestCoretimeProvider;
type ConvertBalance = Identity;
type WeightInfo = ();
type PalletId = TestBrokerId;
type AdminOrigin = EnsureOneOrRoot;
type SovereignAccountOf = SovereignAccountOf;
type MaxAutoRenewals = ConstU32<3>;
type PriceAdapter = CenterTargetPrice<BalanceOf<Self>>;
type MinimumCreditPurchase = MinimumCreditPurchase;
}
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 advance_sale_period() {
let sale = SaleInfo::<Test>::get().unwrap();
let target_block_number =
sale.region_begin as u64 * <<Test as crate::Config>::TimeslicePeriod as Get<u64>>::get();
advance_to(target_block_number)
}
pub fn pot() -> u64 {
balance(Broker::account_id())
}
pub fn mint_to_pot(amount: u64) {
let imb = <Test as crate::Config>::Currency::issue(amount);
let _ = <Test as crate::Config>::Currency::resolve(&Broker::account_id(), imb);
}
pub fn burn_from_pot(amount: u64) {
let _ = <Test as crate::Config>::Currency::burn_from(
&Broker::account_id(),
amount,
Preservation::Expendable,
Precision::Exact,
Fortitude::Polite,
)
.expect("Broker pot should have sufficient balance to burn.");
}
pub fn revenue() -> u64 {
balance(0)
}
pub fn balance(who: u64) -> u64 {
<<Test as Config>::Currency as Inspect<_>>::total_balance(&who)
}
pub fn attribute<T: codec::Decode>(nft: RegionId, attribute: impl codec::Encode) -> T {
<Broker as NftInspect<_>>::typed_attribute::<_, T>(&nft.into(), &attribute).unwrap()
}
pub fn new_config() -> ConfigRecordOf<Test> {
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 fn endow(who: u64, amount: u64) {
assert_ok!(<<Test as Config>::Currency as Mutate<_>>::mint_into(&who, amount));
}
pub struct TestExt(ConfigRecordOf<Test>);
#[allow(dead_code)]
impl TestExt {
pub fn new() -> Self {
Self(new_config())
}
pub fn new_with_config(config: ConfigRecordOf<Test>) -> Self {
Self(config)
}
pub fn advance_notice(mut self, advance_notice: Timeslice) -> Self {
self.0.advance_notice = advance_notice as u64;
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<CoreIndex>) -> 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 {
endow(who, amount);
self
}
pub fn execute_with<R>(self, f: impl Fn() -> R) -> R {
new_test_ext().execute_with(|| {
assert_ok!(Broker::do_configure(self.0));
f()
})
}
}
pub fn new_test_ext() -> pezsp_io::TestExternalities {
let c = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
pezsp_io::TestExternalities::from(c)
}
@@ -0,0 +1,102 @@
// This file is part of Bizinikiwi.
// 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 alloc::vec::Vec;
use pezframe_support::{
pezpallet_prelude::{DispatchResult, *},
traits::nonfungible::{Inspect, Mutate, Transfer},
};
impl<T: Config> Inspect<T::AccountId> for Pallet<T> {
type ItemId = u128;
fn owner(item: &Self::ItemId) -> Option<T::AccountId> {
let record = Regions::<T>::get(RegionId::from(*item))?;
record.owner
}
fn attribute(item: &Self::ItemId, key: &[u8]) -> Option<Vec<u8>> {
let id = RegionId::from(*item);
let item = Regions::<T>::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<T: Config> Transfer<T::AccountId> for Pallet<T> {
fn transfer(item: &Self::ItemId, dest: &T::AccountId) -> DispatchResult {
Self::do_transfer((*item).into(), None, dest.clone()).map_err(Into::into)
}
}
/// We don't really support burning and minting.
///
/// We only need this to allow the region to be reserve transferable.
///
/// For reserve transfers that are not 'local', the asset must first be withdrawn to the holding
/// register and then deposited into the designated account. This process necessitates that the
/// asset is capable of being 'burned' and 'minted'.
///
/// Since each region is associated with specific record data, we will not actually burn the asset.
/// If we did, we wouldn't know what record to assign to the newly minted region. Therefore, instead
/// of burning, we set the asset's owner to `None`. In essence, 'burning' a region involves setting
/// its owner to `None`, whereas 'minting' the region assigns its owner to an actual account. This
/// way we never lose track of the associated record data.
impl<T: Config> Mutate<T::AccountId> for Pallet<T> {
/// Deposit a region into an account.
fn mint_into(item: &Self::ItemId, who: &T::AccountId) -> DispatchResult {
let region_id: RegionId = (*item).into();
let record = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
// 'Minting' can only occur if the asset has previously been burned (i.e. moved to the
// holding register)
ensure!(record.owner.is_none(), Error::<T>::NotAllowed);
Self::issue(
region_id.core,
region_id.begin,
region_id.mask,
record.end,
Some(who.clone()),
record.paid,
);
Ok(())
}
/// Withdraw a region from account.
fn burn(item: &Self::ItemId, maybe_check_owner: Option<&T::AccountId>) -> DispatchResult {
let region_id: RegionId = (*item).into();
let mut record = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
if let Some(owner) = maybe_check_owner {
ensure!(Some(owner.clone()) == record.owner, Error::<T>::NotOwner);
}
record.owner = None;
Regions::<T>::insert(region_id, record);
Ok(())
}
}
@@ -0,0 +1,31 @@
// This file is part of Bizinikiwi.
// 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.
//! Runtime API definition for the FRAME Broker pallet.
use codec::Codec;
use pezsp_runtime::DispatchError;
pezsp_api::decl_runtime_apis! {
pub trait BrokerApi<Balance>
where
Balance: Codec
{
/// If there is an ongoing sale returns the current price of a core.
fn sale_price() -> Result<Balance, DispatchError>;
}
}
@@ -0,0 +1,297 @@
// This file is part of Bizinikiwi.
// 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 alloc::collections::btree_map::BTreeMap;
use codec::{Decode, Encode};
use pezframe_support::{
parameter_types,
traits::{
fungibles::{self, Dust},
tokens::{
self, DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence,
},
},
};
use scale_info::TypeInfo;
use pezsp_arithmetic::traits::Zero;
use pezsp_core::{Get, TypedGet};
use pezsp_runtime::{DispatchError, DispatchResult};
parameter_types! {
static TestAssetOf: BTreeMap<(u32, Vec<u8>), Vec<u8>> = Default::default();
static TestBalanceOf: BTreeMap<(u32, Vec<u8>, Vec<u8>), Vec<u8>> = Default::default();
static TestHoldOf: BTreeMap<(u32, Vec<u8>, Vec<u8>, Vec<u8>), Vec<u8>> = Default::default();
}
pub struct TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>(
core::marker::PhantomData<(Instance, AccountId, AssetId, MinimumBalance, HoldReason)>,
);
impl<
Instance: Get<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason,
> fungibles::Inspect<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
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<Self::Balance> {
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<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason,
> fungibles::Unbalanced<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
where
MinimumBalance::Type: tokens::Balance,
{
fn handle_dust(_dust: Dust<AccountId, Self>) {}
fn write_balance(
asset: Self::AssetId,
who: &AccountId,
amount: Self::Balance,
) -> Result<Option<Self::Balance>, 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<u32>,
AccountId: Encode + Eq,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason,
> fungibles::Mutate<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
where
MinimumBalance::Type: tokens::Balance,
{
}
impl<
Instance: Get<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason,
> fungibles::Balanced<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
where
MinimumBalance::Type: tokens::Balance,
{
type OnDropCredit = fungibles::DecreaseIssuance<AccountId, Self>;
type OnDropDebt = fungibles::IncreaseIssuance<AccountId, Self>;
}
impl<
Instance: Get<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason: Encode + Decode + TypeInfo + 'static,
> fungibles::InspectHold<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
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<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason: Encode + Decode + TypeInfo + 'static,
> fungibles::UnbalancedHold<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
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<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason: Encode + Decode + TypeInfo + 'static,
> fungibles::MutateHold<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
where
MinimumBalance::Type: tokens::Balance,
{
}
impl<
Instance: Get<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason: Encode + Decode + TypeInfo + 'static,
Balance: tokens::Balance,
> fungibles::hold::DoneSlash<AssetId, HoldReason, AccountId, Balance>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
where
MinimumBalance::Type: tokens::Balance,
{
}
impl<
Instance: Get<u32>,
AccountId: Encode,
AssetId: tokens::AssetId + Copy,
MinimumBalance: TypedGet,
HoldReason: Encode + Decode + TypeInfo + 'static,
> fungibles::BalancedHold<AccountId>
for TestFungibles<Instance, AccountId, AssetId, MinimumBalance, HoldReason>
where
MinimumBalance::Type: tokens::Balance,
{
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,386 @@
// This file is part of Bizinikiwi.
// 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 alloc::{vec, vec::Vec};
use pezframe_support::{pezpallet_prelude::*, traits::defensive_prelude::*, weights::WeightMeter};
use pezsp_arithmetic::traits::{One, SaturatedConversion, Saturating, Zero};
use pezsp_runtime::traits::{BlockNumberProvider, ConvertBack, MaybeConvert};
use CompletionStatus::Complete;
impl<T: Config> Pallet<T> {
/// 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 meter = WeightMeter::new();
meter.consume(T::WeightInfo::do_tick_base());
let (mut status, config) = match (Status::<T>::get(), Configuration::<T>::get()) {
(Some(s), Some(c)) => (s, c),
_ => return meter.consumed(),
};
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::<T>::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::<T>::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());
T::Coretime::on_new_timeslice(status.last_timeslice);
meter.consume(T::WeightInfo::on_new_timeslice());
}
Status::<T>::put(&status);
meter.consumed()
}
pub(crate) fn process_core_count(status: &mut StatusRecord) -> bool {
if let Some(core_count) = CoreCountInbox::<T>::take() {
status.core_count = core_count;
Self::deposit_event(Event::<T>::CoreCountChanged { core_count });
return true;
}
false
}
pub(crate) fn process_revenue() -> bool {
let Some(OnDemandRevenueRecord { until, amount }) = RevenueInbox::<T>::take() else {
return false;
};
let when: Timeslice =
(until / T::TimeslicePeriod::get()).saturating_sub(One::one()).saturated_into();
let mut revenue = T::ConvertBalance::convert_back(amount.clone());
if revenue.is_zero() {
Self::deposit_event(Event::<T>::HistoryDropped { when, revenue });
InstaPoolHistory::<T>::remove(when);
return true;
}
log::debug!(
target: "pezpallet_broker::process_revenue",
"Received {amount:?} from RC, converted into {revenue:?} revenue",
);
let mut r = InstaPoolHistory::<T>::get(when).unwrap_or_default();
if r.maybe_payout.is_some() {
Self::deposit_event(Event::<T>::HistoryIgnored { when, revenue });
return true;
}
// Payout system InstaPool Cores.
let total_contrib = r.system_contributions.saturating_add(r.private_contributions);
let system_payout = if !total_contrib.is_zero() {
let system_payout =
revenue.saturating_mul(r.system_contributions.into()) / total_contrib.into();
Self::charge(&Self::account_id(), system_payout).defensive_ok();
revenue.saturating_reduce(system_payout);
system_payout
} else {
Zero::zero()
};
log::debug!(
target: "pezpallet_broker::process_revenue",
"Charged {system_payout:?} for system payouts, {revenue:?} remaining for private contributions",
);
if !revenue.is_zero() && r.private_contributions > 0 {
r.maybe_payout = Some(revenue);
InstaPoolHistory::<T>::insert(when, &r);
Self::deposit_event(Event::<T>::ClaimsReady {
when,
system_payout,
private_payout: revenue,
});
} else {
InstaPoolHistory::<T>::remove(when);
Self::deposit_event(Event::<T>::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<T>,
config: &ConfigRecordOf<T>,
status: &StatusRecord,
) -> Option<()> {
let now = RCBlockNumberProviderOf::<T::Coretime>::current_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::<T>::insert((old_sale.region_begin, old_sale.first_core + i), &just_pool);
}
InstaPoolIo::<T>::mutate(old_sale.region_begin, |r| r.system.saturating_accrue(old_pooled));
InstaPoolIo::<T>::mutate(old_sale.region_end, |r| r.system.saturating_reduce(old_pooled));
// Calculate the start price for the upcoming sale.
let new_prices = T::PriceAdapter::adapt_price(SalePerformance::from_sale(&old_sale));
log::debug!(
"Rotated sale, new prices: {:?}, {:?}",
new_prices.end_price,
new_prices.target_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::<T>::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::<T>::insert((region_begin, first_core), &schedule);
first_core.saturating_inc();
}
InstaPoolIo::<T>::mutate(region_begin, |r| r.system.saturating_accrue(total_pooled));
InstaPoolIo::<T>::mutate(region_end, |r| r.system.saturating_reduce(total_pooled));
let mut leases = Leases::<T>::get();
// Can morph to a renewable as long as it's >=begin and <end.
leases.retain(|&LeaseRecordItem { until, task }| {
let mask = CoreMask::complete();
let assignment = CoreAssignment::Task(task);
let schedule = BoundedVec::truncate_from(vec![ScheduleItem { mask, assignment }]);
Workplan::<T>::insert((region_begin, first_core), &schedule);
// Will the lease expire at the end of the period?
let expire = until < region_end;
if expire {
// last time for this one - make it renewable in the next sale.
let renewal_id = PotentialRenewalId { core: first_core, when: region_end };
let record = PotentialRenewalRecord {
price: new_prices.target_price,
completion: Complete(schedule),
};
PotentialRenewals::<T>::insert(renewal_id, &record);
Self::deposit_event(Event::Renewable {
core: first_core,
price: new_prices.target_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();
!expire
});
Leases::<T>::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;
let sellout_price = if cores_offered > 0 {
// No core sold -> price was too high -> we have to adjust downwards.
Some(new_prices.end_price)
} else {
None
};
// Update SaleInfo
let new_sale = SaleInfoRecord {
sale_start,
leadin_length,
end_price: new_prices.end_price,
sellout_price,
region_begin,
region_end,
first_core,
ideal_cores_sold,
cores_offered,
cores_sold: 0,
};
SaleInfo::<T>::put(&new_sale);
Self::renew_cores(&new_sale);
Self::deposit_event(Event::SaleInitialized {
sale_start,
leadin_length,
start_price: Self::sale_price(&new_sale, now),
end_price: new_prices.end_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::<T>::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::<T>::insert(when, record);
Self::deposit_event(Event::<T>::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<T>,
core: CoreIndex,
) {
let Some(workplan) = Workplan::<T>::take((timeslice, core)) else { return };
let workload = Workload::<T>::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::<T>::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::<Vec<_>>();
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::<T>::CoreAssigned { core, when: rc_begin, assignment });
}
/// Renews all the cores which have auto-renewal enabled.
pub(crate) fn renew_cores(sale: &SaleInfoRecordOf<T>) {
let renewals = AutoRenewals::<T>::get();
let Ok(auto_renewals) = renewals
.into_iter()
.flat_map(|record| {
// Check if the next renewal is scheduled further in the future than the start of
// the next region beginning. If so, we skip the renewal for this core.
if sale.region_begin < record.next_renewal {
return Some(record);
}
let Some(payer) = T::SovereignAccountOf::maybe_convert(record.task) else {
Self::deposit_event(Event::<T>::AutoRenewalFailed {
core: record.core,
payer: None,
});
return None;
};
if let Ok(new_core_index) = Self::do_renew(payer.clone(), record.core) {
Some(AutoRenewalRecord {
core: new_core_index,
task: record.task,
next_renewal: sale.region_end,
})
} else {
Self::deposit_event(Event::<T>::AutoRenewalFailed {
core: record.core,
payer: Some(payer),
});
None
}
})
.collect::<Vec<AutoRenewalRecord>>()
.try_into()
else {
Self::deposit_event(Event::<T>::AutoRenewalLimitReached);
return;
};
AutoRenewals::<T>::set(auto_renewals);
}
}
+378
View File
@@ -0,0 +1,378 @@
// This file is part of Bizinikiwi.
// 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, RCBlockNumberOf, TaskId,
CORE_MASK_BITS,
};
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use pezframe_support::traits::fungible::Inspect;
use pezframe_system::Config as SConfig;
use scale_info::TypeInfo;
use pezsp_arithmetic::Perbill;
use pezsp_core::{ConstU32, RuntimeDebug};
use pezsp_runtime::BoundedVec;
pub type BalanceOf<T> = <<T as Config>::Currency as Inspect<<T as SConfig>::AccountId>>::Balance;
pub type RelayBalanceOf<T> = <<T as Config>::Coretime as CoretimeInterface>::Balance;
pub type RelayBlockNumberOf<T> = RCBlockNumberOf<<T as Config>::Coretime>;
pub type RelayAccountIdOf<T> = <<T as Config>::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 Pezkuwi 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,
DecodeWithMemTracking,
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,
DecodeWithMemTracking,
Copy,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub struct RegionId {
/// The timeslice at which this Region begins.
pub begin: Timeslice,
/// The index of the Pezkuwi 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<u128> for RegionId {
fn from(x: u128) -> Self {
Self { begin: (x >> 96) as u32, core: (x >> 80) as u16, mask: x.into() }
}
}
impl From<RegionId> 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<AccountId, Balance> {
/// The end of the Region.
pub end: Timeslice,
/// The owner of the Region.
pub owner: Option<AccountId>,
/// The amount paid to Pezkuwi for this Region, or `None` if renewal is not allowed.
pub paid: Option<Balance>,
}
pub type RegionRecordOf<T> = RegionRecord<<T as SConfig>::AccountId, BalanceOf<T>>;
/// An distinct item which can be scheduled on a Pezkuwi Core.
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
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<ScheduleItem, ConstU32<{ CORE_MASK_BITS as u32 }>>;
/// 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<AccountId> {
/// The end of the Region contributed.
pub length: Timeslice,
/// The identity of the contributor.
pub payee: AccountId,
}
pub type ContributionRecordOf<T> = ContributionRecord<<T as SConfig>::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<Balance> {
/// 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 Pezkuwi 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<Balance>,
}
pub type InstaPoolHistoryRecordOf<T> = InstaPoolHistoryRecord<BalanceOf<T>>;
/// 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<Schedule> {
match self {
Self::Complete(s) => Some(s),
Self::Partial(_) => None,
}
}
}
/// The identity of a possibly renewable Core workload.
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct PotentialRenewalId {
/// 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 a potential renewal.
///
/// The renewal will only actually be allowed if `CompletionStatus` is `Complete` at the time of
/// renewal.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct PotentialRenewalRecord<Balance> {
/// 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 PotentialRenewalRecordOf<T> = PotentialRenewalRecord<BalanceOf<T>>;
/// 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 Pezkuwi
/// 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 Pezkuwi 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<Balance, RelayBlockNumber> {
/// The relay block number at which the sale will/did start.
pub sale_start: RelayBlockNumber,
/// The length in blocks of the Leadin Period (where the price is decreasing).
pub leadin_length: RelayBlockNumber,
/// The price of Bulk Coretime after the Leadin Period.
pub end_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 price at which cores have been sold out.
///
/// Will only be `None` if no core was offered for sale.
pub sellout_price: Option<Balance>,
/// Number of cores which have been sold; never more than cores_offered.
pub cores_sold: CoreIndex,
}
pub type SaleInfoRecordOf<T> = SaleInfoRecord<BalanceOf<T>, RelayBlockNumberOf<T>>;
/// Record for Pezkuwi Core reservations (generally tasked with the maintenance of System
/// Chains).
pub type ReservationsRecord<Max> = BoundedVec<Schedule, Max>;
pub type ReservationsRecordOf<T> = ReservationsRecord<<T as Config>::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 Pezkuwi Core legacy leases.
pub type LeasesRecord<Max> = BoundedVec<LeaseRecordItem, Max>;
pub type LeasesRecordOf<T> = LeasesRecord<<T as Config>::MaxLeasedCores>;
/// Record for On demand core sales.
///
/// The blocknumber is the relay chain block height `until` which the original request
/// for revenue was made.
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub struct OnDemandRevenueRecord<RelayBlockNumber, RelayBalance> {
/// The height of the Relay-chain at the time the revenue request was made.
pub until: RelayBlockNumber,
/// The accumulated balance of on demand sales made on the relay chain.
pub amount: RelayBalance,
}
pub type OnDemandRevenueRecordOf<T> =
OnDemandRevenueRecord<RelayBlockNumberOf<T>, RelayBalanceOf<T>>;
/// Configuration of this pallet.
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub struct ConfigRecord<RelayBlockNumber> {
/// 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: RelayBlockNumber,
/// The length in blocks of the Leadin Period for forthcoming sales.
pub leadin_length: RelayBlockNumber,
/// 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.
///
/// If more cores are sold than this, then further sales will no longer be considered in
/// determining the sellout price. In other words the sellout price will be the last price
/// paid, without going over this limit.
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<CoreIndex>,
/// 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<T> = ConfigRecord<RelayBlockNumberOf<T>>;
impl<RelayBlockNumber> ConfigRecord<RelayBlockNumber>
where
RelayBlockNumber: pezsp_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(())
}
}
/// A record containing information regarding auto-renewal for a specific core.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct AutoRenewalRecord {
/// The core for which auto renewal is enabled.
pub core: CoreIndex,
/// The task assigned to the core. We keep track of it so we don't have to look it up when
/// performing auto-renewal.
pub task: TaskId,
/// Specifies when the upcoming renewal should be performed. This is used for lease holding
/// tasks to ensure that the renewal process does not begin until the lease expires.
pub next_renewal: Timeslice,
}
@@ -0,0 +1,174 @@
// This file is part of Bizinikiwi.
// 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 pezframe_support::{
pezpallet_prelude::*,
traits::{
fungible::Balanced,
tokens::{Fortitude::Polite, Precision::Exact, Preservation::Expendable},
OnUnbalanced,
},
};
use pezsp_arithmetic::{
traits::{SaturatedConversion, Saturating},
FixedPointNumber, FixedU64,
};
use pezsp_runtime::traits::{AccountIdConversion, BlockNumberProvider};
impl<T: Config> Pallet<T> {
pub fn current_timeslice() -> Timeslice {
let latest = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
let timeslice_period = T::TimeslicePeriod::get();
(latest / timeslice_period).saturated_into()
}
pub fn latest_timeslice_ready_to_commit(config: &ConfigRecordOf<T>) -> Timeslice {
let latest = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
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<T>,
status: &StatusRecord,
) -> Option<Timeslice> {
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<T>, now: RelayBlockNumberOf<T>) -> BalanceOf<T> {
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.end_price)
}
pub(crate) fn charge(who: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {
let credit = T::Currency::withdraw(&who, amount, Exact, Expendable, Polite)?;
T::OnRevenue::on_unbalanced(credit);
Ok(())
}
/// Buy a core at the specified price (price is to be determined by the caller).
///
/// Note: It is the responsibility of the caller to write back the changed `SaleInfoRecordOf` to
/// storage.
pub(crate) fn purchase_core(
who: &T::AccountId,
price: BalanceOf<T>,
sale: &mut SaleInfoRecordOf<T>,
) -> Result<CoreIndex, DispatchError> {
Self::charge(who, price)?;
log::debug!("Purchased core at: {:?}", 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);
}
Ok(core)
}
pub fn issue(
core: CoreIndex,
begin: Timeslice,
mask: CoreMask,
end: Timeslice,
owner: Option<T::AccountId>,
paid: Option<BalanceOf<T>>,
) -> RegionId {
let id = RegionId { begin, core, mask };
let record = RegionRecord { end, owner, paid };
Regions::<T>::insert(&id, &record);
id
}
pub(crate) fn utilize(
mut region_id: RegionId,
maybe_check_owner: Option<T::AccountId>,
finality: Finality,
) -> Result<Option<(RegionId, RegionRecordOf<T>)>, Error<T>> {
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
let region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
if let Some(check_owner) = maybe_check_owner {
ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
}
Regions::<T>::remove(&region_id);
let last_committed_timeslice = status.last_committed_timeslice;
if region_id.begin <= last_committed_timeslice {
let duration = region.end.saturating_sub(region_id.begin);
region_id.begin = last_committed_timeslice + 1;
if region_id.begin >= region.end {
Self::deposit_event(Event::RegionDropped { region_id, duration });
return Ok(None);
}
} else {
Workplan::<T>::mutate_extant((region_id.begin, region_id.core), |p| {
p.retain(|i| (i.mask & region_id.mask).is_void())
});
}
if finality == Finality::Provisional {
Regions::<T>::insert(&region_id, &region);
}
Ok(Some((region_id, region)))
}
// Remove a region from on-demand pool contributions. Useful in cases where it was pooled
// provisionally and it is being redispatched (partition/interlace/assign).
//
// Takes both the region_id and (a reference to) the region as arguments to avoid another DB
// read. No-op for regions which have not been pooled.
pub(crate) fn force_unpool_region(
region_id: RegionId,
region: &RegionRecordOf<T>,
status: &StatusRecord,
) {
// We don't care if this fails or not, just that it is removed if present. This is to
// account for the case where a region is pooled provisionally and redispatched.
if InstaPoolContribution::<T>::take(region_id).is_some() {
// `InstaPoolHistory` is calculated from the `InstaPoolIo` one timeslice in advance.
// Therefore we need to schedule this for the timeslice after that.
let end_timeslice = status.last_committed_timeslice + 1;
// InstaPoolIo has already accounted for regions that have already ended. Regions ending
// this timeslice would have region.end == unpooled_at below.
if region.end <= end_timeslice {
return;
}
// Account for the change in `InstaPoolIo` either from the start of the region or from
// the current timeslice if we are already part-way through the region.
let size = region_id.mask.count_ones() as i32;
let unpooled_at = end_timeslice.max(region_id.begin);
InstaPoolIo::<T>::mutate(unpooled_at, |a| a.private.saturating_reduce(size));
InstaPoolIo::<T>::mutate(region.end, |a| a.private.saturating_accrue(size));
Self::deposit_event(Event::<T>::RegionUnpooled { region_id, when: unpooled_at });
};
}
}
File diff suppressed because it is too large Load Diff