// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use super::*; use frame_support::{ pallet_prelude::{DispatchResult, *}, traits::{fungible::Mutate, tokens::Preservation::Expendable, DefensiveResult}, }; use sp_arithmetic::traits::{CheckedDiv, Saturating, Zero}; use sp_runtime::traits::Convert; use CompletionStatus::{Complete, Partial}; impl Pallet { pub(crate) fn do_configure(config: ConfigRecordOf) -> DispatchResult { config.validate().map_err(|()| Error::::InvalidConfig)?; Configuration::::put(config); Ok(()) } pub(crate) fn do_request_core_count(core_count: CoreIndex) -> DispatchResult { T::Coretime::request_core_count(core_count); Self::deposit_event(Event::::CoreCountRequested { core_count }); Ok(()) } pub(crate) fn do_notify_core_count(core_count: CoreIndex) -> DispatchResult { CoreCountInbox::::put(core_count); Ok(()) } pub(crate) fn do_reserve(workload: Schedule) -> DispatchResult { let mut r = Reservations::::get(); let index = r.len() as u32; r.try_push(workload.clone()).map_err(|_| Error::::TooManyReservations)?; Reservations::::put(r); Self::deposit_event(Event::::ReservationMade { index, workload }); Ok(()) } pub(crate) fn do_unreserve(index: u32) -> DispatchResult { let mut r = Reservations::::get(); ensure!(index < r.len() as u32, Error::::UnknownReservation); let workload = r.remove(index as usize); Reservations::::put(r); Self::deposit_event(Event::::ReservationCancelled { index, workload }); Ok(()) } pub(crate) fn do_set_lease(task: TaskId, until: Timeslice) -> DispatchResult { let mut r = Leases::::get(); ensure!(until > Self::current_timeslice(), Error::::AlreadyExpired); r.try_push(LeaseRecordItem { until, task }) .map_err(|_| Error::::TooManyLeases)?; Leases::::put(r); Self::deposit_event(Event::::Leased { until, task }); Ok(()) } pub(crate) fn do_start_sales(price: BalanceOf, extra_cores: CoreIndex) -> DispatchResult { let config = Configuration::::get().ok_or(Error::::Uninitialized)?; // Determine the core count let core_count = Leases::::decode_len().unwrap_or(0) as CoreIndex + Reservations::::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 = frame_system::Pallet::::block_number(); // Imaginary old sale for bootstrapping the first actual sale: let old_sale = SaleInfoRecord { sale_start: now, leadin_length: Zero::zero(), price, sellout_price: None, region_begin: commit_timeslice, region_end: commit_timeslice.saturating_add(config.region_length), first_core: 0, ideal_cores_sold: 0, cores_offered: 0, cores_sold: 0, }; Self::deposit_event(Event::::SalesStarted { price, core_count }); Self::rotate_sale(old_sale, &config, &status); Status::::put(&status); Ok(()) } pub(crate) fn do_purchase( who: T::AccountId, price_limit: BalanceOf, ) -> Result { let status = Status::::get().ok_or(Error::::Uninitialized)?; let mut sale = SaleInfo::::get().ok_or(Error::::NoSales)?; Self::ensure_cores_for_sale(&status, &sale)?; let now = frame_system::Pallet::::block_number(); ensure!(now > sale.sale_start, Error::::TooEarly); let price = Self::sale_price(&sale, now); ensure!(price_limit >= price, Error::::Overpriced); Self::charge(&who, price)?; let core = sale.first_core.saturating_add(sale.cores_sold); sale.cores_sold.saturating_inc(); if sale.cores_sold <= sale.ideal_cores_sold || sale.sellout_price.is_none() { sale.sellout_price = Some(price); } SaleInfo::::put(&sale); let id = Self::issue(core, sale.region_begin, sale.region_end, 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 `AllowedRenewals` whose value is a timeslice equal to the /// current sale status's `region_end`. pub(crate) fn do_renew(who: T::AccountId, core: CoreIndex) -> Result { let config = Configuration::::get().ok_or(Error::::Uninitialized)?; let status = Status::::get().ok_or(Error::::Uninitialized)?; let mut sale = SaleInfo::::get().ok_or(Error::::NoSales)?; Self::ensure_cores_for_sale(&status, &sale)?; let renewal_id = AllowedRenewalId { core, when: sale.region_begin }; let record = AllowedRenewals::::get(renewal_id).ok_or(Error::::NotAllowed)?; let workload = record.completion.drain_complete().ok_or(Error::::IncompleteAssignment)?; let old_core = core; let core = sale.first_core.saturating_add(sale.cores_sold); Self::charge(&who, record.price)?; Self::deposit_event(Event::Renewed { who, old_core, core, price: record.price, begin: sale.region_begin, duration: sale.region_end.saturating_sub(sale.region_begin), workload: workload.clone(), }); sale.cores_sold.saturating_inc(); Workplan::::insert((sale.region_begin, core), &workload); let begin = sale.region_end; let price_cap = record.price + config.renewal_bump * record.price; let now = frame_system::Pallet::::block_number(); let price = Self::sale_price(&sale, now).min(price_cap); let new_record = AllowedRenewalRecord { price, completion: Complete(workload) }; AllowedRenewals::::remove(renewal_id); AllowedRenewals::::insert(AllowedRenewalId { core, when: begin }, &new_record); SaleInfo::::put(&sale); if let Some(workload) = new_record.completion.drain_complete() { Self::deposit_event(Event::Renewable { core, price, begin, workload }); } Ok(core) } pub(crate) fn do_transfer( region_id: RegionId, maybe_check_owner: Option, new_owner: T::AccountId, ) -> Result<(), Error> { let mut region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; if let Some(check_owner) = maybe_check_owner { ensure!(Some(check_owner) == region.owner, Error::::NotOwner); } let old_owner = region.owner; region.owner = Some(new_owner); Regions::::insert(®ion_id, ®ion); let duration = region.end.saturating_sub(region_id.begin); Self::deposit_event(Event::Transferred { region_id, old_owner, owner: region.owner, duration, }); Ok(()) } pub(crate) fn do_partition( region_id: RegionId, maybe_check_owner: Option, pivot_offset: Timeslice, ) -> Result<(RegionId, RegionId), Error> { let mut region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; if let Some(check_owner) = maybe_check_owner { ensure!(Some(check_owner) == region.owner, Error::::NotOwner); } let pivot = region_id.begin.saturating_add(pivot_offset); ensure!(pivot < region.end, Error::::PivotTooLate); ensure!(pivot > region_id.begin, Error::::PivotTooEarly); region.paid = None; let new_region_ids = (region_id, RegionId { begin: pivot, ..region_id }); Regions::::insert(&new_region_ids.0, &RegionRecord { end: pivot, ..region.clone() }); Regions::::insert(&new_region_ids.1, ®ion); Self::deposit_event(Event::Partitioned { old_region_id: region_id, new_region_ids }); Ok(new_region_ids) } pub(crate) fn do_interlace( region_id: RegionId, maybe_check_owner: Option, pivot: CoreMask, ) -> Result<(RegionId, RegionId), Error> { let region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; if let Some(check_owner) = maybe_check_owner { ensure!(Some(check_owner) == region.owner, Error::::NotOwner); } ensure!((pivot & !region_id.mask).is_void(), Error::::ExteriorPivot); ensure!(!pivot.is_void(), Error::::VoidPivot); ensure!(pivot != region_id.mask, Error::::CompletePivot); // The old region should be removed. Regions::::remove(®ion_id); let one = RegionId { mask: pivot, ..region_id }; Regions::::insert(&one, ®ion); let other = RegionId { mask: region_id.mask ^ pivot, ..region_id }; Regions::::insert(&other, ®ion); let new_region_ids = (one, other); Self::deposit_event(Event::Interlaced { old_region_id: region_id, new_region_ids }); Ok(new_region_ids) } pub(crate) fn do_assign( region_id: RegionId, maybe_check_owner: Option, target: TaskId, finality: Finality, ) -> Result<(), Error> { let config = Configuration::::get().ok_or(Error::::Uninitialized)?; if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? { let workplan_key = (region_id.begin, region_id.core); let mut workplan = Workplan::::get(&workplan_key).unwrap_or_default(); // Ensure no previous allocations exist. workplan.retain(|i| (i.mask & region_id.mask).is_void()); if workplan .try_push(ScheduleItem { mask: region_id.mask, assignment: CoreAssignment::Task(target), }) .is_ok() { Workplan::::insert(&workplan_key, &workplan); } let duration = region.end.saturating_sub(region_id.begin); if duration == config.region_length && finality == Finality::Final { if let Some(price) = region.paid { let renewal_id = AllowedRenewalId { core: region_id.core, when: region.end }; let assigned = match AllowedRenewals::::get(renewal_id) { Some(AllowedRenewalRecord { completion: Partial(w), price: p }) if price == p => w, _ => CoreMask::void(), } | region_id.mask; let workload = if assigned.is_complete() { Complete(workplan) } else { Partial(assigned) }; let record = AllowedRenewalRecord { price, completion: workload }; AllowedRenewals::::insert(&renewal_id, &record); if let Some(workload) = record.completion.drain_complete() { Self::deposit_event(Event::Renewable { core: region_id.core, price, begin: region.end, workload, }); } } } Self::deposit_event(Event::Assigned { region_id, task: target, duration }); } Ok(()) } pub(crate) fn do_pool( region_id: RegionId, maybe_check_owner: Option, payee: T::AccountId, finality: Finality, ) -> Result<(), Error> { if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? { let workplan_key = (region_id.begin, region_id.core); let mut workplan = Workplan::::get(&workplan_key).unwrap_or_default(); let duration = region.end.saturating_sub(region_id.begin); if workplan .try_push(ScheduleItem { mask: region_id.mask, assignment: CoreAssignment::Pool }) .is_ok() { Workplan::::insert(&workplan_key, &workplan); let size = region_id.mask.count_ones() as i32; InstaPoolIo::::mutate(region_id.begin, |a| a.private.saturating_accrue(size)); InstaPoolIo::::mutate(region.end, |a| a.private.saturating_reduce(size)); let record = ContributionRecord { length: duration, payee }; InstaPoolContribution::::insert(®ion_id, record); } Self::deposit_event(Event::Pooled { region_id, duration }); } Ok(()) } pub(crate) fn do_claim_revenue( mut region: RegionId, max_timeslices: Timeslice, ) -> DispatchResult { ensure!(max_timeslices > 0, Error::::NoClaimTimeslices); let mut contribution = InstaPoolContribution::::take(region).ok_or(Error::::UnknownContribution)?; let contributed_parts = region.mask.count_ones(); Self::deposit_event(Event::RevenueClaimBegun { region, max_timeslices }); let mut payout = BalanceOf::::zero(); let last = region.begin + contribution.length.min(max_timeslices); for r in region.begin..last { region.begin = r + 1; contribution.length.saturating_dec(); let Some(mut pool_record) = InstaPoolHistory::::get(r) else { continue }; let Some(total_payout) = pool_record.maybe_payout else { break }; let p = total_payout .saturating_mul(contributed_parts.into()) .checked_div(&pool_record.private_contributions.into()) .unwrap_or_default(); payout.saturating_accrue(p); pool_record.private_contributions.saturating_reduce(contributed_parts); let remaining_payout = total_payout.saturating_sub(p); if !remaining_payout.is_zero() && pool_record.private_contributions > 0 { pool_record.maybe_payout = Some(remaining_payout); InstaPoolHistory::::insert(r, &pool_record); } else { InstaPoolHistory::::remove(r); } if !p.is_zero() { Self::deposit_event(Event::RevenueClaimItem { when: r, amount: p }); } } if contribution.length > 0 { InstaPoolContribution::::insert(region, &contribution); } T::Currency::transfer(&Self::account_id(), &contribution.payee, payout, Expendable) .defensive_ok(); let next = if last < region.begin + contribution.length { Some(region) } else { None }; Self::deposit_event(Event::RevenueClaimPaid { who: contribution.payee, amount: payout, next, }); Ok(()) } pub(crate) fn do_purchase_credit( who: T::AccountId, amount: BalanceOf, beneficiary: RelayAccountIdOf, ) -> DispatchResult { T::Currency::transfer(&who, &Self::account_id(), amount, Expendable)?; let rc_amount = T::ConvertBalance::convert(amount); T::Coretime::credit_account(beneficiary.clone(), rc_amount); Self::deposit_event(Event::::CreditPurchased { who, beneficiary, amount }); Ok(()) } pub(crate) fn do_drop_region(region_id: RegionId) -> DispatchResult { let status = Status::::get().ok_or(Error::::Uninitialized)?; let region = Regions::::get(®ion_id).ok_or(Error::::UnknownRegion)?; ensure!(status.last_committed_timeslice >= region.end, Error::::StillValid); Regions::::remove(®ion_id); let duration = region.end.saturating_sub(region_id.begin); Self::deposit_event(Event::RegionDropped { region_id, duration }); Ok(()) } pub(crate) fn do_drop_contribution(region_id: RegionId) -> DispatchResult { let config = Configuration::::get().ok_or(Error::::Uninitialized)?; let status = Status::::get().ok_or(Error::::Uninitialized)?; let contrib = InstaPoolContribution::::get(®ion_id).ok_or(Error::::UnknownContribution)?; let end = region_id.begin.saturating_add(contrib.length); ensure!( status.last_timeslice >= end.saturating_add(config.contribution_timeout), Error::::StillValid ); InstaPoolContribution::::remove(region_id); Self::deposit_event(Event::ContributionDropped { region_id }); Ok(()) } pub(crate) fn do_drop_history(when: Timeslice) -> DispatchResult { let config = Configuration::::get().ok_or(Error::::Uninitialized)?; let status = Status::::get().ok_or(Error::::Uninitialized)?; ensure!( status.last_timeslice > when.saturating_add(config.contribution_timeout), Error::::StillValid ); let record = InstaPoolHistory::::take(when).ok_or(Error::::NoHistory)?; if let Some(payout) = record.maybe_payout { let _ = Self::charge(&Self::account_id(), payout); } let revenue = record.maybe_payout.unwrap_or_default(); Self::deposit_event(Event::HistoryDropped { when, revenue }); Ok(()) } pub(crate) fn do_drop_renewal(core: CoreIndex, when: Timeslice) -> DispatchResult { let status = Status::::get().ok_or(Error::::Uninitialized)?; ensure!(status.last_committed_timeslice >= when, Error::::StillValid); let id = AllowedRenewalId { core, when }; ensure!(AllowedRenewals::::contains_key(id), Error::::UnknownRenewal); AllowedRenewals::::remove(id); Self::deposit_event(Event::AllowedRenewalDropped { core, when }); Ok(()) } pub(crate) fn do_swap_leases(id: TaskId, other: TaskId) -> DispatchResult { let mut id_leases_count = 0; let mut other_leases_count = 0; Leases::::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 ensure_cores_for_sale( status: &StatusRecord, sale: &SaleInfoRecordOf, ) -> Result<(), DispatchError> { ensure!(sale.first_core < status.core_count, Error::::Unavailable); ensure!(sale.cores_sold < sale.cores_offered, Error::::SoldOut); Ok(()) } /// If there is an ongoing sale returns the current price of a core. pub fn current_price() -> Result, DispatchError> { let status = Status::::get().ok_or(Error::::Uninitialized)?; let sale = SaleInfo::::get().ok_or(Error::::NoSales)?; Self::ensure_cores_for_sale(&status, &sale)?; let now = frame_system::Pallet::::block_number(); Ok(Self::sale_price(&sale, now)) } }