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:
@@ -0,0 +1,63 @@
|
||||
[package]
|
||||
name = "pezpallet-broker"
|
||||
version = "0.6.0"
|
||||
description = "Brokerage tool for managing Pezkuwi Core scheduling"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license = "Apache-2.0"
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
bitvec = { workspace = true }
|
||||
codec = { features = ["derive"], workspace = true }
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { workspace = true }
|
||||
pezframe-system = { workspace = true }
|
||||
log = { workspace = true }
|
||||
scale-info = { features = ["derive"], workspace = true }
|
||||
pezsp-api = { workspace = true }
|
||||
pezsp-arithmetic = { workspace = true }
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-runtime = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
pezsp-io = { workspace = true, default-features = true }
|
||||
pezsp-tracing = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bitvec/std",
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"scale-info/std",
|
||||
"pezsp-api/std",
|
||||
"pezsp-arithmetic/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezsp-api/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Pallet Broker
|
||||
|
||||
Brokerage tool for managing PezkuwiChain Core scheduling.
|
||||
|
||||
Properly described in [RFC-0001 Agile Coretime](https://github.com/polkadot-fellows/RFCs/blob/main/text/0001-agile-coretime.md).
|
||||
|
||||
## Implementation Specifics
|
||||
|
||||
### Core Mask Bits
|
||||
|
||||
This is 1/80th of a PezkuwiChain Core per timeslice. Assuming timeslices are 80 blocks, then this
|
||||
indicates usage of a single core one time over a timeslice.
|
||||
|
||||
### The Sale
|
||||
|
||||
```nocompile
|
||||
1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7
|
||||
--------------------------------------------------------
|
||||
< interlude >
|
||||
< sale >
|
||||
... of which ...
|
||||
< descending-price >< fixed-price >
|
||||
| <-------\
|
||||
price fixed, unsold assigned to instapool, system cores reserved -/
|
||||
```
|
||||
@@ -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
@@ -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(®ion_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(®ion_id, ®ion);
|
||||
let duration = region.end.saturating_sub(region_id.begin);
|
||||
Self::deposit_event(Event::Transferred {
|
||||
region_id,
|
||||
old_owner,
|
||||
owner: region.owner,
|
||||
duration,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn do_partition(
|
||||
region_id: RegionId,
|
||||
maybe_check_owner: Option<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(®ion_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, ®ion, &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, ®ion);
|
||||
Self::deposit_event(Event::Partitioned { old_region_id: region_id, new_region_ids });
|
||||
|
||||
Ok(new_region_ids)
|
||||
}
|
||||
|
||||
pub(crate) fn do_interlace(
|
||||
region_id: RegionId,
|
||||
maybe_check_owner: Option<T::AccountId>,
|
||||
pivot: CoreMask,
|
||||
) -> Result<(RegionId, RegionId), Error<T>> {
|
||||
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
|
||||
let region = Regions::<T>::get(®ion_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, ®ion, &status);
|
||||
|
||||
// The old region should be removed.
|
||||
Regions::<T>::remove(®ion_id);
|
||||
|
||||
let one = RegionId { mask: pivot, ..region_id };
|
||||
Regions::<T>::insert(&one, ®ion);
|
||||
let other = RegionId { mask: region_id.mask ^ pivot, ..region_id };
|
||||
Regions::<T>::insert(&other, ®ion);
|
||||
|
||||
let new_region_ids = (one, other);
|
||||
Self::deposit_event(Event::Interlaced { old_region_id: region_id, new_region_ids });
|
||||
Ok(new_region_ids)
|
||||
}
|
||||
|
||||
pub(crate) fn do_assign(
|
||||
region_id: RegionId,
|
||||
maybe_check_owner: Option<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, ®ion, &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(®ion_id, record);
|
||||
}
|
||||
|
||||
Self::deposit_event(Event::Pooled { region_id, duration });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn do_claim_revenue(
|
||||
mut region: RegionId,
|
||||
max_timeslices: Timeslice,
|
||||
) -> DispatchResult {
|
||||
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(®ion_id).ok_or(Error::<T>::UnknownRegion)?;
|
||||
ensure!(status.last_committed_timeslice >= region.end, Error::<T>::StillValid);
|
||||
|
||||
Regions::<T>::remove(®ion_id);
|
||||
let duration = region.end.saturating_sub(region_id.begin);
|
||||
Self::deposit_event(Event::RegionDropped { region_id, duration });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn do_drop_contribution(region_id: RegionId) -> DispatchResult {
|
||||
let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
|
||||
let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
|
||||
let contrib =
|
||||
InstaPoolContribution::<T>::get(®ion_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
@@ -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,
|
||||
>;
|
||||
@@ -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(®ion_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(®ion_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);
|
||||
}
|
||||
}
|
||||
@@ -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(®ion_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(®ion_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(®ion_id, ®ion);
|
||||
}
|
||||
|
||||
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
Reference in New Issue
Block a user