radix_engine/blueprints/consensus_manager/
consensus_manager.rs

1use super::{EpochChangeEvent, RoundChangeEvent, ValidatorCreator, ValidatorOwnerBadgeData};
2use crate::blueprints::consensus_manager::VALIDATOR_ROLE;
3use crate::errors::ApplicationError;
4use crate::errors::RuntimeError;
5use crate::internal_prelude::*;
6use radix_engine_interface::api::field_api::LockFlags;
7use radix_engine_interface::api::object_api::ModuleId;
8use radix_engine_interface::api::{
9    AttachedModuleId, CollectionIndex, FieldValue, SystemApi, ACTOR_STATE_SELF,
10};
11use radix_engine_interface::blueprints::consensus_manager::*;
12use radix_engine_interface::blueprints::package::BlueprintDefinitionInit;
13use radix_engine_interface::blueprints::resource::*;
14use radix_engine_interface::object_modules::metadata::UncheckedUrl;
15use radix_engine_interface::{metadata_init, mint_roles, rule};
16use radix_native_sdk::modules::metadata::Metadata;
17use radix_native_sdk::modules::role_assignment::RoleAssignment;
18use radix_native_sdk::resource::NativeVault;
19use radix_native_sdk::resource::{NativeBucket, ResourceManager};
20use radix_native_sdk::runtime::Runtime;
21
22const MILLIS_IN_SECOND: i64 = 1000;
23const SECONDS_IN_MINUTE: i64 = 60;
24const MILLIS_IN_MINUTE: i64 = MILLIS_IN_SECOND * SECONDS_IN_MINUTE;
25
26#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
27pub struct ConsensusManagerConfigSubstate {
28    pub config: ConsensusManagerConfig,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
32pub struct ConsensusManagerSubstate {
33    /// Whether the consensus process has started
34    pub started: bool,
35    /// The current epoch.
36    pub epoch: Epoch,
37    /// The effective start-time of the epoch.
38    /// This is used to calculate the effective duration, for the purpose of calculating
39    /// when to change epoch. This will typically be close to the `actual_epoch_start_milli`
40    /// but may differ slightly as it attempts to avoid minor systematic drift in the epoch
41    /// start time.
42    pub effective_epoch_start_milli: i64,
43    /// The actual start-time of the epoch.
44    /// This is just saved as a sanity-check for checking divergence between actual and effective.
45    pub actual_epoch_start_milli: i64,
46    /// The current round in the epoch.
47    pub round: Round,
48    /// The current leader - this is used for knowing who was the validator for the following
49    /// round of transactions
50    pub current_leader: Option<ValidatorIndex>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, ScryptoSbor)]
54pub struct Validator {
55    pub key: Secp256k1PublicKey,
56    pub stake: Decimal,
57}
58
59#[derive(Debug, PartialEq, Eq, ScryptoSbor)]
60pub struct ValidatorRewardsSubstate {
61    pub proposer_rewards: IndexMap<ValidatorIndex, Decimal>,
62    pub rewards_vault: Vault,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
66pub struct CurrentValidatorSetSubstate {
67    pub validator_set: ActiveValidatorSet,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
71#[sbor(transparent)]
72pub struct ActiveValidatorSet {
73    /// The validators in the set, ordered by stake descending.
74    pub validators_by_stake_desc: IndexMap<ComponentAddress, Validator>,
75}
76
77impl ActiveValidatorSet {
78    pub fn get_by_index(&self, index: ValidatorIndex) -> Option<(&ComponentAddress, &Validator)> {
79        self.validators_by_stake_desc.get_index(index as usize)
80    }
81
82    pub fn get_by_address(&self, address: &ComponentAddress) -> Option<&Validator> {
83        self.validators_by_stake_desc.get(address)
84    }
85
86    /// Note for performance - this is calculated by iterating over the whole validator set.
87    pub fn get_by_public_key(
88        &self,
89        public_key: &Secp256k1PublicKey,
90    ) -> Option<(&ComponentAddress, &Validator)> {
91        self.validators_by_stake_desc
92            .iter()
93            .find(|(_, validator)| &validator.key == public_key)
94    }
95
96    /// Note for performance - this is calculated by iterating over the whole validator set.
97    pub fn total_active_stake_xrd(&self) -> Result<Decimal, RuntimeError> {
98        let mut sum = Decimal::ZERO;
99        for v in self
100            .validators_by_stake_desc
101            .iter()
102            .map(|(_, validator)| validator.stake)
103        {
104            sum = sum.checked_add(v).ok_or(RuntimeError::ApplicationError(
105                ApplicationError::ConsensusManagerError(
106                    ConsensusManagerError::UnexpectedDecimalComputationError,
107                ),
108            ))?;
109        }
110        Ok(sum)
111    }
112
113    pub fn validator_count(&self) -> usize {
114        self.validators_by_stake_desc.len()
115    }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
119#[sbor(transparent)]
120pub struct ProposerMilliTimestampSubstate {
121    /// A number of millis elapsed since epoch (i.e. a classic "epoch millis" timestamp).
122    /// A signed number is traditionally used (for reasons like representing instants before A.D.
123    /// 1970, which may not even apply in our case).
124    pub epoch_milli: i64,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
128#[sbor(transparent)]
129pub struct ProposerMinuteTimestampSubstate {
130    /// A number of full minutes elapsed since epoch.
131    /// A signed number is used for the same reasons as in [`ProposerMilliTimestampSubstate`], and
132    /// gives us time until A.D. 5772.
133    pub epoch_minute: i32,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
137pub struct CurrentProposalStatisticSubstate {
138    /// A proposal statistic of each validator from the current validator set, in the iteration
139    /// order of [`CurrentValidatorSetSubstate.validator_set`].
140    pub validator_statistics: Vec<ProposalStatistic>,
141}
142
143impl CurrentProposalStatisticSubstate {
144    /// Gets a mutable reference to a proposal statistic tracker of an individual validator.
145    pub fn get_mut_proposal_statistic(
146        &mut self,
147        validator_index: ValidatorIndex,
148    ) -> Result<&mut ProposalStatistic, RuntimeError> {
149        let validator_count = self.validator_statistics.len();
150        self.validator_statistics
151            .get_mut(validator_index as usize)
152            .ok_or_else(|| {
153                RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
154                    ConsensusManagerError::InvalidValidatorIndex {
155                        index: validator_index,
156                        count: validator_count,
157                    },
158                ))
159            })
160    }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Default, ScryptoSbor)]
164pub struct ProposalStatistic {
165    /// A counter of successful proposals made by a specific validator.
166    pub made: u64,
167    /// A counter of missed proposals (caused both by gap rounds or fallback rounds).
168    pub missed: u64,
169}
170
171impl ProposalStatistic {
172    /// A ratio of successful to total proposals.
173    /// There is a special case of a validator which did not have a chance of leading even a single
174    /// round of consensus - currently we assume they should not be punished (i.e. we return `1.0`).
175    pub fn success_ratio(&self) -> Result<Decimal, RuntimeError> {
176        let total = self.made + self.missed;
177        if total == 0 {
178            return Ok(Decimal::one());
179        }
180        Ok(Decimal::from(self.made)
181            .checked_div(total)
182            .ok_or(RuntimeError::ApplicationError(
183                ApplicationError::ConsensusManagerError(
184                    ConsensusManagerError::UnexpectedDecimalComputationError,
185                ),
186            ))?)
187    }
188}
189
190#[derive(Debug, Clone, Eq, PartialEq, ScryptoSbor)]
191pub enum ConsensusManagerError {
192    InvalidRoundUpdate {
193        from: Round,
194        to: Round,
195    },
196    InvalidProposerTimestampUpdate {
197        from_millis: i64,
198        to_millis: i64,
199    },
200    InconsistentGapRounds {
201        gap_rounds: usize,
202        progressed_rounds: u64,
203    },
204    InvalidValidatorIndex {
205        index: ValidatorIndex,
206        count: usize,
207    },
208    AlreadyStarted,
209    NotXrd,
210    UnexpectedDecimalComputationError,
211    EpochMathOverflow,
212    InvalidConsensusTime(i64),
213    ExceededValidatorCount {
214        current: u32,
215        max: u32,
216    },
217}
218
219declare_native_blueprint_state! {
220    blueprint_ident: ConsensusManager,
221    blueprint_snake_case: consensus_manager,
222    features: {
223    },
224    fields: {
225        config: {
226            ident: Configuration,
227            field_type: {
228                kind: StaticSingleVersioned,
229            },
230            condition: Condition::Always,
231        },
232        state: {
233            ident: State,
234            field_type: {
235                kind: StaticSingleVersioned,
236            },
237            condition: Condition::Always,
238        },
239        validator_rewards: {
240            ident: ValidatorRewards,
241            field_type: {
242                kind: StaticSingleVersioned,
243            },
244            condition: Condition::Always,
245        },
246        current_validator_set: {
247            ident: CurrentValidatorSet,
248            field_type: {
249                kind: StaticSingleVersioned,
250            },
251            condition: Condition::Always,
252        },
253        current_proposal_statistic: {
254            ident: CurrentProposalStatistic,
255            field_type: {
256                kind: StaticSingleVersioned,
257            },
258            condition: Condition::Always,
259        },
260        proposer_minute_timestamp: {
261            ident: ProposerMinuteTimestamp,
262            field_type: {
263                kind: StaticSingleVersioned,
264            },
265            condition: Condition::Always,
266        },
267        proposer_milli_timestamp: {
268            ident: ProposerMilliTimestamp,
269            field_type: {
270                kind: StaticSingleVersioned,
271            },
272            condition: Condition::Always,
273        },
274    },
275    collections: {
276        registered_validators_by_stake: SortedIndex {
277            entry_ident: RegisteredValidatorByStake,
278            key_type: {
279                kind: Static,
280                content_type: ComponentAddress,
281            },
282            full_key_content: {
283                full_content_type: ValidatorByStakeKey,
284                sort_prefix_property_name: inverse_stake_sort_prefix,
285            },
286            value_type: {
287                kind: StaticSingleVersioned,
288            },
289            allow_ownership: false,
290        },
291    }
292}
293
294#[derive(Debug, Clone, ScryptoSbor)]
295pub struct ValidatorByStakeKey {
296    pub divided_stake: u16,
297    pub validator_address: ComponentAddress,
298}
299
300impl SortedIndexKeyContentSource<ConsensusManagerRegisteredValidatorByStakeKeyPayload>
301    for ValidatorByStakeKey
302{
303    fn sort_key(&self) -> u16 {
304        u16::MAX - self.divided_stake
305    }
306
307    fn into_content(
308        self,
309    ) -> <ConsensusManagerRegisteredValidatorByStakeKeyPayload as SortedIndexKeyPayload>::Content
310    {
311        self.validator_address
312    }
313}
314
315impl SortedIndexKeyFullContent<ConsensusManagerRegisteredValidatorByStakeKeyPayload>
316    for ValidatorByStakeKey
317{
318    fn from_sort_key_and_content(sort_key: u16, validator_address: ComponentAddress) -> Self {
319        Self {
320            divided_stake: u16::MAX - sort_key,
321            validator_address,
322        }
323    }
324
325    fn as_content(&self) -> &ComponentAddress {
326        &self.validator_address
327    }
328}
329
330pub type ConsensusManagerConfigurationV1 = ConsensusManagerConfigSubstate;
331pub type ConsensusManagerStateV1 = ConsensusManagerSubstate;
332pub type ConsensusManagerValidatorRewardsV1 = ValidatorRewardsSubstate;
333pub type ConsensusManagerCurrentValidatorSetV1 = CurrentValidatorSetSubstate;
334pub type ConsensusManagerCurrentProposalStatisticV1 = CurrentProposalStatisticSubstate;
335pub type ConsensusManagerProposerMinuteTimestampV1 = ProposerMinuteTimestampSubstate;
336pub type ConsensusManagerProposerMilliTimestampV1 = ProposerMilliTimestampSubstate;
337pub type ConsensusManagerRegisteredValidatorByStakeV1 = Validator;
338
339pub const CONSENSUS_MANAGER_REGISTERED_VALIDATORS_BY_STAKE_INDEX: CollectionIndex = 0u8;
340
341pub struct ConsensusManagerBlueprint;
342
343impl ConsensusManagerBlueprint {
344    pub fn definition() -> BlueprintDefinitionInit {
345        let mut aggregator = TypeAggregator::<ScryptoCustomTypeKind>::new();
346
347        let feature_set = ConsensusManagerFeatureSet::all_features();
348        let state = ConsensusManagerStateSchemaInit::create_schema_init(&mut aggregator);
349
350        let mut functions = index_map_new();
351        functions.insert(
352            CONSENSUS_MANAGER_CREATE_IDENT.to_string(),
353            FunctionSchemaInit {
354                receiver: None,
355                input: TypeRef::Static(
356                    aggregator.add_child_type_and_descendents::<ConsensusManagerCreateInput>(),
357                ),
358                output: TypeRef::Static(
359                    aggregator.add_child_type_and_descendents::<ConsensusManagerCreateOutput>(),
360                ),
361                export: CONSENSUS_MANAGER_CREATE_IDENT.to_string(),
362            },
363        );
364        functions.insert(
365            CONSENSUS_MANAGER_GET_CURRENT_EPOCH_IDENT.to_string(),
366            FunctionSchemaInit {
367                receiver: Some(ReceiverInfo::normal_ref()),
368                input: TypeRef::Static(
369                    aggregator
370                        .add_child_type_and_descendents::<ConsensusManagerGetCurrentEpochInput>(),
371                ),
372                output: TypeRef::Static(
373                    aggregator
374                        .add_child_type_and_descendents::<ConsensusManagerGetCurrentEpochOutput>(),
375                ),
376                export: CONSENSUS_MANAGER_GET_CURRENT_EPOCH_IDENT.to_string(),
377            },
378        );
379        functions.insert(
380            CONSENSUS_MANAGER_START_IDENT.to_string(),
381            FunctionSchemaInit {
382                receiver: Some(ReceiverInfo::normal_ref_mut()),
383                input: TypeRef::Static(
384                    aggregator.add_child_type_and_descendents::<ConsensusManagerStartInput>(),
385                ),
386                output: TypeRef::Static(
387                    aggregator.add_child_type_and_descendents::<ConsensusManagerStartOutput>(),
388                ),
389                export: CONSENSUS_MANAGER_START_IDENT.to_string(),
390            },
391        );
392        functions.insert(
393            CONSENSUS_MANAGER_GET_CURRENT_TIME_IDENT.to_string(),
394            FunctionSchemaInit {
395                receiver: Some(ReceiverInfo::normal_ref()),
396                input: TypeRef::Static(
397                    aggregator
398                        .add_child_type_and_descendents::<ConsensusManagerGetCurrentTimeInputV1>(),
399                ),
400                output: TypeRef::Static(
401                    aggregator
402                        .add_child_type_and_descendents::<ConsensusManagerGetCurrentTimeOutput>(),
403                ),
404                export: CONSENSUS_MANAGER_GET_CURRENT_TIME_IDENT.to_string(),
405            },
406        );
407        functions.insert(
408            CONSENSUS_MANAGER_COMPARE_CURRENT_TIME_IDENT.to_string(),
409            FunctionSchemaInit {
410                receiver: Some(ReceiverInfo::normal_ref()),
411                input: TypeRef::Static(
412                    aggregator
413                        .add_child_type_and_descendents::<ConsensusManagerCompareCurrentTimeInputV1>(
414                        ),
415                ),
416                output: TypeRef::Static(
417                    aggregator
418                        .add_child_type_and_descendents::<ConsensusManagerCompareCurrentTimeOutput>(
419                        ),
420                ),
421                export: CONSENSUS_MANAGER_COMPARE_CURRENT_TIME_IDENT.to_string(),
422            },
423        );
424        functions.insert(
425            CONSENSUS_MANAGER_NEXT_ROUND_IDENT.to_string(),
426            FunctionSchemaInit {
427                receiver: Some(ReceiverInfo::normal_ref_mut()),
428                input: TypeRef::Static(
429                    aggregator.add_child_type_and_descendents::<ConsensusManagerNextRoundInput>(),
430                ),
431                output: TypeRef::Static(
432                    aggregator.add_child_type_and_descendents::<ConsensusManagerNextRoundOutput>(),
433                ),
434                export: CONSENSUS_MANAGER_NEXT_ROUND_IDENT.to_string(),
435            },
436        );
437        functions.insert(
438            CONSENSUS_MANAGER_CREATE_VALIDATOR_IDENT.to_string(),
439            FunctionSchemaInit {
440                receiver: Some(ReceiverInfo::normal_ref_mut()),
441                input: TypeRef::Static(
442                    aggregator
443                        .add_child_type_and_descendents::<ConsensusManagerCreateValidatorInput>(),
444                ),
445                output: TypeRef::Static(
446                    aggregator
447                        .add_child_type_and_descendents::<ConsensusManagerCreateValidatorOutput>(),
448                ),
449                export: CONSENSUS_MANAGER_CREATE_VALIDATOR_IDENT.to_string(),
450            },
451        );
452
453        let event_schema = event_schema! {
454            aggregator,
455            [
456                RoundChangeEvent,
457                EpochChangeEvent
458            ]
459        };
460
461        let consensus_manager_schema = generate_full_schema(aggregator);
462
463        BlueprintDefinitionInit {
464            blueprint_type: BlueprintType::default(),
465            is_transient: false,
466            feature_set,
467            dependencies: indexset!(
468                XRD.into(),
469                PACKAGE_OF_DIRECT_CALLER_RESOURCE.into(),
470                SYSTEM_EXECUTION_RESOURCE.into(),
471                VALIDATOR_OWNER_BADGE.into(),
472            ),
473            schema: BlueprintSchemaInit {
474                generics: vec![],
475                schema: consensus_manager_schema,
476                state,
477                events: event_schema,
478                types: BlueprintTypeSchemaInit::default(),
479                functions: BlueprintFunctionsSchemaInit { functions },
480                hooks: BlueprintHooksInit::default(),
481            },
482
483            royalty_config: PackageRoyaltyConfig::default(),
484            auth_config: AuthConfig {
485                function_auth: FunctionAuth::AccessRules(indexmap!(
486                    CONSENSUS_MANAGER_CREATE_IDENT.to_string() => rule!(require(system_execution(SystemExecution::Protocol))),
487                )),
488                method_auth: MethodAuthTemplate::StaticRoleDefinition(roles_template!(
489                    roles {
490                        VALIDATOR_ROLE;
491                    },
492                    methods {
493                        CONSENSUS_MANAGER_START_IDENT => []; // Genesis is able to call this by skipping auth
494                        CONSENSUS_MANAGER_NEXT_ROUND_IDENT => [VALIDATOR_ROLE];
495
496                        CONSENSUS_MANAGER_GET_CURRENT_EPOCH_IDENT => MethodAccessibility::Public;
497                        CONSENSUS_MANAGER_GET_CURRENT_TIME_IDENT => MethodAccessibility::Public;
498                        CONSENSUS_MANAGER_COMPARE_CURRENT_TIME_IDENT => MethodAccessibility::Public;
499                        CONSENSUS_MANAGER_CREATE_VALIDATOR_IDENT => MethodAccessibility::Public;
500                    }
501                )),
502            },
503        }
504    }
505
506    pub(crate) fn create<Y: SystemApi<RuntimeError>>(
507        validator_token_address_reservation: GlobalAddressReservation,
508        consensus_manager_address_reservation: GlobalAddressReservation,
509        genesis_epoch: Epoch,
510        initial_config: ConsensusManagerConfig,
511        initial_time_milli: i64,
512        initial_current_leader: Option<ValidatorIndex>,
513        api: &mut Y,
514    ) -> Result<(), RuntimeError> {
515        if initial_config.max_validators > ValidatorIndex::MAX as u32 {
516            return Err(RuntimeError::ApplicationError(
517                ApplicationError::ConsensusManagerError(
518                    ConsensusManagerError::ExceededValidatorCount {
519                        current: initial_config.max_validators,
520                        max: ValidatorIndex::MAX as u32,
521                    },
522                ),
523            ));
524        }
525
526        {
527            // TODO: remove mint and premint all tokens
528            let global_id =
529                NonFungibleGlobalId::package_of_direct_caller_badge(CONSENSUS_MANAGER_PACKAGE);
530            let consensus_manager_address =
531                api.get_reservation_address(consensus_manager_address_reservation.0.as_node_id())?;
532
533            ResourceManager::new_non_fungible::<ValidatorOwnerBadgeData, _, _, _>(
534                OwnerRole::Fixed(rule!(require(global_caller(consensus_manager_address)))),
535                NonFungibleIdType::Bytes,
536                true,
537                NonFungibleResourceRoles {
538                    mint_roles: mint_roles! {
539                        minter => rule!(require(global_id));
540                        minter_updater => rule!(deny_all);
541                    },
542                    ..Default::default()
543                },
544                metadata_init! {
545                    "name" => "Validator Owner Badges".to_owned(), locked;
546                    "description" => "Badges created by the Radix system that provide individual control over the validator components created for validator node-runners.".to_owned(), locked;
547                    "tags" => vec!["badge".to_owned(), "validator".to_owned()], locked;
548                    "icon_url" => UncheckedUrl::of("https://assets.radixdlt.com/icons/icon-validator_owner_badge.png".to_owned()), locked;
549                },
550                Some(validator_token_address_reservation),
551                api,
552            )?;
553        };
554
555        let consensus_manager_id = {
556            let config = ConsensusManagerConfigSubstate {
557                config: initial_config,
558            };
559            let consensus_manager = ConsensusManagerSubstate {
560                started: false,
561                epoch: genesis_epoch,
562                actual_epoch_start_milli: initial_time_milli,
563                effective_epoch_start_milli: initial_time_milli,
564                round: Round::zero(),
565                current_leader: initial_current_leader,
566            };
567            let validator_rewards = ValidatorRewardsSubstate {
568                proposer_rewards: index_map_new(),
569                rewards_vault: Vault::create(XRD, api)?,
570            };
571            let current_validator_set = CurrentValidatorSetSubstate {
572                validator_set: ActiveValidatorSet {
573                    validators_by_stake_desc: index_map_new(),
574                },
575            };
576            let current_proposal_statistic = CurrentProposalStatisticSubstate {
577                validator_statistics: Vec::new(),
578            };
579            let minute_timestamp = ProposerMinuteTimestampSubstate {
580                epoch_minute: Self::milli_to_minute(initial_time_milli).ok_or(
581                    RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
582                        ConsensusManagerError::InvalidConsensusTime(initial_time_milli),
583                    )),
584                )?,
585            };
586            let milli_timestamp = ProposerMilliTimestampSubstate {
587                epoch_milli: initial_time_milli,
588            };
589
590            api.new_simple_object(
591                CONSENSUS_MANAGER_BLUEPRINT,
592                indexmap! {
593                    ConsensusManagerField::Configuration.field_index() => FieldValue::immutable(&ConsensusManagerConfigurationFieldPayload::from_content_source(config)),
594                    ConsensusManagerField::State.field_index() => FieldValue::new(&ConsensusManagerStateFieldPayload::from_content_source(consensus_manager)),
595                    ConsensusManagerField::ValidatorRewards.field_index() => FieldValue::new(&ConsensusManagerValidatorRewardsFieldPayload::from_content_source(validator_rewards)),
596                    ConsensusManagerField::CurrentValidatorSet.field_index() => FieldValue::new(&ConsensusManagerCurrentValidatorSetFieldPayload::from_content_source(current_validator_set)),
597                    ConsensusManagerField::CurrentProposalStatistic.field_index() => FieldValue::new(&ConsensusManagerCurrentProposalStatisticFieldPayload::from_content_source(current_proposal_statistic)),
598                    ConsensusManagerField::ProposerMinuteTimestamp.field_index() => FieldValue::new(&ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source(minute_timestamp)),
599                    ConsensusManagerField::ProposerMilliTimestamp.field_index() => FieldValue::new(&ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source(milli_timestamp)),
600                },
601            )?
602        };
603
604        let role_definitions = roles2! {
605            VALIDATOR_ROLE => rule!(require(system_execution(SystemExecution::Validator)));
606        };
607
608        let roles = indexmap!(ModuleId::Main => role_definitions);
609        let role_assignment = RoleAssignment::create(OwnerRole::None, roles, api)?.0;
610        let metadata = Metadata::create_with_data(
611            metadata_init! {
612                "name" => "Consensus Manager".to_owned(), locked;
613                "description" => "A component that keeps track of various consensus related concepts such as the epoch, round, current validator set, and so on.".to_owned(), locked;
614            },
615            api,
616        )?;
617
618        api.globalize(
619            consensus_manager_id,
620            indexmap!(
621                AttachedModuleId::RoleAssignment => role_assignment.0,
622                AttachedModuleId::Metadata => metadata.0,
623            ),
624            Some(consensus_manager_address_reservation),
625        )?;
626
627        Ok(())
628    }
629
630    pub(crate) fn get_current_epoch<Y: SystemApi<RuntimeError>>(
631        api: &mut Y,
632    ) -> Result<Epoch, RuntimeError> {
633        let handle = api.actor_open_field(
634            ACTOR_STATE_SELF,
635            ConsensusManagerField::State.into(),
636            LockFlags::read_only(),
637        )?;
638
639        let consensus_manager = api
640            .field_read_typed::<ConsensusManagerStateFieldPayload>(handle)?
641            .fully_update_and_into_latest_version();
642
643        Ok(consensus_manager.epoch)
644    }
645
646    pub(crate) fn start<Y: SystemApi<RuntimeError>>(api: &mut Y) -> Result<(), RuntimeError> {
647        let config_substate = {
648            let config_handle = api.actor_open_field(
649                ACTOR_STATE_SELF,
650                ConsensusManagerField::Configuration.into(),
651                LockFlags::read_only(),
652            )?;
653            let config_substate = api
654                .field_read_typed::<ConsensusManagerConfigurationFieldPayload>(config_handle)?
655                .fully_update_and_into_latest_version();
656            api.field_close(config_handle)?;
657            config_substate
658        };
659
660        let manager_handle = api.actor_open_field(
661            ACTOR_STATE_SELF,
662            ConsensusManagerField::State.into(),
663            LockFlags::MUTABLE,
664        )?;
665        let mut manager_substate = api
666            .field_read_typed::<ConsensusManagerStateFieldPayload>(manager_handle)?
667            .fully_update_and_into_latest_version();
668
669        if manager_substate.started {
670            return Err(RuntimeError::ApplicationError(
671                ApplicationError::ConsensusManagerError(ConsensusManagerError::AlreadyStarted),
672            ));
673        }
674        let post_genesis_epoch =
675            manager_substate
676                .epoch
677                .next()
678                .ok_or(RuntimeError::ApplicationError(
679                    ApplicationError::ConsensusManagerError(
680                        ConsensusManagerError::EpochMathOverflow,
681                    ),
682                ))?;
683
684        Self::epoch_change(post_genesis_epoch, &config_substate.config, api)?;
685        manager_substate.started = true;
686        manager_substate.epoch = post_genesis_epoch;
687        manager_substate.round = Round::zero();
688
689        api.field_write_typed(
690            manager_handle,
691            &ConsensusManagerStateFieldPayload::from_content_source(manager_substate),
692        )?;
693        api.field_close(manager_handle)?;
694
695        Ok(())
696    }
697
698    pub(crate) fn get_current_time_v1<Y: SystemApi<RuntimeError>>(
699        precision: TimePrecisionV1,
700        api: &mut Y,
701    ) -> Result<Instant, RuntimeError> {
702        match precision {
703            TimePrecisionV1::Minute => {
704                let handle = api.actor_open_field(
705                    ACTOR_STATE_SELF,
706                    ConsensusManagerField::ProposerMinuteTimestamp.into(),
707                    LockFlags::read_only(),
708                )?;
709                let proposer_minute_timestamp = api
710                    .field_read_typed::<ConsensusManagerProposerMinuteTimestampFieldPayload>(
711                        handle,
712                    )?
713                    .fully_update_and_into_latest_version();
714                api.field_close(handle)?;
715
716                Ok(Self::epoch_minute_to_instant(
717                    proposer_minute_timestamp.epoch_minute,
718                ))
719            }
720        }
721    }
722
723    pub(crate) fn get_current_time_v2<Y: SystemApi<RuntimeError>>(
724        precision: TimePrecisionV2,
725        api: &mut Y,
726    ) -> Result<Instant, RuntimeError> {
727        match precision {
728            TimePrecisionV2::Minute => {
729                let handle = api.actor_open_field(
730                    ACTOR_STATE_SELF,
731                    ConsensusManagerField::ProposerMinuteTimestamp.into(),
732                    LockFlags::read_only(),
733                )?;
734                let proposer_minute_timestamp = api
735                    .field_read_typed::<ConsensusManagerProposerMinuteTimestampFieldPayload>(
736                        handle,
737                    )?
738                    .fully_update_and_into_latest_version();
739                api.field_close(handle)?;
740
741                Ok(Self::epoch_minute_to_instant(
742                    proposer_minute_timestamp.epoch_minute,
743                ))
744            }
745            TimePrecisionV2::Second => {
746                let handle = api.actor_open_field(
747                    ACTOR_STATE_SELF,
748                    ConsensusManagerField::ProposerMilliTimestamp.into(),
749                    LockFlags::read_only(),
750                )?;
751                let proposer_milli_timestamp = api
752                    .field_read_typed::<ConsensusManagerProposerMilliTimestampFieldPayload>(handle)?
753                    .fully_update_and_into_latest_version();
754                api.field_close(handle)?;
755
756                Ok(Self::epoch_milli_to_instant(
757                    proposer_milli_timestamp.epoch_milli,
758                ))
759            }
760        }
761    }
762
763    pub(crate) fn compare_current_time_v1<Y: SystemApi<RuntimeError>>(
764        other_arbitrary_precision_instant: Instant,
765        precision: TimePrecisionV1,
766        operator: TimeComparisonOperator,
767        api: &mut Y,
768    ) -> Result<bool, RuntimeError> {
769        match precision {
770            TimePrecisionV1::Minute => {
771                let other_epoch_minute = other_arbitrary_precision_instant
772                    .seconds_since_unix_epoch
773                    .checked_mul(MILLIS_IN_SECOND)
774                    .and_then(|result| Self::milli_to_minute(result))
775                    .unwrap_or_else(|| {
776                        // This is to deal with overflows, i32 MAX and MIN values should work with current time
777                        if other_arbitrary_precision_instant
778                            .seconds_since_unix_epoch
779                            .is_negative()
780                        {
781                            i32::MIN
782                        } else {
783                            i32::MAX
784                        }
785                    });
786
787                let handle = api.actor_open_field(
788                    ACTOR_STATE_SELF,
789                    ConsensusManagerField::ProposerMinuteTimestamp.into(),
790                    LockFlags::read_only(),
791                )?;
792                let proposer_minute_timestamp = api
793                    .field_read_typed::<ConsensusManagerProposerMinuteTimestampFieldPayload>(
794                        handle,
795                    )?
796                    .fully_update_and_into_latest_version();
797                api.field_close(handle)?;
798
799                // convert back to Instant only for comparison operation
800                let proposer_instant =
801                    Self::epoch_minute_to_instant(proposer_minute_timestamp.epoch_minute);
802                let other_instant = Self::epoch_minute_to_instant(other_epoch_minute);
803                let result = proposer_instant.compare(other_instant, operator);
804                Ok(result)
805            }
806        }
807    }
808
809    pub(crate) fn compare_current_time_v2<Y: SystemApi<RuntimeError>>(
810        other_arbitrary_precision_instant: Instant,
811        precision: TimePrecisionV2,
812        operator: TimeComparisonOperator,
813        api: &mut Y,
814    ) -> Result<bool, RuntimeError> {
815        match precision {
816            TimePrecisionV2::Minute => {
817                let other_epoch_minute = other_arbitrary_precision_instant
818                    .seconds_since_unix_epoch
819                    .checked_mul(MILLIS_IN_SECOND)
820                    .and_then(|result| Self::milli_to_minute(result))
821                    .unwrap_or_else(|| {
822                        // This is to deal with overflows, i32 MAX and MIN values should work with current time
823                        if other_arbitrary_precision_instant
824                            .seconds_since_unix_epoch
825                            .is_negative()
826                        {
827                            i32::MIN
828                        } else {
829                            i32::MAX
830                        }
831                    });
832
833                let handle = api.actor_open_field(
834                    ACTOR_STATE_SELF,
835                    ConsensusManagerField::ProposerMinuteTimestamp.into(),
836                    LockFlags::read_only(),
837                )?;
838                let proposer_minute_timestamp = api
839                    .field_read_typed::<ConsensusManagerProposerMinuteTimestampFieldPayload>(
840                        handle,
841                    )?
842                    .fully_update_and_into_latest_version();
843                api.field_close(handle)?;
844
845                // convert back to Instant only for comparison operation
846                let proposer_instant =
847                    Self::epoch_minute_to_instant(proposer_minute_timestamp.epoch_minute);
848                let other_instant = Self::epoch_minute_to_instant(other_epoch_minute);
849                let result = proposer_instant.compare(other_instant, operator);
850                Ok(result)
851            }
852
853            TimePrecisionV2::Second => {
854                let other_epoch_second = other_arbitrary_precision_instant.seconds_since_unix_epoch;
855
856                let handle = api.actor_open_field(
857                    ACTOR_STATE_SELF,
858                    ConsensusManagerField::ProposerMilliTimestamp.into(),
859                    LockFlags::read_only(),
860                )?;
861                let proposer_milli_timestamp = api
862                    .field_read_typed::<ConsensusManagerProposerMilliTimestampFieldPayload>(handle)?
863                    .fully_update_and_into_latest_version();
864                api.field_close(handle)?;
865
866                // convert back to Instant only for comparison operation
867                let proposer_instant =
868                    Self::epoch_milli_to_instant(proposer_milli_timestamp.epoch_milli);
869                let other_instant = Instant::new(other_epoch_second);
870                let result = proposer_instant.compare(other_instant, operator);
871                Ok(result)
872            }
873        }
874    }
875
876    fn epoch_minute_to_instant(epoch_minute: i32) -> Instant {
877        Instant::new(epoch_minute as i64 * SECONDS_IN_MINUTE)
878    }
879
880    fn epoch_milli_to_instant(epoch_milli: i64) -> Instant {
881        Instant::new(epoch_milli / MILLIS_IN_SECOND)
882    }
883
884    fn milli_to_minute(epoch_milli: i64) -> Option<i32> {
885        i32::try_from(epoch_milli / MILLIS_IN_MINUTE).ok() // safe until A.D. 5700
886    }
887
888    pub(crate) fn next_round<Y: SystemApi<RuntimeError>>(
889        round: Round,
890        proposer_timestamp_milli: i64,
891        proposal_history: LeaderProposalHistory,
892        api: &mut Y,
893    ) -> Result<(), RuntimeError> {
894        Self::check_non_decreasing_and_update_timestamps(proposer_timestamp_milli, api)?;
895
896        let config_handle = api.actor_open_field(
897            ACTOR_STATE_SELF,
898            ConsensusManagerField::Configuration.into(),
899            LockFlags::read_only(),
900        )?;
901        let config_substate = api
902            .field_read_typed::<ConsensusManagerConfigurationFieldPayload>(config_handle)?
903            .fully_update_and_into_latest_version();
904        api.field_close(config_handle)?;
905
906        let manager_handle = api.actor_open_field(
907            ACTOR_STATE_SELF,
908            ConsensusManagerField::State.into(),
909            LockFlags::MUTABLE,
910        )?;
911        let mut manager_substate = api
912            .field_read_typed::<ConsensusManagerStateFieldPayload>(manager_handle)?
913            .fully_update_and_into_latest_version();
914
915        let progressed_rounds = Round::calculate_progress(manager_substate.round, round)
916            .ok_or_else(|| {
917                RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
918                    ConsensusManagerError::InvalidRoundUpdate {
919                        from: manager_substate.round,
920                        to: round,
921                    },
922                ))
923            })?;
924
925        let current_leader = proposal_history.current_leader;
926        Self::update_proposal_statistics(progressed_rounds, proposal_history, api)?;
927
928        let config = &config_substate.config;
929        let should_epoch_change = config.epoch_change_condition.should_epoch_change(
930            manager_substate.effective_epoch_start_milli,
931            proposer_timestamp_milli,
932            round,
933        );
934        match should_epoch_change {
935            EpochChangeOutcome::NoChange => {
936                Runtime::emit_event(api, RoundChangeEvent { round })?;
937                manager_substate.round = round;
938            }
939            EpochChangeOutcome::Change {
940                next_epoch_effective_start_millis: next_epoch_effective_start,
941            } => {
942                let next_epoch =
943                    manager_substate
944                        .epoch
945                        .next()
946                        .ok_or(RuntimeError::ApplicationError(
947                            ApplicationError::ConsensusManagerError(
948                                ConsensusManagerError::EpochMathOverflow,
949                            ),
950                        ))?;
951                Self::epoch_change(next_epoch, config, api)?;
952                manager_substate.epoch = next_epoch;
953                manager_substate.round = Round::zero();
954                manager_substate.actual_epoch_start_milli = proposer_timestamp_milli;
955                manager_substate.effective_epoch_start_milli = next_epoch_effective_start;
956            }
957        }
958        manager_substate.current_leader = Some(current_leader);
959
960        api.field_write_typed(
961            manager_handle,
962            &ConsensusManagerStateFieldPayload::from_content_source(manager_substate),
963        )?;
964        api.field_close(manager_handle)?;
965
966        Ok(())
967    }
968
969    fn get_validator_xrd_cost<Y: SystemApi<RuntimeError>>(
970        api: &mut Y,
971    ) -> Result<Option<Decimal>, RuntimeError> {
972        let manager_handle = api.actor_open_field(
973            ACTOR_STATE_SELF,
974            ConsensusManagerField::State.field_index(),
975            LockFlags::read_only(),
976        )?;
977        let manager_substate =
978            api.field_read_typed::<ConsensusManagerStateFieldPayload>(manager_handle)?;
979        let manager_substate = manager_substate.fully_update_and_into_latest_version();
980
981        let validator_creation_xrd_cost = if manager_substate.started {
982            let config_handle = api.actor_open_field(
983                ACTOR_STATE_SELF,
984                ConsensusManagerField::Configuration.into(),
985                LockFlags::read_only(),
986            )?;
987            let manager_config: ConsensusManagerConfigurationFieldPayload =
988                api.field_read_typed(config_handle)?;
989            api.field_close(config_handle)?;
990
991            let validator_creation_xrd_cost = manager_config
992                .fully_update_and_into_latest_version()
993                .config
994                .validator_creation_usd_cost
995                .checked_mul(api.usd_price()?)
996                .ok_or(RuntimeError::ApplicationError(
997                    ApplicationError::ConsensusManagerError(
998                        ConsensusManagerError::UnexpectedDecimalComputationError,
999                    ),
1000                ))?;
1001            Some(validator_creation_xrd_cost)
1002        } else {
1003            None
1004        };
1005
1006        api.field_close(manager_handle)?;
1007
1008        Ok(validator_creation_xrd_cost)
1009    }
1010
1011    pub(crate) fn create_validator<Y: SystemApi<RuntimeError>>(
1012        key: Secp256k1PublicKey,
1013        fee_factor: Decimal,
1014        xrd_payment: Bucket,
1015        api: &mut Y,
1016    ) -> Result<(ComponentAddress, Bucket, Bucket), RuntimeError> {
1017        if !xrd_payment.resource_address(api)?.eq(&XRD) {
1018            return Err(RuntimeError::ApplicationError(
1019                ApplicationError::ConsensusManagerError(ConsensusManagerError::NotXrd),
1020            ));
1021        }
1022
1023        let validator_xrd_cost = Self::get_validator_xrd_cost(api)?;
1024        if let Some(xrd_cost) = validator_xrd_cost {
1025            let xrd_paid = xrd_payment.take(xrd_cost, api)?;
1026            xrd_paid.burn(api)?;
1027        }
1028
1029        let (validator_address, owner_token_bucket) =
1030            ValidatorCreator::create(key, false, fee_factor, api)?;
1031
1032        Ok((validator_address, owner_token_bucket, xrd_payment))
1033    }
1034
1035    fn check_non_decreasing_and_update_timestamps<Y: SystemApi<RuntimeError>>(
1036        current_time_ms: i64,
1037        api: &mut Y,
1038    ) -> Result<(), RuntimeError> {
1039        let handle = api.actor_open_field(
1040            ACTOR_STATE_SELF,
1041            ConsensusManagerField::ProposerMilliTimestamp.into(),
1042            LockFlags::MUTABLE,
1043        )?;
1044        let exact_time_substate: ConsensusManagerProposerMilliTimestampFieldPayload =
1045            api.field_read_typed(handle)?;
1046        let mut exact_time_substate = exact_time_substate.fully_update_and_into_latest_version();
1047        let previous_timestamp = exact_time_substate.epoch_milli;
1048        if current_time_ms < previous_timestamp {
1049            return Err(RuntimeError::ApplicationError(
1050                ApplicationError::ConsensusManagerError(
1051                    ConsensusManagerError::InvalidProposerTimestampUpdate {
1052                        from_millis: previous_timestamp,
1053                        to_millis: current_time_ms,
1054                    },
1055                ),
1056            ));
1057        } else if current_time_ms > previous_timestamp {
1058            exact_time_substate.epoch_milli = current_time_ms;
1059            api.field_write_typed(
1060                handle,
1061                &ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source(
1062                    exact_time_substate,
1063                ),
1064            )?;
1065        }
1066        api.field_close(handle)?;
1067
1068        let new_rounded_value = Self::milli_to_minute(current_time_ms).ok_or(
1069            RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
1070                ConsensusManagerError::InvalidConsensusTime(current_time_ms),
1071            )),
1072        )?;
1073        let handle = api.actor_open_field(
1074            ACTOR_STATE_SELF,
1075            ConsensusManagerField::ProposerMinuteTimestamp.into(),
1076            LockFlags::MUTABLE,
1077        )?;
1078        let rounded_timestamp_substate: ConsensusManagerProposerMinuteTimestampFieldPayload =
1079            api.field_read_typed(handle)?;
1080        let mut rounded_timestamp_substate =
1081            rounded_timestamp_substate.fully_update_and_into_latest_version();
1082        let previous_rounded_value = rounded_timestamp_substate.epoch_minute;
1083        if new_rounded_value > previous_rounded_value {
1084            rounded_timestamp_substate.epoch_minute = new_rounded_value;
1085            api.field_write_typed(
1086                handle,
1087                &ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source(
1088                    rounded_timestamp_substate,
1089                ),
1090            )?;
1091        }
1092        api.field_close(handle)?;
1093
1094        Ok(())
1095    }
1096
1097    fn update_proposal_statistics<Y: SystemApi<RuntimeError>>(
1098        progressed_rounds: u64,
1099        proposal_history: LeaderProposalHistory,
1100        api: &mut Y,
1101    ) -> Result<(), RuntimeError> {
1102        if proposal_history.gap_round_leaders.len() as u64 != progressed_rounds - 1 {
1103            return Err(RuntimeError::ApplicationError(
1104                ApplicationError::ConsensusManagerError(
1105                    ConsensusManagerError::InconsistentGapRounds {
1106                        gap_rounds: proposal_history.gap_round_leaders.len(),
1107                        progressed_rounds,
1108                    },
1109                ),
1110            ));
1111        }
1112
1113        let statistic_handle = api.actor_open_field(
1114            ACTOR_STATE_SELF,
1115            ConsensusManagerField::CurrentProposalStatistic.into(),
1116            LockFlags::MUTABLE,
1117        )?;
1118        let statistic: ConsensusManagerCurrentProposalStatisticFieldPayload =
1119            api.field_read_typed(statistic_handle)?;
1120        let mut statistic = statistic.fully_update_and_into_latest_version();
1121        for gap_round_leader in proposal_history.gap_round_leaders {
1122            let gap_round_statistic = statistic.get_mut_proposal_statistic(gap_round_leader)?;
1123            gap_round_statistic.missed += 1;
1124        }
1125        let current_round_statistic =
1126            statistic.get_mut_proposal_statistic(proposal_history.current_leader)?;
1127        if proposal_history.is_fallback {
1128            current_round_statistic.missed += 1;
1129        } else {
1130            current_round_statistic.made += 1;
1131        }
1132        api.field_write_typed(
1133            statistic_handle,
1134            &ConsensusManagerCurrentProposalStatisticFieldPayload::from_content_source(statistic),
1135        )?;
1136        api.field_close(statistic_handle)?;
1137
1138        Ok(())
1139    }
1140
1141    fn epoch_change<Y: SystemApi<RuntimeError>>(
1142        next_epoch: Epoch,
1143        config: &ConsensusManagerConfig,
1144        api: &mut Y,
1145    ) -> Result<(), RuntimeError> {
1146        // Read previous validator set
1147        let validator_set_handle = api.actor_open_field(
1148            ACTOR_STATE_SELF,
1149            ConsensusManagerField::CurrentValidatorSet.into(),
1150            LockFlags::MUTABLE,
1151        )?;
1152        let validator_set_substate: ConsensusManagerCurrentValidatorSetFieldPayload =
1153            api.field_read_typed(validator_set_handle)?;
1154        let mut validator_set_substate =
1155            validator_set_substate.fully_update_and_into_latest_version();
1156        let previous_validator_set = validator_set_substate.validator_set;
1157
1158        // Read previous validator statistics
1159        let statistic_handle = api.actor_open_field(
1160            ACTOR_STATE_SELF,
1161            ConsensusManagerField::CurrentProposalStatistic.into(),
1162            LockFlags::MUTABLE,
1163        )?;
1164        let statistic_substate: ConsensusManagerCurrentProposalStatisticFieldPayload =
1165            api.field_read_typed(statistic_handle)?;
1166        let mut statistic_substate = statistic_substate.fully_update_and_into_latest_version();
1167        let previous_statistics = statistic_substate.validator_statistics;
1168
1169        // Read & write validator rewards
1170        let rewards_handle = api.actor_open_field(
1171            ACTOR_STATE_SELF,
1172            ConsensusManagerField::ValidatorRewards.into(),
1173            LockFlags::MUTABLE,
1174        )?;
1175        let mut rewards_substate = api
1176            .field_read_typed::<ConsensusManagerValidatorRewardsFieldPayload>(rewards_handle)?
1177            .fully_update_and_into_latest_version();
1178
1179        // Apply emissions
1180        Self::apply_validator_emissions_and_rewards(
1181            previous_validator_set,
1182            previous_statistics,
1183            config,
1184            &mut rewards_substate,
1185            next_epoch.previous().ok_or(RuntimeError::ApplicationError(
1186                ApplicationError::ConsensusManagerError(ConsensusManagerError::EpochMathOverflow),
1187            ))?,
1188            api,
1189        )?;
1190
1191        // Select next validator set
1192        // NOTE - because the stake index is by u16 buckets, it's possible that there are multiple validators at the cut off point
1193        // that fall into the same bucket.
1194        // To reduce the risk of that causing issues, we take a decent chunk more than we need from the index.
1195        // It's still possible that the bucket is _very_ large and we miss some validators in the bucket, and fail to read validators
1196        // with a higher stake, but lower DbSortKey.
1197        // The risk is very low though in practice, and only affects validators near the bottom of the list who would likely get very
1198        // few proposals, so we feel it's an okay trade-off.
1199        let num_validators_to_read_from_store =
1200            config.max_validators + (config.max_validators / 10) + 10;
1201
1202        let mut top_registered_validators: Vec<(
1203            ComponentAddress,
1204            ConsensusManagerRegisteredValidatorByStakeEntryPayload,
1205        )> = api.actor_sorted_index_scan_typed(
1206            ACTOR_STATE_SELF,
1207            ConsensusManagerCollection::RegisteredValidatorByStakeSortedIndex.collection_index(),
1208            num_validators_to_read_from_store,
1209        )?;
1210
1211        // The index scan should already pull the validators out in stake DESC, but if multiple validators are on the same u16 stake,
1212        // then let's be even more accurate here. This sort is stable, so if two validators tie, then the resultant order will be
1213        // decided on sort key DESC.
1214        top_registered_validators.sort_by(|(_, validator_1), (_, validator_2)| {
1215            let validator1 = validator_1.as_unique_version();
1216            let validator2 = validator_2.as_unique_version();
1217            validator1.stake.cmp(&validator2.stake).reverse()
1218        });
1219
1220        let next_active_validator_set = ActiveValidatorSet {
1221            validators_by_stake_desc: top_registered_validators
1222                .into_iter()
1223                .take(config.max_validators as usize)
1224                .map(|(component_address, validator)| {
1225                    (
1226                        component_address,
1227                        validator.fully_update_and_into_latest_version(),
1228                    )
1229                })
1230                .collect(),
1231        };
1232
1233        let mut next_validator_set_total_stake = Decimal::zero();
1234        let mut significant_protocol_update_readiness: IndexMap<String, Decimal> = index_map_new();
1235        for (validator_address, validator) in
1236            next_active_validator_set.validators_by_stake_desc.iter()
1237        {
1238            next_validator_set_total_stake = next_validator_set_total_stake
1239                .checked_add(validator.stake)
1240                .ok_or(RuntimeError::ApplicationError(
1241                    ApplicationError::ConsensusManagerError(
1242                        ConsensusManagerError::UnexpectedDecimalComputationError,
1243                    ),
1244                ))?;
1245            let rtn = api.call_method(
1246                validator_address.as_node_id(),
1247                VALIDATOR_GET_PROTOCOL_UPDATE_READINESS_IDENT,
1248                scrypto_encode(&ValidatorGetProtocolUpdateReadinessInput {}).unwrap(),
1249            )?;
1250            if let Some(protocol_update_readiness) = scrypto_decode::<Option<String>>(&rtn).unwrap()
1251            {
1252                let entry = significant_protocol_update_readiness
1253                    .entry(protocol_update_readiness)
1254                    .or_insert(Decimal::zero());
1255                *entry =
1256                    entry
1257                        .checked_add(validator.stake)
1258                        .ok_or(RuntimeError::ApplicationError(
1259                            ApplicationError::ConsensusManagerError(
1260                                ConsensusManagerError::UnexpectedDecimalComputationError,
1261                            ),
1262                        ))?;
1263            }
1264        }
1265
1266        // Only store protocol updates that have been signalled by at
1267        // least 10% of the new epoch's validator set total stake.
1268        let significant_protocol_update_readiness_stake_threshold = next_validator_set_total_stake
1269            .checked_mul(dec!("0.1"))
1270            .ok_or(RuntimeError::ApplicationError(
1271                ApplicationError::ConsensusManagerError(
1272                    ConsensusManagerError::UnexpectedDecimalComputationError,
1273                ),
1274            ))?;
1275        significant_protocol_update_readiness.retain(|_, stake_signalled| {
1276            *stake_signalled >= significant_protocol_update_readiness_stake_threshold
1277        });
1278
1279        // Emit epoch change event
1280        Runtime::emit_event(
1281            api,
1282            EpochChangeEvent {
1283                epoch: next_epoch,
1284                validator_set: next_active_validator_set.clone(),
1285                significant_protocol_update_readiness,
1286            },
1287        )?;
1288
1289        // Write updated validator rewards
1290        api.field_write_typed(
1291            rewards_handle,
1292            &ConsensusManagerValidatorRewardsFieldPayload::from_content_source(rewards_substate),
1293        )?;
1294        api.field_close(rewards_handle)?;
1295
1296        // Write zeroed statistics of next validators
1297        statistic_substate.validator_statistics = (0..next_active_validator_set.validator_count())
1298            .map(|_index| ProposalStatistic::default())
1299            .collect();
1300        api.field_write_typed(
1301            statistic_handle,
1302            &ConsensusManagerCurrentProposalStatisticFieldPayload::from_content_source(
1303                statistic_substate,
1304            ),
1305        )?;
1306        api.field_close(statistic_handle)?;
1307
1308        // Write next validator set
1309        validator_set_substate.validator_set = next_active_validator_set;
1310        api.field_write_typed(
1311            validator_set_handle,
1312            &ConsensusManagerCurrentValidatorSetFieldPayload::from_content_source(
1313                validator_set_substate,
1314            ),
1315        )?;
1316        api.field_close(validator_set_handle)?;
1317
1318        Ok(())
1319    }
1320
1321    /// Emits a configured XRD amount ([`ConsensusManagerConfigSubstate.total_emission_xrd_per_epoch`])
1322    /// and distributes it across the given validator set, according to their stake.
1323    fn apply_validator_emissions_and_rewards<Y: SystemApi<RuntimeError>>(
1324        validator_set: ActiveValidatorSet,
1325        validator_statistics: Vec<ProposalStatistic>,
1326        config: &ConsensusManagerConfig,
1327        validator_rewards: &mut ValidatorRewardsSubstate,
1328        epoch: Epoch, // the concluded epoch, for event creation
1329        api: &mut Y,
1330    ) -> Result<(), RuntimeError> {
1331        let mut stake_sum_xrd = Decimal::ZERO;
1332
1333        let mut validator_infos: IndexMap<ValidatorIndex, ValidatorInfo> = index_map_new();
1334        for (index, (address, validator)) in validator_set
1335            .validators_by_stake_desc
1336            .into_iter()
1337            .enumerate()
1338        {
1339            if let Some(info) = ValidatorInfo::create_if_applicable(
1340                address,
1341                validator.stake,
1342                validator_statistics[index].clone(),
1343                config.min_validator_reliability,
1344            )? {
1345                stake_sum_xrd = stake_sum_xrd.checked_add(info.stake_xrd).ok_or(
1346                    RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
1347                        ConsensusManagerError::UnexpectedDecimalComputationError,
1348                    )),
1349                )?;
1350
1351                validator_infos.insert(
1352                    TryInto::<ValidatorIndex>::try_into(index)
1353                        // Should never happen. We made sure no more than u8::MAX validators are stored
1354                        .expect("Validator index exceeds the range of u8"),
1355                    info,
1356                );
1357            } else {
1358                // Excluded due to slashing ?
1359            }
1360        }
1361        if validator_infos.is_empty() {
1362            return Ok(());
1363        }
1364
1365        //======================
1366        // Distribute emissions
1367        //======================
1368
1369        // calculate "how much XRD is emitted by 1 XRD staked", and later apply it evenly among validators
1370        // (the gains are slightly rounded down, but more fairly distributed - not affected by different rounding errors for different validators)
1371        let emission_per_staked_xrd = config
1372            .total_emission_xrd_per_epoch
1373            .checked_div(stake_sum_xrd)
1374            .ok_or(RuntimeError::ApplicationError(
1375                ApplicationError::ConsensusManagerError(
1376                    ConsensusManagerError::UnexpectedDecimalComputationError,
1377                ),
1378            ))?;
1379        let effective_total_emission_xrd = {
1380            let mut sum = Decimal::ZERO;
1381
1382            for v in validator_infos.values() {
1383                let emission = v
1384                    .effective_stake_xrd
1385                    .checked_mul(emission_per_staked_xrd)
1386                    .ok_or(RuntimeError::ApplicationError(
1387                        ApplicationError::ConsensusManagerError(
1388                            ConsensusManagerError::UnexpectedDecimalComputationError,
1389                        ),
1390                    ))?;
1391                sum = sum
1392                    .checked_add(emission)
1393                    .ok_or(RuntimeError::ApplicationError(
1394                        ApplicationError::ConsensusManagerError(
1395                            ConsensusManagerError::UnexpectedDecimalComputationError,
1396                        ),
1397                    ))?;
1398            }
1399            sum
1400        };
1401
1402        let total_emission_xrd_bucket =
1403            ResourceManager(XRD).mint_fungible(effective_total_emission_xrd, api)?;
1404
1405        for validator_info in validator_infos.values() {
1406            let emission_xrd_bucket = total_emission_xrd_bucket.take(
1407                validator_info
1408                    .effective_stake_xrd
1409                    .checked_mul(emission_per_staked_xrd)
1410                    .ok_or(RuntimeError::ApplicationError(
1411                        ApplicationError::ConsensusManagerError(
1412                            ConsensusManagerError::UnexpectedDecimalComputationError,
1413                        ),
1414                    ))?,
1415                api,
1416            )?;
1417            api.call_method(
1418                validator_info.address.as_node_id(),
1419                VALIDATOR_APPLY_EMISSION_IDENT,
1420                scrypto_encode(&ValidatorApplyEmissionInput {
1421                    xrd_bucket: emission_xrd_bucket.into(),
1422                    epoch,
1423                    proposals_made: validator_info.proposal_statistic.made,
1424                    proposals_missed: validator_info.proposal_statistic.missed,
1425                })
1426                .unwrap(),
1427            )?;
1428        }
1429        total_emission_xrd_bucket.drop_empty(api)?;
1430
1431        //===========================
1432        // Distribute rewards (fees)
1433        //===========================
1434        let mut total_effective_stake = Decimal::ZERO;
1435        let mut total_claimable_proposer_rewards = Decimal::ZERO;
1436
1437        // Note that `validator_infos` are for applicable validators (i.e. stake > 0) only
1438        // Being an applicable validator doesn't necessarily mean the effective stake is positive, due to reliability rescaling.
1439        for (index, validator_info) in &validator_infos {
1440            total_effective_stake = total_effective_stake
1441                .checked_add(validator_info.effective_stake_xrd)
1442                .ok_or(RuntimeError::ApplicationError(
1443                    ApplicationError::ConsensusManagerError(
1444                        ConsensusManagerError::UnexpectedDecimalComputationError,
1445                    ),
1446                ))?;
1447            total_claimable_proposer_rewards = total_claimable_proposer_rewards
1448                .checked_add(
1449                    validator_rewards
1450                        .proposer_rewards
1451                        .get(index)
1452                        .cloned()
1453                        .unwrap_or_default(),
1454                )
1455                .ok_or(RuntimeError::ApplicationError(
1456                    ApplicationError::ConsensusManagerError(
1457                        ConsensusManagerError::UnexpectedDecimalComputationError,
1458                    ),
1459                ))?;
1460        }
1461
1462        let total_claimable_validator_set_rewards = validator_rewards
1463            .rewards_vault
1464            .amount(api)?
1465            .checked_sub(total_claimable_proposer_rewards)
1466            .ok_or(RuntimeError::ApplicationError(
1467                ApplicationError::ConsensusManagerError(
1468                    ConsensusManagerError::UnexpectedDecimalComputationError,
1469                ),
1470            ))?;
1471        let reward_per_effective_stake = if total_effective_stake.is_zero() {
1472            // This is another extreme use case.
1473            // Can the network even progress if total effective stake is zero?
1474            Decimal::ZERO
1475        } else {
1476            total_claimable_validator_set_rewards
1477                .checked_div(total_effective_stake)
1478                .ok_or(RuntimeError::ApplicationError(
1479                    ApplicationError::ConsensusManagerError(
1480                        ConsensusManagerError::UnexpectedDecimalComputationError,
1481                    ),
1482                ))?
1483        };
1484
1485        for (index, validator_info) in validator_infos {
1486            let as_proposer = validator_rewards
1487                .proposer_rewards
1488                .swap_remove(&index)
1489                .unwrap_or_default();
1490            let as_member_of_validator_set = validator_info
1491                .effective_stake_xrd
1492                .checked_mul(reward_per_effective_stake)
1493                .ok_or(RuntimeError::ApplicationError(
1494                    ApplicationError::ConsensusManagerError(
1495                        ConsensusManagerError::UnexpectedDecimalComputationError,
1496                    ),
1497                ))?;
1498            let total_rewards = as_proposer.checked_add(as_member_of_validator_set).ok_or(
1499                RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
1500                    ConsensusManagerError::UnexpectedDecimalComputationError,
1501                )),
1502            )?;
1503            if total_rewards.is_zero() {
1504                continue;
1505            }
1506
1507            // Note that dusted xrd (due to rounding) are kept in the vault and will
1508            // become retrievable next time.
1509            let xrd_bucket = validator_rewards.rewards_vault.take(total_rewards, api)?;
1510
1511            api.call_method(
1512                validator_info.address.as_node_id(),
1513                VALIDATOR_APPLY_REWARD_IDENT,
1514                scrypto_encode(&ValidatorApplyRewardInput { xrd_bucket, epoch }).unwrap(),
1515            )?;
1516        }
1517
1518        // For any reason, if a validator isn't included in the `validator_infos` but has accumulated
1519        // proposer rewards, we reset the counter as the rewards has been distributed to other validators.
1520        validator_rewards.proposer_rewards.clear();
1521
1522        Ok(())
1523    }
1524}
1525
1526#[derive(Debug)]
1527struct ValidatorInfo {
1528    pub address: ComponentAddress,
1529    pub stake_xrd: Decimal,
1530    pub effective_stake_xrd: Decimal,
1531    pub proposal_statistic: ProposalStatistic, // needed only for passing the information to event
1532}
1533
1534impl ValidatorInfo {
1535    fn create_if_applicable(
1536        address: ComponentAddress,
1537        stake_xrd: Decimal,
1538        proposal_statistic: ProposalStatistic,
1539        min_required_reliability: Decimal,
1540    ) -> Result<Option<Self>, RuntimeError> {
1541        if stake_xrd.is_positive() {
1542            let reliability_factor = Self::to_reliability_factor(
1543                proposal_statistic.success_ratio()?,
1544                min_required_reliability,
1545            )?;
1546            let effective_stake_xrd =
1547                stake_xrd
1548                    .checked_mul(reliability_factor)
1549                    .ok_or(RuntimeError::ApplicationError(
1550                        ApplicationError::ConsensusManagerError(
1551                            ConsensusManagerError::UnexpectedDecimalComputationError,
1552                        ),
1553                    ))?;
1554            Ok(Some(Self {
1555                address,
1556                stake_xrd,
1557                proposal_statistic,
1558                effective_stake_xrd,
1559            }))
1560        } else {
1561            Ok(None)
1562        }
1563    }
1564
1565    /// Converts the absolute reliability measure (e.g. "0.97 uptime") into a reliability factor
1566    /// which directly drives the fraction of received emission (e.g. "0.25 of base emission"), by
1567    /// rescaling it into the allowed reliability range (e.g. "required >0.96 uptime").
1568    fn to_reliability_factor(
1569        reliability: Decimal,
1570        min_required_reliability: Decimal,
1571    ) -> Result<Decimal, RuntimeError> {
1572        let reliability_reserve = reliability.checked_sub(min_required_reliability).ok_or(
1573            RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
1574                ConsensusManagerError::UnexpectedDecimalComputationError,
1575            )),
1576        )?;
1577        if reliability_reserve.is_negative() {
1578            return Ok(Decimal::zero());
1579        }
1580        let max_allowed_unreliability =
1581            Decimal::one().checked_sub(min_required_reliability).ok_or(
1582                RuntimeError::ApplicationError(ApplicationError::ConsensusManagerError(
1583                    ConsensusManagerError::UnexpectedDecimalComputationError,
1584                )),
1585            )?;
1586        if max_allowed_unreliability.is_zero() {
1587            // special-casing the dirac delta behavior
1588            if reliability == Decimal::one() {
1589                return Ok(Decimal::one());
1590            } else {
1591                return Ok(Decimal::zero());
1592            }
1593        }
1594        Ok(reliability_reserve
1595            .checked_div(max_allowed_unreliability)
1596            .ok_or(RuntimeError::ApplicationError(
1597                ApplicationError::ConsensusManagerError(
1598                    ConsensusManagerError::UnexpectedDecimalComputationError,
1599                ),
1600            ))?)
1601    }
1602}
1603
1604#[cfg(test)]
1605mod tests {
1606    use super::*;
1607
1608    #[test]
1609    fn test_to_reliability_factor() {
1610        let min_required_reliability = dec!("0.8");
1611        assert_eq!(
1612            ValidatorInfo::to_reliability_factor(dec!("0"), min_required_reliability),
1613            Ok(dec!("0"))
1614        );
1615        assert_eq!(
1616            ValidatorInfo::to_reliability_factor(dec!("0.4"), min_required_reliability),
1617            Ok(dec!("0"))
1618        );
1619
1620        // Is the following rescaling desired?
1621        assert_eq!(
1622            ValidatorInfo::to_reliability_factor(dec!("0.8"), min_required_reliability),
1623            Ok(dec!("0"))
1624        );
1625        assert_eq!(
1626            ValidatorInfo::to_reliability_factor(dec!("0.9"), min_required_reliability),
1627            Ok(dec!("0.5"))
1628        );
1629        assert_eq!(
1630            ValidatorInfo::to_reliability_factor(dec!("0.95"), min_required_reliability),
1631            Ok(dec!("0.75"))
1632        );
1633        assert_eq!(
1634            ValidatorInfo::to_reliability_factor(dec!("1"), min_required_reliability),
1635            Ok(dec!("1"))
1636        );
1637    }
1638}