1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
//! # Configuration Pallet
18
//!
19
//! This pallet stores the configuration for an orchestration-collator assignation chain. In
20
//! particular stores:
21
//!
22
//!    - How many collators are taken.
23
//!    - How many of those collators should be serving the orchestrator chain
24
//!    - Howe many of those collators should be serving the containerChains
25
//!
26
//! All configuration changes are protected behind the root origin
27
//! CHanges to the configuration are not immeditaly applied, but rather we wait
28
//! T::SessionDelay to apply these changes
29

            
30
#![cfg_attr(not(feature = "std"), no_std)]
31

            
32
#[cfg(test)]
33
mod mock;
34

            
35
#[cfg(test)]
36
mod tests;
37
pub mod weights;
38

            
39
pub use weights::WeightInfo;
40

            
41
#[cfg(any(test, feature = "runtime-benchmarks"))]
42
mod benchmarks;
43

            
44
pub use pallet::*;
45
use {
46
    frame_support::pallet_prelude::*,
47
    frame_system::pallet_prelude::*,
48
    serde::{Deserialize, Serialize},
49
    sp_runtime::{traits::AtLeast32BitUnsigned, Perbill, Saturating},
50
    sp_std::prelude::*,
51
    tp_traits::GetSessionIndex,
52
};
53

            
54
const LOG_TARGET: &str = "pallet_configuration";
55

            
56
/// All configuration of the runtime with respect to parachains and parathreads.
57
#[derive(
58
    Clone,
59
    Encode,
60
    Decode,
61
    PartialEq,
62
    sp_core::RuntimeDebug,
63
11349
    scale_info::TypeInfo,
64
    Serialize,
65
    Deserialize,
66
    MaxEncodedLen,
67
)]
68
pub struct HostConfiguration {
69
    /// Maximum number of collators, in total, including orchestrator and containers
70
    pub max_collators: u32,
71
    /// Minimum number of collators to be assigned to orchestrator chain
72
    pub min_orchestrator_collators: u32,
73
    /// Maximum number of collators to be assigned to orchestrator chain after all the container chains have been
74
    /// assigned collators.
75
    pub max_orchestrator_collators: u32,
76
    /// How many collators to assign to one container chain
77
    pub collators_per_container: u32,
78
    /// Rotate all collators once every n sessions. If this value is 0 means that there is no rotation
79
    pub full_rotation_period: u32,
80
    /// How many collators to assign to one parathread
81
    // TODO: for now we only support 1 collator per parathread because using Aura for consensus conflicts with
82
    // the idea of being able to create blocks every n slots: if there are 2 collators and we create blocks
83
    // every 2 slots, 1 collator will create all the blocks.
84
    pub collators_per_parathread: u32,
85
    /// How many parathreads can be assigned to one collator
86
    pub parathreads_per_collator: u32,
87
    /// Ratio of collators that we expect to be assigned to container chains. Affects fees.
88
    pub target_container_chain_fullness: Perbill,
89
    ///  Maximum number of cores that can be allocated to parachains (only applicable for solo chain)
90
    pub max_parachain_cores_percentage: Option<Perbill>,
91
}
92

            
93
impl Default for HostConfiguration {
94
11702
    fn default() -> Self {
95
11702
        Self {
96
11702
            max_collators: 100u32,
97
11702
            min_orchestrator_collators: 2u32,
98
11702
            max_orchestrator_collators: 5u32,
99
11702
            collators_per_container: 2u32,
100
11702
            full_rotation_period: 24u32,
101
11702
            collators_per_parathread: 1,
102
11702
            parathreads_per_collator: 1,
103
11702
            target_container_chain_fullness: Perbill::from_percent(80),
104
11702
            max_parachain_cores_percentage: None,
105
11702
        }
106
11702
    }
107
}
108

            
109
/// Enumerates the possible inconsistencies of `HostConfiguration`.
110
#[derive(Debug)]
111
pub enum InconsistentError {
112
    /// `max_orchestrator_collators` is lower than `min_orchestrator_collators`
113
    MaxCollatorsLowerThanMinCollators,
114
    /// `min_orchestrator_collators` must be at least 1
115
    MinOrchestratorCollatorsTooLow,
116
    /// `max_collators` must be at least 1
117
    MaxCollatorsTooLow,
118
    /// Tried to modify an unimplemented parameter
119
    UnimplementedParameter,
120
}
121

            
122
impl HostConfiguration {
123
    /// Checks that this instance is consistent with the requirements on each individual member.
124
    ///
125
    /// # Errors
126
    ///
127
    /// This function returns an error if the configuration is inconsistent.
128
5476
    pub fn check_consistency(
129
5476
        &self,
130
5476
        allow_empty_orchestrator: bool,
131
5476
    ) -> Result<(), InconsistentError> {
132
5476
        if self.max_collators < 1 {
133
1
            return Err(InconsistentError::MaxCollatorsTooLow);
134
5475
        }
135
5475
        if self.min_orchestrator_collators < 1 && !allow_empty_orchestrator {
136
2
            return Err(InconsistentError::MinOrchestratorCollatorsTooLow);
137
5473
        }
138
5473
        if self.max_orchestrator_collators < self.min_orchestrator_collators {
139
            return Err(InconsistentError::MaxCollatorsLowerThanMinCollators);
140
5473
        }
141
5473
        if self.parathreads_per_collator != 1 {
142
            return Err(InconsistentError::UnimplementedParameter);
143
5473
        }
144
5473
        if self.max_collators < self.min_orchestrator_collators {
145
2
            return Err(InconsistentError::MaxCollatorsLowerThanMinCollators);
146
5471
        }
147
5471
        Ok(())
148
5476
    }
149

            
150
    /// Checks that this instance is consistent with the requirements on each individual member.
151
    ///
152
    /// # Panics
153
    ///
154
    /// This function panics if the configuration is inconsistent.
155
4690
    pub fn panic_if_not_consistent(&self, allow_empty_orchestrator: bool) {
156
4690
        if let Err(err) = self.check_consistency(allow_empty_orchestrator) {
157
            panic!("Host configuration is inconsistent: {:?}", err);
158
4690
        }
159
4690
    }
160
}
161

            
162
7084
#[frame_support::pallet]
163
pub mod pallet {
164
    use tp_traits::GetHostConfiguration;
165

            
166
    use super::*;
167

            
168
41956
    #[pallet::pallet]
169
    pub struct Pallet<T>(_);
170

            
171
    /// Configure the pallet by specifying the parameters and types on which it depends.
172
    #[pallet::config]
173
    pub trait Config: frame_system::Config {
174
        type SessionIndex: parity_scale_codec::FullCodec + TypeInfo + Copy + AtLeast32BitUnsigned;
175

            
176
        // `SESSION_DELAY` is used to delay any changes to Paras registration or configurations.
177
        // Wait until the session index is 2 larger then the current index to apply any changes,
178
        // which guarantees that at least one full session has passed before any changes are applied.
179
        #[pallet::constant]
180
        type SessionDelay: Get<Self::SessionIndex>;
181

            
182
        type CurrentSessionIndex: GetSessionIndex<Self::SessionIndex>;
183

            
184
        type ForceEmptyOrchestrator: Get<bool>;
185

            
186
        /// Weight information for extrinsics in this pallet.
187
        type WeightInfo: WeightInfo;
188
    }
189

            
190
20
    #[pallet::error]
191
    pub enum Error<T> {
192
        /// The new value for a configuration parameter is invalid.
193
        InvalidNewValue,
194
    }
195

            
196
    /// The active configuration for the current session.
197
56792
    #[pallet::storage]
198
    pub(crate) type ActiveConfig<T: Config> = StorageValue<_, HostConfiguration, ValueQuery>;
199

            
200
    /// Pending configuration changes.
201
    ///
202
    /// This is a list of configuration changes, each with a session index at which it should
203
    /// be applied.
204
    ///
205
    /// The list is sorted ascending by session index. Also, this list can only contain at most
206
    /// 2 items: for the next session and for the `scheduled_session`.
207
46662
    #[pallet::storage]
208
    // TODO: instead of making this unbounded, we could refactor into a BoundedVec<X, Const<2>>
209
    // since it can have at most 2 items anyway. But the upstream pallet doesn't do that so low
210
    // priority.
211
    #[pallet::unbounded]
212
    pub(crate) type PendingConfigs<T: Config> =
213
        StorageValue<_, Vec<(T::SessionIndex, HostConfiguration)>, ValueQuery>;
214

            
215
    /// If this is set, then the configuration setters will bypass the consistency checks. This
216
    /// is meant to be used only as the last resort.
217
150
    #[pallet::storage]
218
    pub(crate) type BypassConsistencyCheck<T: Config> = StorageValue<_, bool, ValueQuery>;
219

            
220
    #[pallet::genesis_config]
221
    #[derive(frame_support::DefaultNoBound)]
222
    pub struct GenesisConfig<T: Config> {
223
        pub config: HostConfiguration,
224
        #[serde(skip)]
225
        pub _config: sp_std::marker::PhantomData<T>,
226
    }
227

            
228
357
    #[pallet::genesis_build]
229
    impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
230
370
        fn build(&self) {
231
370
            self.config
232
370
                .panic_if_not_consistent(T::ForceEmptyOrchestrator::get());
233
370
            ActiveConfig::<T>::put(&self.config);
234
370
        }
235
    }
236

            
237
389
    #[pallet::call]
238
    impl<T: Config> Pallet<T> {
239
        #[pallet::call_index(0)]
240
        #[pallet::weight((
241
			T::WeightInfo::set_config_with_u32(),
242
			DispatchClass::Operational,
243
		))]
244
23
        pub fn set_max_collators(origin: OriginFor<T>, new: u32) -> DispatchResult {
245
23
            ensure_root(origin)?;
246
17
            Self::schedule_config_update(|config| {
247
17
                config.max_collators = new;
248
17
            })
249
        }
250

            
251
        #[pallet::call_index(1)]
252
        #[pallet::weight((
253
			T::WeightInfo::set_config_with_u32(),
254
			DispatchClass::Operational,
255
		))]
256
22
        pub fn set_min_orchestrator_collators(origin: OriginFor<T>, new: u32) -> DispatchResult {
257
22
            ensure_root(origin)?;
258
16
            Self::schedule_config_update(|config| {
259
16
                if config.max_orchestrator_collators < new {
260
13
                    config.max_orchestrator_collators = new;
261
13
                }
262
16
                config.min_orchestrator_collators = new;
263
16
            })
264
        }
265

            
266
        #[pallet::call_index(2)]
267
        #[pallet::weight((
268
			T::WeightInfo::set_config_with_u32(),
269
			DispatchClass::Operational,
270
		))]
271
17
        pub fn set_max_orchestrator_collators(origin: OriginFor<T>, new: u32) -> DispatchResult {
272
17
            ensure_root(origin)?;
273
11
            Self::schedule_config_update(|config| {
274
11
                if config.min_orchestrator_collators > new {
275
3
                    config.min_orchestrator_collators = new;
276
9
                }
277
11
                config.max_orchestrator_collators = new;
278
11
            })
279
        }
280

            
281
        #[pallet::call_index(3)]
282
        #[pallet::weight((
283
			T::WeightInfo::set_config_with_u32(),
284
			DispatchClass::Operational,
285
		))]
286
24
        pub fn set_collators_per_container(origin: OriginFor<T>, new: u32) -> DispatchResult {
287
24
            ensure_root(origin)?;
288
18
            Self::schedule_config_update(|config| {
289
18
                config.collators_per_container = new;
290
18
            })
291
        }
292

            
293
        #[pallet::call_index(4)]
294
        #[pallet::weight((
295
			T::WeightInfo::set_config_with_u32(),
296
			DispatchClass::Operational,
297
		))]
298
7
        pub fn set_full_rotation_period(origin: OriginFor<T>, new: u32) -> DispatchResult {
299
7
            ensure_root(origin)?;
300
7
            Self::schedule_config_update(|config| {
301
7
                config.full_rotation_period = new;
302
7
            })
303
        }
304

            
305
        #[pallet::call_index(5)]
306
        #[pallet::weight((
307
        T::WeightInfo::set_config_with_u32(),
308
        DispatchClass::Operational,
309
        ))]
310
        pub fn set_collators_per_parathread(origin: OriginFor<T>, new: u32) -> DispatchResult {
311
            ensure_root(origin)?;
312
            Self::schedule_config_update(|config| {
313
                config.collators_per_parathread = new;
314
            })
315
        }
316

            
317
        #[pallet::call_index(6)]
318
        #[pallet::weight((
319
        T::WeightInfo::set_config_with_u32(),
320
        DispatchClass::Operational,
321
        ))]
322
        pub fn set_parathreads_per_collator(origin: OriginFor<T>, new: u32) -> DispatchResult {
323
            ensure_root(origin)?;
324
            Self::schedule_config_update(|config| {
325
                config.parathreads_per_collator = new;
326
            })
327
        }
328

            
329
        #[pallet::call_index(7)]
330
        #[pallet::weight((
331
        T::WeightInfo::set_config_with_u32(),
332
        DispatchClass::Operational,
333
        ))]
334
        pub fn set_target_container_chain_fullness(
335
            origin: OriginFor<T>,
336
            new: Perbill,
337
6
        ) -> DispatchResult {
338
6
            ensure_root(origin)?;
339
6
            Self::schedule_config_update(|config| {
340
6
                config.target_container_chain_fullness = new;
341
6
            })
342
        }
343

            
344
        #[pallet::call_index(8)]
345
        #[pallet::weight((
346
        T::WeightInfo::set_config_with_u32(),
347
        DispatchClass::Operational,
348
        ))]
349
        pub fn set_max_parachain_cores_percentage(
350
            origin: OriginFor<T>,
351
            new: Option<Perbill>,
352
        ) -> DispatchResult {
353
            ensure_root(origin)?;
354
            Self::schedule_config_update(|config| {
355
                config.max_parachain_cores_percentage = new;
356
            })
357
        }
358

            
359
        /// Setting this to true will disable consistency checks for the configuration setters.
360
        /// Use with caution.
361
        #[pallet::call_index(44)]
362
        #[pallet::weight((
363
			T::DbWeight::get().writes(1),
364
			DispatchClass::Operational,
365
		))]
366
        pub fn set_bypass_consistency_check(origin: OriginFor<T>, new: bool) -> DispatchResult {
367
            ensure_root(origin)?;
368
            BypassConsistencyCheck::<T>::put(new);
369
            Ok(())
370
        }
371
    }
372

            
373
    /// A struct that holds the configuration that was active before the session change and optionally
374
    /// a configuration that became active after the session change.
375
    pub struct SessionChangeOutcome {
376
        /// Previously active configuration.
377
        pub prev_config: HostConfiguration,
378
        /// If new configuration was applied during the session change, this is the new configuration.
379
        pub new_config: Option<HostConfiguration>,
380
    }
381

            
382
    impl<T: Config> Pallet<T> {
383
        /// Called by the initializer to note that a new session has started.
384
        ///
385
        /// Returns the configuration that was actual before the session change and the configuration
386
        /// that became active after the session change. If there were no scheduled changes, both will
387
        /// be the same.
388
2966
        pub fn initializer_on_new_session(session_index: &T::SessionIndex) -> SessionChangeOutcome {
389
2966
            let pending_configs = <PendingConfigs<T>>::get();
390
2966
            let prev_config = ActiveConfig::<T>::get();
391
2966

            
392
2966
            // No pending configuration changes, so we're done.
393
2966
            if pending_configs.is_empty() {
394
2854
                return SessionChangeOutcome {
395
2854
                    prev_config,
396
2854
                    new_config: None,
397
2854
                };
398
112
            }
399
112

            
400
112
            // We partition those configs scheduled for the present
401
112
            // and those for the future
402
112
            let (mut past_and_present, future) = pending_configs
403
112
                .into_iter()
404
120
                .partition::<Vec<_>, _>(|&(apply_at_session, _)| {
405
120
                    apply_at_session <= *session_index
406
120
                });
407
112

            
408
112
            if past_and_present.len() > 1 {
409
                // This should never happen since we schedule configuration changes only into the future
410
                // sessions and this handler called for each session change.
411
                log::error!(
412
                    target: LOG_TARGET,
413
                    "Skipping applying configuration changes scheduled sessions in the past",
414
                );
415
112
            }
416

            
417
112
            let new_config = past_and_present.pop().map(|(_, config)| config);
418
112
            if let Some(ref new_config) = new_config {
419
60
                // Apply the new configuration.
420
60
                ActiveConfig::<T>::put(new_config);
421
60
            }
422

            
423
            // We insert future as PendingConfig
424
112
            <PendingConfigs<T>>::put(future);
425
112

            
426
112
            SessionChangeOutcome {
427
112
                prev_config,
428
112
                new_config,
429
112
            }
430
2966
        }
431

            
432
        /// Return the session index that should be used for any future scheduled changes.
433
70
        fn scheduled_session() -> T::SessionIndex {
434
70
            T::CurrentSessionIndex::session_index().saturating_add(T::SessionDelay::get())
435
70
        }
436

            
437
        /// Forcibly set the active config. This should be used with extreme care, and typically
438
        /// only when enabling parachains runtime pallets for the first time on a chain which has
439
        /// been running without them.
440
        pub fn force_set_active_config(config: HostConfiguration) {
441
            ActiveConfig::<T>::set(config);
442
        }
443

            
444
        /// This function should be used to update members of the configuration.
445
        ///
446
        /// This function is used to update the configuration in a way that is safe. It will check the
447
        /// resulting configuration and ensure that the update is valid. If the update is invalid, it
448
        /// will check if the previous configuration was valid. If it was invalid, we proceed with
449
        /// updating the configuration, giving a chance to recover from such a condition.
450
        ///
451
        /// The actual configuration change take place after a couple of sessions have passed. In case
452
        /// this function is called more than once in a session, then the pending configuration change
453
        /// will be updated and the changes will be applied at once.
454
        // NOTE: Explicitly tell rustc not to inline this because otherwise heuristics note the incoming
455
        // closure making it's attractive to inline. However, in this case, we will end up with lots of
456
        // duplicated code (making this function to show up in the top of heaviest functions) only for
457
        // the sake of essentially avoiding an indirect call. Doesn't worth it.
458
        #[inline(never)]
459
75
        fn schedule_config_update(updater: impl FnOnce(&mut HostConfiguration)) -> DispatchResult {
460
75
            let mut pending_configs = <PendingConfigs<T>>::get();
461
75

            
462
75
            // 1. pending_configs = []
463
75
            //    No pending configuration changes.
464
75
            //
465
75
            //    That means we should use the active config as the base configuration. We will insert
466
75
            //    the new pending configuration as (cur+2, new_config) into the list.
467
75
            //
468
75
            // 2. pending_configs = [(cur+2, X)]
469
75
            //    There is a configuration that is pending for the scheduled session.
470
75
            //
471
75
            //    We will use X as the base configuration. We can update the pending configuration X
472
75
            //    directly.
473
75
            //
474
75
            // 3. pending_configs = [(cur+1, X)]
475
75
            //    There is a pending configuration scheduled and it will be applied in the next session.
476
75
            //
477
75
            //    We will use X as the base configuration. We need to schedule a new configuration change
478
75
            //    for the `scheduled_session` and use X as the base for the new configuration.
479
75
            //
480
75
            // 4. pending_configs = [(cur+1, X), (cur+2, Y)]
481
75
            //    There is a pending configuration change in the next session and for the scheduled
482
75
            //    session. Due to case â„–3, we can be sure that Y is based on top of X. This means we
483
75
            //    can use Y as the base configuration and update Y directly.
484
75
            //
485
75
            // There cannot be (cur, X) because those are applied in the session change handler for the
486
75
            // current session.
487
75

            
488
75
            // First, we need to decide what we should use as the base configuration.
489
75
            let mut base_config = pending_configs
490
75
                .last()
491
75
                .map(|(_, config)| config.clone())
492
75
                .unwrap_or_else(Self::config);
493
75
            let base_config_consistent = base_config
494
75
                .check_consistency(T::ForceEmptyOrchestrator::get())
495
75
                .is_ok();
496
75

            
497
75
            // Now, we need to decide what the new configuration should be.
498
75
            // We also move the `base_config` to `new_config` to empahsize that the base config was
499
75
            // destroyed by the `updater`.
500
75
            updater(&mut base_config);
501
75
            let new_config = base_config;
502
75

            
503
75
            if BypassConsistencyCheck::<T>::get() {
504
                // This will emit a warning each configuration update if the consistency check is
505
                // bypassed. This is an attempt to make sure the bypass is not accidentally left on.
506
                log::warn!(
507
                    target: LOG_TARGET,
508
                    "Bypassing the consistency check for the configuration change!",
509
                );
510
75
            } else if let Err(e) = new_config.check_consistency(T::ForceEmptyOrchestrator::get()) {
511
5
                if base_config_consistent {
512
                    // Base configuration is consistent and the new configuration is inconsistent.
513
                    // This means that the value set by the `updater` is invalid and we can return
514
                    // it as an error.
515
5
                    log::warn!(
516
                        target: LOG_TARGET,
517
                        "Configuration change rejected due to invalid configuration: {:?}",
518
                        e,
519
                    );
520
5
                    return Err(Error::<T>::InvalidNewValue.into());
521
                } else {
522
                    // The configuration was already broken, so we can as well proceed with the update.
523
                    // You cannot break something that is already broken.
524
                    //
525
                    // That will allow to call several functions and ultimately return the configuration
526
                    // into consistent state.
527
                    log::warn!(
528
                        target: LOG_TARGET,
529
                        "The new configuration is broken but the old is broken as well. Proceeding",
530
                    );
531
                }
532
70
            }
533

            
534
70
            let scheduled_session = Self::scheduled_session();
535

            
536
70
            if let Some(&mut (_, ref mut config)) = pending_configs
537
70
                .iter_mut()
538
70
                .find(|&&mut (apply_at_session, _)| apply_at_session >= scheduled_session)
539
10
            {
540
10
                *config = new_config;
541
60
            } else {
542
60
                // We are scheduling a new configuration change for the scheduled session.
543
60
                pending_configs.push((scheduled_session, new_config));
544
60
            }
545

            
546
70
            <PendingConfigs<T>>::put(pending_configs);
547
70

            
548
70
            Ok(())
549
75
        }
550

            
551
25000
        pub fn config() -> HostConfiguration {
552
25000
            ActiveConfig::<T>::get()
553
25000
        }
554

            
555
20103
        pub fn pending_configs() -> Vec<(T::SessionIndex, HostConfiguration)> {
556
20103
            PendingConfigs::<T>::get()
557
20103
        }
558

            
559
20101
        pub fn config_at_session(session_index: T::SessionIndex) -> HostConfiguration {
560
20101
            let (past_and_present, _) = Pallet::<T>::pending_configs()
561
20101
                .into_iter()
562
20101
                .partition::<Vec<_>, _>(|&(apply_at_session, _)| apply_at_session <= session_index);
563

            
564
20101
            let config = if let Some(last) = past_and_present.last() {
565
405
                last.1.clone()
566
            } else {
567
19696
                Pallet::<T>::config()
568
            };
569

            
570
20101
            config
571
20101
        }
572
    }
573

            
574
    impl<T: Config> GetHostConfiguration<T::SessionIndex> for Pallet<T> {
575
5763
        fn max_collators(session_index: T::SessionIndex) -> u32 {
576
5763
            Self::config_at_session(session_index).max_collators
577
5763
        }
578

            
579
2944
        fn collators_per_container(session_index: T::SessionIndex) -> u32 {
580
2944
            Self::config_at_session(session_index).collators_per_container
581
2944
        }
582

            
583
2944
        fn collators_per_parathread(session_index: T::SessionIndex) -> u32 {
584
2944
            Self::config_at_session(session_index).collators_per_parathread
585
2944
        }
586

            
587
5503
        fn min_collators_for_orchestrator(session_index: T::SessionIndex) -> u32 {
588
5503
            Self::config_at_session(session_index).min_orchestrator_collators
589
5503
        }
590

            
591
2559
        fn max_collators_for_orchestrator(session_index: T::SessionIndex) -> u32 {
592
2559
            Self::config_at_session(session_index).max_orchestrator_collators
593
2559
        }
594

            
595
        fn target_container_chain_fullness(session_index: T::SessionIndex) -> Perbill {
596
            Self::config_at_session(session_index).target_container_chain_fullness
597
        }
598

            
599
388
        fn max_parachain_cores_percentage(session_index: T::SessionIndex) -> Option<Perbill> {
600
388
            Self::config_at_session(session_index).max_parachain_cores_percentage
601
388
        }
602
    }
603
}