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},
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 ForceReservations for dynamic core assignment in rotate_sale.
73		ForceReservations::<T>::try_mutate(|r| {
74			r.try_push(workload.clone()).map_err(|_| Error::<T>::TooManyReservations)
75		})?;
76
77		// Assign now until the next sale boundary unless the next timeslice is already the sale
78		// boundary.
79		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
80		let timeslice = status.last_committed_timeslice.saturating_add(1);
81		if timeslice < sale.region_begin {
82			Workplan::<T>::insert((timeslice, core), &workload);
83		}
84
85		Ok(())
86	}
87
88	pub(crate) fn do_set_lease(task: TaskId, until: Timeslice) -> DispatchResult {
89		let mut r = Leases::<T>::get();
90		ensure!(until > Self::current_timeslice(), Error::<T>::AlreadyExpired);
91		r.try_push(LeaseRecordItem { until, task })
92			.map_err(|_| Error::<T>::TooManyLeases)?;
93		Leases::<T>::put(r);
94		Self::deposit_event(Event::<T>::Leased { until, task });
95		Ok(())
96	}
97
98	pub(crate) fn do_remove_lease(task: TaskId) -> DispatchResult {
99		let mut r = Leases::<T>::get();
100		let i = r.iter().position(|lease| lease.task == task).ok_or(Error::<T>::LeaseNotFound)?;
101		r.remove(i);
102		Leases::<T>::put(r);
103		Self::deposit_event(Event::<T>::LeaseRemoved { task });
104		Ok(())
105	}
106
107	pub(crate) fn do_start_sales(
108		end_price: BalanceOf<T>,
109		extra_cores: CoreIndex,
110	) -> DispatchResult {
111		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
112
113		// Determine the core count
114		let core_count = Leases::<T>::decode_len().unwrap_or(0) as CoreIndex +
115			Reservations::<T>::decode_len().unwrap_or(0) as CoreIndex +
116			extra_cores;
117
118		Self::do_request_core_count(core_count)?;
119
120		let commit_timeslice = Self::latest_timeslice_ready_to_commit(&config);
121		let status = StatusRecord {
122			core_count,
123			private_pool_size: 0,
124			system_pool_size: 0,
125			last_committed_timeslice: commit_timeslice.saturating_sub(1),
126			last_timeslice: Self::current_timeslice(),
127		};
128		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
129		// Imaginary old sale for bootstrapping the first actual sale:
130		let old_sale = SaleInfoRecord {
131			sale_start: now,
132			leadin_length: Zero::zero(),
133			end_price,
134			sellout_price: None,
135			region_begin: commit_timeslice,
136			region_end: commit_timeslice.saturating_add(config.region_length),
137			first_core: 0,
138			ideal_cores_sold: 0,
139			cores_offered: 0,
140			cores_sold: 0,
141			sale_index: 0,
142		};
143		Self::deposit_event(Event::<T>::SalesStarted { price: end_price, core_count });
144		Self::rotate_sale(old_sale, &config, &status);
145		Status::<T>::put(&status);
146		Ok(())
147	}
148
149	pub(crate) fn do_purchase(
150		who: T::AccountId,
151		price_limit: BalanceOf<T>,
152	) -> Result<RegionId, DispatchError> {
153		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
154		let mut sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
155		Self::ensure_cores_for_sale(&status, &sale)?;
156
157		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
158		ensure!(now > sale.sale_start, Error::<T>::TooEarly);
159		let price = Self::sale_price(&sale, now);
160		ensure!(price_limit >= price, Error::<T>::Overpriced);
161
162		let core = Self::purchase_core(&who, price, &mut sale)?;
163
164		SaleInfo::<T>::put(&sale);
165		let id = Self::issue(
166			core,
167			sale.region_begin,
168			CoreMask::complete(),
169			sale.region_end,
170			Some(who.clone()),
171			Some(price),
172		);
173		let duration = sale.region_end.saturating_sub(sale.region_begin);
174		Self::deposit_event(Event::Purchased { who, region_id: id, price, duration });
175		Ok(id)
176	}
177
178	/// Must be called on a core in `PotentialRenewals` whose value is a timeslice equal to the
179	/// current sale status's `region_end`.
180	pub(crate) fn do_renew(who: T::AccountId, core: CoreIndex) -> Result<CoreIndex, DispatchError> {
181		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
182		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
183		let mut sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
184		Self::ensure_cores_for_sale(&status, &sale)?;
185
186		let renewal_id = PotentialRenewalId { core, when: sale.region_begin };
187		let record = PotentialRenewals::<T>::get(renewal_id).ok_or(Error::<T>::NotAllowed)?;
188		let workload =
189			record.completion.drain_complete().ok_or(Error::<T>::IncompleteAssignment)?;
190
191		let old_core = core;
192
193		let core = Self::purchase_core(&who, record.price, &mut sale)?;
194
195		Self::deposit_event(Event::Renewed {
196			who,
197			old_core,
198			core,
199			price: record.price,
200			begin: sale.region_begin,
201			duration: sale.region_end.saturating_sub(sale.region_begin),
202			workload: workload.clone(),
203		});
204
205		Workplan::<T>::insert((sale.region_begin, core), &workload);
206
207		let begin = sale.region_end;
208		let end_price = sale.end_price;
209		// Renewals should never be priced lower than the current `end_price`:
210		let price_cap = cmp::max(record.price + config.renewal_bump * record.price, end_price);
211		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
212		let price = Self::sale_price(&sale, now).min(price_cap);
213		log::debug!(
214			"Renew with: sale price: {:?}, price cap: {:?}, old price: {:?}",
215			price,
216			price_cap,
217			record.price
218		);
219		let new_record = PotentialRenewalRecord { price, completion: Complete(workload) };
220		PotentialRenewals::<T>::remove(renewal_id);
221		PotentialRenewals::<T>::insert(PotentialRenewalId { core, when: begin }, &new_record);
222		SaleInfo::<T>::put(&sale);
223		if let Some(workload) = new_record.completion.drain_complete() {
224			log::debug!("Recording renewable price for next run: {:?}", price);
225			Self::deposit_event(Event::Renewable { core, price, begin, workload });
226		}
227		Ok(core)
228	}
229
230	pub(crate) fn do_transfer(
231		region_id: RegionId,
232		maybe_check_owner: Option<T::AccountId>,
233		new_owner: T::AccountId,
234	) -> Result<(), Error<T>> {
235		let mut region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
236
237		if let Some(check_owner) = maybe_check_owner {
238			ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
239		}
240
241		let old_owner = region.owner;
242		region.owner = Some(new_owner);
243		Regions::<T>::insert(&region_id, &region);
244		let duration = region.end.saturating_sub(region_id.begin);
245		Self::deposit_event(Event::Transferred {
246			region_id,
247			old_owner,
248			owner: region.owner,
249			duration,
250		});
251
252		Ok(())
253	}
254
255	pub(crate) fn do_partition(
256		region_id: RegionId,
257		maybe_check_owner: Option<T::AccountId>,
258		pivot_offset: Timeslice,
259	) -> Result<(RegionId, RegionId), Error<T>> {
260		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
261		let mut region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
262
263		if let Some(check_owner) = maybe_check_owner {
264			ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
265		}
266		let pivot = region_id.begin.saturating_add(pivot_offset);
267		ensure!(pivot < region.end, Error::<T>::PivotTooLate);
268		ensure!(pivot > region_id.begin, Error::<T>::PivotTooEarly);
269
270		region.paid = None;
271		let new_region_ids = (region_id, RegionId { begin: pivot, ..region_id });
272
273		// Remove this region from the pool in case it has been assigned provisionally. If we get
274		// this far then it is still in `Regions` and thus could only have been pooled
275		// provisionally.
276		Self::force_unpool_region(region_id, &region, &status);
277
278		// Overwrite the previous region with its new end and create a new region for the second
279		// part of the partition.
280		Regions::<T>::insert(&new_region_ids.0, &RegionRecord { end: pivot, ..region.clone() });
281		Regions::<T>::insert(&new_region_ids.1, &region);
282		Self::deposit_event(Event::Partitioned { old_region_id: region_id, new_region_ids });
283
284		Ok(new_region_ids)
285	}
286
287	pub(crate) fn do_interlace(
288		region_id: RegionId,
289		maybe_check_owner: Option<T::AccountId>,
290		pivot: CoreMask,
291	) -> Result<(RegionId, RegionId), Error<T>> {
292		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
293		let region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
294
295		if let Some(check_owner) = maybe_check_owner {
296			ensure!(Some(check_owner) == region.owner, Error::<T>::NotOwner);
297		}
298
299		ensure!((pivot & !region_id.mask).is_void(), Error::<T>::ExteriorPivot);
300		ensure!(!pivot.is_void(), Error::<T>::VoidPivot);
301		ensure!(pivot != region_id.mask, Error::<T>::CompletePivot);
302
303		// Remove this region from the pool in case it has been assigned provisionally. If we get
304		// this far then it is still in `Regions` and thus could only have been pooled
305		// provisionally.
306		Self::force_unpool_region(region_id, &region, &status);
307
308		// The old region should be removed.
309		Regions::<T>::remove(&region_id);
310
311		let one = RegionId { mask: pivot, ..region_id };
312		Regions::<T>::insert(&one, &region);
313		let other = RegionId { mask: region_id.mask ^ pivot, ..region_id };
314		Regions::<T>::insert(&other, &region);
315
316		let new_region_ids = (one, other);
317		Self::deposit_event(Event::Interlaced { old_region_id: region_id, new_region_ids });
318		Ok(new_region_ids)
319	}
320
321	pub(crate) fn do_assign(
322		region_id: RegionId,
323		maybe_check_owner: Option<T::AccountId>,
324		target: TaskId,
325		finality: Finality,
326	) -> Result<(), Error<T>> {
327		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
328		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
329
330		if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? {
331			let workplan_key = (region_id.begin, region_id.core);
332			let mut workplan = Workplan::<T>::get(&workplan_key).unwrap_or_default();
333
334			// Remove this region from the pool in case it has been assigned provisionally. If we
335			// get this far then it is still in `Regions` and thus could only have been pooled
336			// provisionally.
337			Self::force_unpool_region(region_id, &region, &status);
338
339			// Ensure no previous allocations exist.
340			workplan.retain(|i| (i.mask & region_id.mask).is_void());
341			if workplan
342				.try_push(ScheduleItem {
343					mask: region_id.mask,
344					assignment: CoreAssignment::Task(target),
345				})
346				.is_ok()
347			{
348				Workplan::<T>::insert(&workplan_key, &workplan);
349			}
350
351			let duration = region.end.saturating_sub(region_id.begin);
352			if duration == config.region_length && finality == Finality::Final {
353				if let Some(price) = region.paid {
354					let renewal_id = PotentialRenewalId { core: region_id.core, when: region.end };
355					let assigned = match PotentialRenewals::<T>::get(renewal_id) {
356						Some(PotentialRenewalRecord { completion: Partial(w), price: p })
357							if price == p =>
358						{
359							w
360						},
361						_ => CoreMask::void(),
362					} | region_id.mask;
363					let workload =
364						if assigned.is_complete() { Complete(workplan) } else { Partial(assigned) };
365					let record = PotentialRenewalRecord { price, completion: workload };
366					// Note: This entry alone does not yet actually allow renewals (the completion
367					// status has to be complete for `do_renew` to accept it).
368					PotentialRenewals::<T>::insert(&renewal_id, &record);
369					if let Some(workload) = record.completion.drain_complete() {
370						Self::deposit_event(Event::Renewable {
371							core: region_id.core,
372							price,
373							begin: region.end,
374							workload,
375						});
376					}
377				}
378			}
379			Self::deposit_event(Event::Assigned { region_id, task: target, duration });
380		}
381		Ok(())
382	}
383
384	pub(crate) fn do_remove_assignment(region_id: RegionId) -> DispatchResult {
385		let workplan_key = (region_id.begin, region_id.core);
386		ensure!(Workplan::<T>::contains_key(&workplan_key), Error::<T>::AssignmentNotFound);
387		Workplan::<T>::remove(&workplan_key);
388		Self::deposit_event(Event::<T>::AssignmentRemoved { region_id });
389		Ok(())
390	}
391
392	pub(crate) fn do_pool(
393		region_id: RegionId,
394		maybe_check_owner: Option<T::AccountId>,
395		payee: T::AccountId,
396		finality: Finality,
397	) -> Result<(), Error<T>> {
398		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
399
400		if let Some((region_id, region)) = Self::utilize(region_id, maybe_check_owner, finality)? {
401			let workplan_key = (region_id.begin, region_id.core);
402			let mut workplan = Workplan::<T>::get(&workplan_key).unwrap_or_default();
403
404			// Remove this region from the pool in case it has been pooled provisionally, so that
405			// pooling it again does not count it twice.
406			Self::force_unpool_region(region_id, &region, &status);
407
408			let duration = region.end.saturating_sub(region_id.begin);
409			if workplan
410				.try_push(ScheduleItem { mask: region_id.mask, assignment: CoreAssignment::Pool })
411				.is_ok()
412			{
413				Workplan::<T>::insert(&workplan_key, &workplan);
414				let size = region_id.mask.count_ones() as i32;
415				InstaPoolIo::<T>::mutate(region_id.begin, |a| a.private.saturating_accrue(size));
416				InstaPoolIo::<T>::mutate(region.end, |a| a.private.saturating_reduce(size));
417				let record = ContributionRecord { length: duration, payee };
418				InstaPoolContribution::<T>::insert(&region_id, record);
419			}
420
421			Self::deposit_event(Event::Pooled { region_id, duration });
422		}
423		Ok(())
424	}
425
426	pub(crate) fn do_claim_revenue(
427		mut region: RegionId,
428		max_timeslices: Timeslice,
429	) -> DispatchResult {
430		ensure!(max_timeslices > 0, Error::<T>::NoClaimTimeslices);
431		let mut contribution =
432			InstaPoolContribution::<T>::take(region).ok_or(Error::<T>::UnknownContribution)?;
433		let contributed_parts = region.mask.count_ones();
434
435		Self::deposit_event(Event::RevenueClaimBegun { region, max_timeslices });
436
437		let mut payout = BalanceOf::<T>::zero();
438		let last = region.begin + contribution.length.min(max_timeslices);
439		for r in region.begin..last {
440			region.begin = r + 1;
441			contribution.length.saturating_dec();
442
443			let Some(mut pool_record) = InstaPoolHistory::<T>::get(r) else { continue };
444			let Some(total_payout) = pool_record.maybe_payout else { break };
445			let p = total_payout
446				.saturating_mul(contributed_parts.into())
447				.checked_div(&pool_record.private_contributions.into())
448				.unwrap_or_default();
449
450			payout.saturating_accrue(p);
451			pool_record.private_contributions.saturating_reduce(contributed_parts);
452
453			let remaining_payout = total_payout.saturating_sub(p);
454			if !remaining_payout.is_zero() && pool_record.private_contributions > 0 {
455				pool_record.maybe_payout = Some(remaining_payout);
456				InstaPoolHistory::<T>::insert(r, &pool_record);
457			} else {
458				InstaPoolHistory::<T>::remove(r);
459			}
460			if !p.is_zero() {
461				Self::deposit_event(Event::RevenueClaimItem { when: r, amount: p });
462			}
463		}
464
465		if contribution.length > 0 {
466			InstaPoolContribution::<T>::insert(region, &contribution);
467		}
468		// The steps above removed the contribution and reduced the stored payouts. If the
469		// transfer fails, the error must revert them. The storage changes and the payment
470		// must both happen, or neither.
471		T::Currency::transfer(&Self::account_id(), &contribution.payee, payout, Expendable)?;
472		let next = if last < region.begin + contribution.length { Some(region) } else { None };
473		Self::deposit_event(Event::RevenueClaimPaid {
474			who: contribution.payee,
475			amount: payout,
476			next,
477		});
478		Ok(())
479	}
480
481	pub(crate) fn do_purchase_credit(
482		who: T::AccountId,
483		amount: BalanceOf<T>,
484		beneficiary: RelayAccountIdOf<T>,
485	) -> DispatchResult {
486		ensure!(amount >= T::MinimumCreditPurchase::get(), Error::<T>::CreditPurchaseTooSmall);
487		T::Currency::transfer(&who, &Self::account_id(), amount, Expendable)?;
488		let rc_amount = T::ConvertBalance::convert(amount);
489		T::Coretime::credit_account(beneficiary.clone(), rc_amount);
490		Self::deposit_event(Event::<T>::CreditPurchased { who, beneficiary, amount });
491		Ok(())
492	}
493
494	pub(crate) fn do_drop_region(region_id: RegionId) -> DispatchResult {
495		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
496		let region = Regions::<T>::get(&region_id).ok_or(Error::<T>::UnknownRegion)?;
497		ensure!(status.last_committed_timeslice >= region.end, Error::<T>::StillValid);
498
499		Regions::<T>::remove(&region_id);
500		let duration = region.end.saturating_sub(region_id.begin);
501		Self::deposit_event(Event::RegionDropped { region_id, duration });
502		Ok(())
503	}
504
505	pub(crate) fn do_drop_contribution(region_id: RegionId) -> DispatchResult {
506		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
507		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
508		let contrib =
509			InstaPoolContribution::<T>::get(&region_id).ok_or(Error::<T>::UnknownContribution)?;
510		let end = region_id.begin.saturating_add(contrib.length);
511		ensure!(
512			status.last_timeslice >= end.saturating_add(config.contribution_timeout),
513			Error::<T>::StillValid
514		);
515		InstaPoolContribution::<T>::remove(region_id);
516		Self::deposit_event(Event::ContributionDropped { region_id });
517		Ok(())
518	}
519
520	pub(crate) fn do_drop_history(when: Timeslice) -> DispatchResult {
521		let config = Configuration::<T>::get().ok_or(Error::<T>::Uninitialized)?;
522		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
523		ensure!(
524			status.last_timeslice > when.saturating_add(config.contribution_timeout),
525			Error::<T>::StillValid
526		);
527		let record = InstaPoolHistory::<T>::take(when).ok_or(Error::<T>::NoHistory)?;
528		if let Some(payout) = record.maybe_payout {
529			let _ = Self::charge(&Self::account_id(), payout);
530		}
531		let revenue = record.maybe_payout.unwrap_or_default();
532		Self::deposit_event(Event::HistoryDropped { when, revenue });
533		Ok(())
534	}
535
536	pub(crate) fn do_drop_renewal(core: CoreIndex, when: Timeslice) -> DispatchResult {
537		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
538		ensure!(status.last_committed_timeslice >= when, Error::<T>::StillValid);
539		let id = PotentialRenewalId { core, when };
540		ensure!(PotentialRenewals::<T>::contains_key(id), Error::<T>::UnknownRenewal);
541		PotentialRenewals::<T>::remove(id);
542		Self::deposit_event(Event::PotentialRenewalDropped { core, when });
543		Ok(())
544	}
545
546	pub(crate) fn do_notify_revenue(revenue: OnDemandRevenueRecordOf<T>) -> DispatchResult {
547		RevenueInbox::<T>::put(revenue);
548		Ok(())
549	}
550
551	pub(crate) fn do_swap_leases(id: TaskId, other: TaskId) -> DispatchResult {
552		let mut id_leases_count = 0;
553		let mut other_leases_count = 0;
554		Leases::<T>::mutate(|leases| {
555			leases.iter_mut().for_each(|lease| {
556				if lease.task == id {
557					lease.task = other;
558					id_leases_count += 1;
559				} else if lease.task == other {
560					lease.task = id;
561					other_leases_count += 1;
562				}
563			})
564		});
565		Ok(())
566	}
567
568	pub(crate) fn do_enable_auto_renew(
569		sovereign_account: T::AccountId,
570		core: CoreIndex,
571		task: TaskId,
572		workload_end_hint: Option<Timeslice>,
573	) -> DispatchResult {
574		let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
575		let mut core = core;
576
577		// Check if the core is expiring in the next bulk period with the task's own workload;
578		// if so, we will renew it now.
579		//
580		// A core index can be assigned a different workload each bulk period, so the core could
581		// instead be expiring with another task's workload, which `task` must not pay to renew.
582		// In that case the `workload_end_hint` can still point to the task's own renewal record.
583		let renewable_now =
584			PotentialRenewals::<T>::get(PotentialRenewalId { core, when: sale.region_begin })
585				.map_or(false, |record| record.completion.is_complete_and_contains_task(task));
586
587		let next_renewal = if renewable_now {
588			core = Self::do_renew(sovereign_account.clone(), core)?;
589			// The next renewal is due when the period we just renewed for ends.
590			sale.region_end
591		} else if let Some(workload_end) = workload_end_hint {
592			let record =
593				PotentialRenewals::<T>::get(PotentialRenewalId { core, when: workload_end })
594					.ok_or(Error::<T>::NotAllowed)?;
595			let workload = record.completion.complete().ok_or(Error::<T>::IncompleteAssignment)?;
596			ensure!(
597				workload.iter().any(|item| item.assignment == CoreAssignment::Task(task)),
598				Error::<T>::TaskNotInWorkload
599			);
600			workload_end
601		} else {
602			return Err(Error::<T>::TaskNotInWorkload.into());
603		};
604
605		// We are sorting auto renewals by `CoreIndex`.
606		AutoRenewals::<T>::try_mutate(|renewals| {
607			let pos = renewals
608				.binary_search_by(|r: &AutoRenewalRecord| r.core.cmp(&core))
609				.unwrap_or_else(|e| e);
610			renewals.try_insert(pos, AutoRenewalRecord { core, task, next_renewal })
611		})
612		.map_err(|_| Error::<T>::TooManyAutoRenewals)?;
613
614		Self::deposit_event(Event::AutoRenewalEnabled { core, task });
615		Ok(())
616	}
617
618	pub(crate) fn do_disable_auto_renew(core: CoreIndex, task: TaskId) -> DispatchResult {
619		AutoRenewals::<T>::try_mutate(|renewals| -> DispatchResult {
620			let pos = renewals
621				.binary_search_by(|r: &AutoRenewalRecord| r.core.cmp(&core))
622				.map_err(|_| Error::<T>::AutoRenewalNotEnabled)?;
623
624			let renewal_record = renewals.get(pos).ok_or(Error::<T>::AutoRenewalNotEnabled)?;
625
626			ensure!(
627				renewal_record.core == core && renewal_record.task == task,
628				Error::<T>::NoPermission
629			);
630			renewals.remove(pos);
631			Ok(())
632		})?;
633
634		Self::deposit_event(Event::AutoRenewalDisabled { core, task });
635		Ok(())
636	}
637
638	pub(crate) fn do_remove_potential_renewal(core: CoreIndex, when: Timeslice) -> DispatchResult {
639		let renewal_id = PotentialRenewalId { core, when };
640
641		PotentialRenewals::<T>::take(renewal_id).ok_or(Error::<T>::UnknownRenewal)?;
642
643		Self::deposit_event(Event::PotentialRenewalRemoved { core, timeslice: when });
644
645		Ok(())
646	}
647
648	pub(crate) fn ensure_cores_for_sale(
649		status: &StatusRecord,
650		sale: &SaleInfoRecordOf<T>,
651	) -> Result<(), DispatchError> {
652		ensure!(sale.first_core < status.core_count, Error::<T>::Unavailable);
653		ensure!(sale.cores_sold < sale.cores_offered, Error::<T>::SoldOut);
654
655		Ok(())
656	}
657
658	/// If there is an ongoing sale returns the current price of a core.
659	pub fn current_price() -> Result<BalanceOf<T>, DispatchError> {
660		let status = Status::<T>::get().ok_or(Error::<T>::Uninitialized)?;
661		let sale = SaleInfo::<T>::get().ok_or(Error::<T>::NoSales)?;
662
663		Self::ensure_cores_for_sale(&status, &sale)?;
664
665		let now = RCBlockNumberProviderOf::<T::Coretime>::current_block_number();
666		Ok(Self::sale_price(&sale, now))
667	}
668}