referrerpolicy=no-referrer-when-downgrade

pallet_broker/
dispatchable_impls.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use core::cmp;
19
20use super::*;
21use frame_support::{
22	pallet_prelude::*,
23	traits::{fungible::Mutate, tokens::Preservation::Expendable, DefensiveResult},
24};
25use sp_arithmetic::traits::{CheckedDiv, Saturating, Zero};
26use sp_runtime::traits::{BlockNumberProvider, Convert};
27use CompletionStatus::{Complete, Partial};
28
29impl<T: Config> Pallet<T> {
30	pub(crate) fn do_configure(config: ConfigRecordOf<T>) -> DispatchResult {
31		config.validate().map_err(|()| Error::<T>::InvalidConfig)?;
32		Configuration::<T>::put(config);
33		Ok(())
34	}
35
36	pub(crate) fn do_request_core_count(core_count: CoreIndex) -> DispatchResult {
37		T::Coretime::request_core_count(core_count);
38		Self::deposit_event(Event::<T>::CoreCountRequested { core_count });
39		Ok(())
40	}
41
42	pub(crate) fn do_notify_core_count(core_count: CoreIndex) -> DispatchResult {
43		CoreCountInbox::<T>::put(core_count);
44		Ok(())
45	}
46
47	pub(crate) fn do_reserve(workload: Schedule) -> DispatchResult {
48		let mut r = Reservations::<T>::get();
49		let index = r.len() as u32;
50		r.try_push(workload.clone()).map_err(|_| Error::<T>::TooManyReservations)?;
51		Reservations::<T>::put(r);
52		Self::deposit_event(Event::<T>::ReservationMade { index, workload });
53		Ok(())
54	}
55
56	pub(crate) fn do_unreserve(index: u32) -> DispatchResult {
57		let mut r = Reservations::<T>::get();
58		ensure!(index < r.len() as u32, Error::<T>::UnknownReservation);
59		let workload = r.remove(index as usize);
60		Reservations::<T>::put(r);
61		Self::deposit_event(Event::<T>::ReservationCancelled { index, workload });
62		Ok(())
63	}
64
65	pub(crate) fn do_force_reserve(workload: Schedule, core: CoreIndex) -> DispatchResult {
66		// Sales must have started, otherwise reserve is equivalent.
67		let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
68
69		// Reserve - starts at second sale period boundary from now.
70		Self::do_reserve(workload.clone())?;
71
72		// Add to workload - grants one region from the next sale boundary.
73		Workplan::<T>::insert((sale.region_begin, core), &workload);
74
75		// Assign now until the next sale boundary unless the next timeslice is already the sale
76		// boundary.
77		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
78		let timeslice = status.last_committed_timeslice.saturating_add(1);
79		if timeslice < sale.region_begin {
80			Workplan::<T>::insert((timeslice, core), &workload);
81		}
82
83		Ok(())
84	}
85
86	pub(crate) fn do_set_lease(task: TaskId, until: Timeslice) -> DispatchResult {
87		let mut r = Leases::<T>::get();
88		ensure!(until > Self::current_timeslice(), Error::<T>::AlreadyExpired);
89		r.try_push(LeaseRecordItem { until, task })
90			.map_err(|_| Error::<T>::TooManyLeases)?;
91		Leases::<T>::put(r);
92		Self::deposit_event(Event::<T>::Leased { until, task });
93		Ok(())
94	}
95
96	pub(crate) fn do_remove_lease(task: TaskId) -> DispatchResult {
97		let mut r = Leases::<T>::get();
98		let i = r.iter().position(|lease| lease.task == task).ok_or(Error::<T>::LeaseNotFound)?;
99		r.remove(i);
100		Leases::<T>::put(r);
101		Self::deposit_event(Event::<T>::LeaseRemoved { task });
102		Ok(())
103	}
104
105	pub(crate) fn do_start_sales(
106		end_price: BalanceOf<T>,
107		extra_cores: CoreIndex,
108	) -> DispatchResult {
109		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
110
111		// Determine the core count
112		let core_count = Leases::<T>::decode_len().unwrap_or(0) as CoreIndex +
113			Reservations::<T>::decode_len().unwrap_or(0) as CoreIndex +
114			extra_cores;
115
116		Self::do_request_core_count(core_count)?;
117
118		let commit_timeslice = Self::latest_timeslice_ready_to_commit(&config);
119		let status = StatusRecord {
120			core_count,
121			private_pool_size: 0,
122			system_pool_size: 0,
123			last_committed_timeslice: commit_timeslice.saturating_sub(1),
124			last_timeslice: Self::current_timeslice(),
125		};
126		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
127		// Imaginary old sale for bootstrapping the first actual sale:
128		let old_sale = SaleInfoRecord {
129			sale_start: now,
130			leadin_length: Zero::zero(),
131			end_price,
132			sellout_price: None,
133			region_begin: commit_timeslice,
134			region_end: commit_timeslice.saturating_add(config.region_length),
135			first_core: 0,
136			ideal_cores_sold: 0,
137			cores_offered: 0,
138			cores_sold: 0,
139		};
140		Self::deposit_event(Event::<T>::SalesStarted { price: end_price, core_count });
141		Self::rotate_sale(old_sale, &config, &status);
142		Status::<T>::put(&status);
143		Ok(())
144	}
145
146	pub(crate) fn do_purchase(
147		who: T::AccountId,
148		price_limit: BalanceOf<T>,
149	) -> Result<RegionId, DispatchError> {
150		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
151		let mut sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
152		Self::ensure_cores_for_sale(&status, &sale)?;
153
154		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
155		ensure!(now > sale.sale_start, Error::<T>::TooEarly);
156		let price = Self::sale_price(&sale, now);
157		ensure!(price_limit >= price, Error::<T>::Overpriced);
158
159		let core = Self::purchase_core(&who, price, &mut sale)?;
160
161		SaleInfo::<T>::put(&sale);
162		let id = Self::issue(
163			core,
164			sale.region_begin,
165			CoreMask::complete(),
166			sale.region_end,
167			Some(who.clone()),
168			Some(price),
169		);
170		let duration = sale.region_end.saturating_sub(sale.region_begin);
171		Self::deposit_event(Event::Purchased { who, region_id: id, price, duration });
172		Ok(id)
173	}
174
175	/// Must be called on a core in `PotentialRenewals` whose value is a timeslice equal to the
176	/// current sale status's `region_end`.
177	pub(crate) fn do_renew(who: T::AccountId, core: CoreIndex) -> Result<CoreIndex, DispatchError> {
178		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
179		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
180		let mut sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
181		Self::ensure_cores_for_sale(&status, &sale)?;
182
183		let renewal_id = PotentialRenewalId { core, when: sale.region_begin };
184		let record = PotentialRenewals::<T>::get(renewal_id).ok_or(Error::<T>::NotAllowed)?;
185		let workload =
186			record.completion.drain_complete().ok_or(Error::<T>::IncompleteAssignment)?;
187
188		let old_core = core;
189
190		let core = Self::purchase_core(&who, record.price, &mut sale)?;
191
192		Self::deposit_event(Event::Renewed {
193			who,
194			old_core,
195			core,
196			price: record.price,
197			begin: sale.region_begin,
198			duration: sale.region_end.saturating_sub(sale.region_begin),
199			workload: workload.clone(),
200		});
201
202		Workplan::<T>::insert((sale.region_begin, core), &workload);
203
204		let begin = sale.region_end;
205		let end_price = sale.end_price;
206		// Renewals should never be priced lower than the current `end_price`:
207		let price_cap = cmp::max(record.price + config.renewal_bump * record.price, end_price);
208		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
209		let price = Self::sale_price(&sale, now).min(price_cap);
210		log::debug!(
211			"Renew with: sale price: {:?}, price cap: {:?}, old price: {:?}",
212			price,
213			price_cap,
214			record.price
215		);
216		let new_record = PotentialRenewalRecord { price, completion: Complete(workload) };
217		PotentialRenewals::<T>::remove(renewal_id);
218		PotentialRenewals::<T>::insert(PotentialRenewalId { core, when: begin }, &new_record);
219		SaleInfo::<T>::put(&sale);
220		if let Some(workload) = new_record.completion.drain_complete() {
221			log::debug!("Recording renewable price for next run: {:?}", price);
222			Self::deposit_event(Event::Renewable { core, price, begin, workload });
223		}
224		Ok(core)
225	}
226
227	pub(crate) fn do_transfer(
228		region_id: RegionId,
229		maybe_check_owner: Option<T::AccountId>,
230		new_owner: T::AccountId,
231	) -> Result<(), Error<T>> {
232		let mut region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
233
234		if let Some(check_owner) = maybe_check_owner {
235			ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
236		}
237
238		let old_owner = region.owner;
239		region.owner = Some(new_owner);
240		Regions::<T>::insert(&region_id, &region);
241		let duration = region.end.saturating_sub(region_id.begin);
242		Self::deposit_event(Event::Transferred {
243			region_id,
244			old_owner,
245			owner: region.owner,
246			duration,
247		});
248
249		Ok(())
250	}
251
252	pub(crate) fn do_partition(
253		region_id: RegionId,
254		maybe_check_owner: Option<T::AccountId>,
255		pivot_offset: Timeslice,
256	) -> Result<(RegionId, RegionId), Error<T>> {
257		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
258		let mut region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
259
260		if let Some(check_owner) = maybe_check_owner {
261			ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
262		}
263		let pivot = region_id.begin.saturating_add(pivot_offset);
264		ensure!(pivot < region.end, Error::<T>::PivotTooLate);
265		ensure!(pivot > region_id.begin, Error::<T>::PivotTooEarly);
266
267		region.paid = None;
268		let new_region_ids = (region_id, RegionId { begin: pivot, ..region_id });
269
270		// Remove this region from the pool in case it has been assigned provisionally. If we get
271		// this far then it is still in `Regions` and thus could only have been pooled
272		// provisionally.
273		Self::force_unpool_region(region_id, &region, &status);
274
275		// Overwrite the previous region with its new end and create a new region for the second
276		// part of the partition.
277		Regions::<T>::insert(&new_region_ids.0, &RegionRecord { end: pivot, ..region.clone() });
278		Regions::<T>::insert(&new_region_ids.1, &region);
279		Self::deposit_event(Event::Partitioned { old_region_id: region_id, new_region_ids });
280
281		Ok(new_region_ids)
282	}
283
284	pub(crate) fn do_interlace(
285		region_id: RegionId,
286		maybe_check_owner: Option<T::AccountId>,
287		pivot: CoreMask,
288	) -> Result<(RegionId, RegionId), Error<T>> {
289		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
290		let region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
291
292		if let Some(check_owner) = maybe_check_owner {
293			ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
294		}
295
296		ensure!((pivot & !region_id.mask).is_void(), Error::<T>::ExteriorPivot);
297		ensure!(!pivot.is_void(), Error::<T>::VoidPivot);
298		ensure!(pivot != region_id.mask, Error::<T>::CompletePivot);
299
300		// Remove this region from the pool in case it has been assigned provisionally. If we get
301		// this far then it is still in `Regions` and thus could only have been pooled
302		// provisionally.
303		Self::force_unpool_region(region_id, &region, &status);
304
305		// The old region should be removed.
306		Regions::<T>::remove(&region_id);
307
308		let one = RegionId { mask: pivot, ..region_id };
309		Regions::<T>::insert(&one, &region);
310		let other = RegionId { mask: region_id.mask ^ pivot, ..region_id };
311		Regions::<T>::insert(&other, &region);
312
313		let new_region_ids = (one, other);
314		Self::deposit_event(Event::Interlaced { old_region_id: region_id, new_region_ids });
315		Ok(new_region_ids)
316	}
317
318	pub(crate) fn do_assign(
319		region_id: RegionId,
320		maybe_check_owner: Option<T::AccountId>,
321		target: TaskId,
322		finality: Finality,
323	) -> Result<(), Error<T>> {
324		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
325		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
326
327		if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? {
328			let workplan_key = (region_id.begin, region_id.core);
329			let mut workplan = Workplan::<T>::get(&workplan_key).unwrap_or_default();
330
331			// Remove this region from the pool in case it has been assigned provisionally. If we
332			// get this far then it is still in `Regions` and thus could only have been pooled
333			// provisionally.
334			Self::force_unpool_region(region_id, &region, &status);
335
336			// Ensure no previous allocations exist.
337			workplan.retain(|i| (i.mask & region_id.mask).is_void());
338			if workplan
339				.try_push(ScheduleItem {
340					mask: region_id.mask,
341					assignment: CoreAssignment::Task(target),
342				})
343				.is_ok()
344			{
345				Workplan::<T>::insert(&workplan_key, &workplan);
346			}
347
348			let duration = region.end.saturating_sub(region_id.begin);
349			if duration == config.region_length && finality == Finality::Final {
350				if let Some(price) = region.paid {
351					let renewal_id = PotentialRenewalId { core: region_id.core, when: region.end };
352					let assigned = match PotentialRenewals::<T>::get(renewal_id) {
353						Some(PotentialRenewalRecord { completion: Partial(w), price: p })
354							if price == p =>
355							w,
356						_ => CoreMask::void(),
357					} | region_id.mask;
358					let workload =
359						if assigned.is_complete() { Complete(workplan) } else { Partial(assigned) };
360					let record = PotentialRenewalRecord { price, completion: workload };
361					// Note: This entry alone does not yet actually allow renewals (the completion
362					// status has to be complete for `do_renew` to accept it).
363					PotentialRenewals::<T>::insert(&renewal_id, &record);
364					if let Some(workload) = record.completion.drain_complete() {
365						Self::deposit_event(Event::Renewable {
366							core: region_id.core,
367							price,
368							begin: region.end,
369							workload,
370						});
371					}
372				}
373			}
374			Self::deposit_event(Event::Assigned { region_id, task: target, duration });
375		}
376		Ok(())
377	}
378
379	pub(crate) fn do_remove_assignment(region_id: RegionId) -> DispatchResult {
380		let workplan_key = (region_id.begin, region_id.core);
381		ensure!(Workplan::<T>::contains_key(&workplan_key), Error::<T>::AssignmentNotFound);
382		Workplan::<T>::remove(&workplan_key);
383		Self::deposit_event(Event::<T>::AssignmentRemoved { region_id });
384		Ok(())
385	}
386
387	pub(crate) fn do_pool(
388		region_id: RegionId,
389		maybe_check_owner: Option<T::AccountId>,
390		payee: T::AccountId,
391		finality: Finality,
392	) -> Result<(), Error<T>> {
393		if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? {
394			let workplan_key = (region_id.begin, region_id.core);
395			let mut workplan = Workplan::<T>::get(&workplan_key).unwrap_or_default();
396			let duration = region.end.saturating_sub(region_id.begin);
397			if workplan
398				.try_push(ScheduleItem { mask: region_id.mask, assignment: CoreAssignment::Pool })
399				.is_ok()
400			{
401				Workplan::<T>::insert(&workplan_key, &workplan);
402				let size = region_id.mask.count_ones() as i32;
403				InstaPoolIo::<T>::mutate(region_id.begin, |a| a.private.saturating_accrue(size));
404				InstaPoolIo::<T>::mutate(region.end, |a| a.private.saturating_reduce(size));
405				let record = ContributionRecord { length: duration, payee };
406				InstaPoolContribution::<T>::insert(&region_id, record);
407			}
408
409			Self::deposit_event(Event::Pooled { region_id, duration });
410		}
411		Ok(())
412	}
413
414	pub(crate) fn do_claim_revenue(
415		mut region: RegionId,
416		max_timeslices: Timeslice,
417	) -> DispatchResult {
418		ensure!(max_timeslices > 0, Error::<T>::NoClaimTimeslices);
419		let mut contribution =
420			InstaPoolContribution::<T>::take(region).ok_or(Error::<T>::UnknownContribution)?;
421		let contributed_parts = region.mask.count_ones();
422
423		Self::deposit_event(Event::RevenueClaimBegun { region, max_timeslices });
424
425		let mut payout = BalanceOf::<T>::zero();
426		let last = region.begin + contribution.length.min(max_timeslices);
427		for r in region.begin..last {
428			region.begin = r + 1;
429			contribution.length.saturating_dec();
430
431			let Some(mut pool_record) = InstaPoolHistory::<T>::get(r) else { continue };
432			let Some(total_payout) = pool_record.maybe_payout else { break };
433			let p = total_payout
434				.saturating_mul(contributed_parts.into())
435				.checked_div(&pool_record.private_contributions.into())
436				.unwrap_or_default();
437
438			payout.saturating_accrue(p);
439			pool_record.private_contributions.saturating_reduce(contributed_parts);
440
441			let remaining_payout = total_payout.saturating_sub(p);
442			if !remaining_payout.is_zero() && pool_record.private_contributions > 0 {
443				pool_record.maybe_payout = Some(remaining_payout);
444				InstaPoolHistory::<T>::insert(r, &pool_record);
445			} else {
446				InstaPoolHistory::<T>::remove(r);
447			}
448			if !p.is_zero() {
449				Self::deposit_event(Event::RevenueClaimItem { when: r, amount: p });
450			}
451		}
452
453		if contribution.length > 0 {
454			InstaPoolContribution::<T>::insert(region, &contribution);
455		}
456		T::Currency::transfer(&Self::account_id(), &contribution.payee, payout, Expendable)
457			.defensive_ok();
458		let next = if last < region.begin + contribution.length { Some(region) } else { None };
459		Self::deposit_event(Event::RevenueClaimPaid {
460			who: contribution.payee,
461			amount: payout,
462			next,
463		});
464		Ok(())
465	}
466
467	pub(crate) fn do_purchase_credit(
468		who: T::AccountId,
469		amount: BalanceOf<T>,
470		beneficiary: RelayAccountIdOf<T>,
471	) -> DispatchResult {
472		ensure!(amount >= T::MinimumCreditPurchase::get(), Error::<T>::CreditPurchaseTooSmall);
473		T::Currency::transfer(&who, &Self::account_id(), amount, Expendable)?;
474		let rc_amount = T::ConvertBalance::convert(amount);
475		T::Coretime::credit_account(beneficiary.clone(), rc_amount);
476		Self::deposit_event(Event::<T>::CreditPurchased { who, beneficiary, amount });
477		Ok(())
478	}
479
480	pub(crate) fn do_drop_region(region_id: RegionId) -> DispatchResult {
481		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
482		let region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
483		ensure!(status.last_committed_timeslice >= region.end, Error::<T>::StillValid);
484
485		Regions::<T>::remove(&region_id);
486		let duration = region.end.saturating_sub(region_id.begin);
487		Self::deposit_event(Event::RegionDropped { region_id, duration });
488		Ok(())
489	}
490
491	pub(crate) fn do_drop_contribution(region_id: RegionId) -> DispatchResult {
492		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
493		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
494		let contrib =
495			InstaPoolContribution::<T>::get(&region_id).ok_or(Error::<T>::UnknownContribution)?;
496		let end = region_id.begin.saturating_add(contrib.length);
497		ensure!(
498			status.last_timeslice >= end.saturating_add(config.contribution_timeout),
499			Error::<T>::StillValid
500		);
501		InstaPoolContribution::<T>::remove(region_id);
502		Self::deposit_event(Event::ContributionDropped { region_id });
503		Ok(())
504	}
505
506	pub(crate) fn do_drop_history(when: Timeslice) -> DispatchResult {
507		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
508		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
509		ensure!(
510			status.last_timeslice > when.saturating_add(config.contribution_timeout),
511			Error::<T>::StillValid
512		);
513		let record = InstaPoolHistory::<T>::take(when).ok_or(Error::<T>::NoHistory)?;
514		if let Some(payout) = record.maybe_payout {
515			let _ = Self::charge(&Self::account_id(), payout);
516		}
517		let revenue = record.maybe_payout.unwrap_or_default();
518		Self::deposit_event(Event::HistoryDropped { when, revenue });
519		Ok(())
520	}
521
522	pub(crate) fn do_drop_renewal(core: CoreIndex, when: Timeslice) -> DispatchResult {
523		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
524		ensure!(status.last_committed_timeslice >= when, Error::<T>::StillValid);
525		let id = PotentialRenewalId { core, when };
526		ensure!(PotentialRenewals::<T>::contains_key(id), Error::<T>::UnknownRenewal);
527		PotentialRenewals::<T>::remove(id);
528		Self::deposit_event(Event::PotentialRenewalDropped { core, when });
529		Ok(())
530	}
531
532	pub(crate) fn do_notify_revenue(revenue: OnDemandRevenueRecordOf<T>) -> DispatchResult {
533		RevenueInbox::<T>::put(revenue);
534		Ok(())
535	}
536
537	pub(crate) fn do_swap_leases(id: TaskId, other: TaskId) -> DispatchResult {
538		let mut id_leases_count = 0;
539		let mut other_leases_count = 0;
540		Leases::<T>::mutate(|leases| {
541			leases.iter_mut().for_each(|lease| {
542				if lease.task == id {
543					lease.task = other;
544					id_leases_count += 1;
545				} else if lease.task == other {
546					lease.task = id;
547					other_leases_count += 1;
548				}
549			})
550		});
551		Ok(())
552	}
553
554	pub(crate) fn do_enable_auto_renew(
555		sovereign_account: T::AccountId,
556		core: CoreIndex,
557		task: TaskId,
558		workload_end_hint: Option<Timeslice>,
559	) -> DispatchResult {
560		let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
561
562		// Check if the core is expiring in the next bulk period; if so, we will renew it now.
563		//
564		// In case we renew it now, we don't need to check the workload end since we know it is
565		// eligible for renewal.
566		if PotentialRenewals::<T>::get(PotentialRenewalId { core, when: sale.region_begin })
567			.is_some()
568		{
569			Self::do_renew(sovereign_account.clone(), core)?;
570		} else if let Some(workload_end) = workload_end_hint {
571			ensure!(
572				PotentialRenewals::<T>::get(PotentialRenewalId { core, when: workload_end })
573					.is_some(),
574				Error::<T>::NotAllowed
575			);
576		} else {
577			return Err(Error::<T>::NotAllowed.into())
578		}
579
580		// We are sorting auto renewals by `CoreIndex`.
581		AutoRenewals::<T>::try_mutate(|renewals| {
582			let pos = renewals
583				.binary_search_by(|r: &AutoRenewalRecord| r.core.cmp(&core))
584				.unwrap_or_else(|e| e);
585			renewals.try_insert(
586				pos,
587				AutoRenewalRecord {
588					core,
589					task,
590					next_renewal: workload_end_hint.unwrap_or(sale.region_end),
591				},
592			)
593		})
594		.map_err(|_| Error::<T>::TooManyAutoRenewals)?;
595
596		Self::deposit_event(Event::AutoRenewalEnabled { core, task });
597		Ok(())
598	}
599
600	pub(crate) fn do_disable_auto_renew(core: CoreIndex, task: TaskId) -> DispatchResult {
601		AutoRenewals::<T>::try_mutate(|renewals| -> DispatchResult {
602			let pos = renewals
603				.binary_search_by(|r: &AutoRenewalRecord| r.core.cmp(&core))
604				.map_err(|_| Error::<T>::AutoRenewalNotEnabled)?;
605
606			let renewal_record = renewals.get(pos).ok_or(Error::<T>::AutoRenewalNotEnabled)?;
607
608			ensure!(
609				renewal_record.core == core && renewal_record.task == task,
610				Error::<T>::NoPermission
611			);
612			renewals.remove(pos);
613			Ok(())
614		})?;
615
616		Self::deposit_event(Event::AutoRenewalDisabled { core, task });
617		Ok(())
618	}
619
620	pub(crate) fn ensure_cores_for_sale(
621		status: &StatusRecord,
622		sale: &SaleInfoRecordOf<T>,
623	) -> Result<(), DispatchError> {
624		ensure!(sale.first_core < status.core_count, Error::<T>::Unavailable);
625		ensure!(sale.cores_sold < sale.cores_offered, Error::<T>::SoldOut);
626
627		Ok(())
628	}
629
630	/// If there is an ongoing sale returns the current price of a core.
631	pub fn current_price() -> Result<BalanceOf<T>, DispatchError> {
632		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
633		let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
634
635		Self::ensure_cores_for_sale(&status, &sale)?;
636
637		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
638		Ok(Self::sale_price(&sale, now))
639	}
640}