Smart Contract

FlowEpoch

A.8624b52f9ddcd04a.FlowEpoch

Deployed

1h ago
Feb 26, 2026, 10:31:17 PM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import FlowToken from 0x1654653399040a61
3import FlowIDTableStaking from 0x8624b52f9ddcd04a
4import FlowClusterQC from 0x8624b52f9ddcd04a
5import FlowDKG from 0x8624b52f9ddcd04a
6import FlowFees from 0xf919ee77447b7497
7
8// The top-level smart contract managing the lifecycle of epochs. In Flow,
9// epochs are the smallest unit of time where the identity table (the set of
10// network operators) is static. Operators may only leave or join the network
11// at epoch boundaries. Operators may be ejected during an epoch for various
12// misdemeanours, but they remain in the identity table until the epoch ends.
13//
14// Epochs are split into 3 phases:
15// |==========================================================
16// | EPOCH N                                   || EPOCH N+1 ...
17// |----- Staking -----|- Setup -|- Committed -|| ...
18// |==========================================================
19//
20// 1)  STAKING PHASE
21// Node operators are able to submit staking requests for the NEXT epoch during
22// this phase. At the end of this phase, the Epoch smart contract resolves the
23// outstanding staking requests and determines the identity table for the next
24// epoch. The Epoch smart contract emits the EpochSetup service event containing
25// the identity table for the next epoch, which initiates the transition to the
26// Epoch Setup Phase.
27//
28// 2) SETUP PHASE
29// When this phase begins the participants in the next epoch are set. During this
30// phase, these participants prepare for the next epoch. In particular, collection
31// nodes submit votes for their cluster's root quorum certificate and consensus
32// nodes run the distributed key generation protocol (DKG) to set up the random
33// beacon. When these preparations are complete, the Epoch smart contract emits the
34// EpochCommit service event containing the artifacts of the set process, which
35// initiates the transition to the Epoch Commit Phase.
36//
37// 3) COMMITTED PHASE
38// When this phase begins, the network is fully prepared to transition to the next
39// epoch. A failure to enter this phase before transitioning to the next epoch
40// indicates that the participants in the next epoch failed to complete the set up
41// procedure, which is a critical failure and will cause the chain to halt.
42
43access(all) contract FlowEpoch {
44
45    access(all) enum EpochPhase: UInt8 {
46        access(all) case STAKINGAUCTION
47        access(all) case EPOCHSETUP
48        access(all) case EPOCHCOMMIT
49    }
50
51    access(all) enum NodeRole: UInt8 {
52        access(all) case NONE
53        access(all) case Collector
54        access(all) case Consensus
55        access(all) case Execution
56        access(all) case Verification
57        access(all) case Access
58    }
59
60    /// The Epoch Start service event is emitted when the contract transitions
61    /// to a new epoch in the staking auction phase.
62    access(all) event EpochStart (
63
64        /// The counter for the current epoch that is beginning
65        counter: UInt64,
66
67        /// The first view (inclusive) of the current epoch.
68        firstView: UInt64,
69
70        /// The last view (inclusive) of the current epoch's staking auction.
71        stakingAuctionEndView: UInt64,
72
73        /// The last view (inclusive) of the current epoch.
74        finalView: UInt64,
75
76        /// Total FLOW staked by all nodes and delegators for the current epoch.
77        totalStaked: UFix64,
78
79        /// Total supply of all FLOW for the current epoch
80        /// Includes the rewards that will be paid for the previous epoch
81        totalFlowSupply: UFix64,
82
83        /// The total rewards that will be paid out at the end of the current epoch.
84        totalRewards: UFix64,
85    )
86
87    /// The Epoch Setup service event is emitted when we transition to the Epoch Setup
88    /// phase. It contains the finalized identity table for the upcoming epoch.
89    access(all) event EpochSetup(
90
91        /// The counter for the upcoming epoch. Must be one greater than the
92        /// counter for the current epoch.
93        counter: UInt64,
94
95        /// Identity table for the upcoming epoch with all node information.
96        /// Includes:
97        /// nodeID, staking key, networking key, networking address, role,
98        /// staking information, weight, and more.
99        nodeInfo: [FlowIDTableStaking.NodeInfo],
100
101        /// The first view (inclusive) of the upcoming epoch.
102        firstView: UInt64,
103
104        /// The last view (inclusive) of the upcoming epoch.
105        finalView: UInt64,
106
107        /// The cluster assignment for the upcoming epoch. Each element in the list
108        /// represents one cluster and contains all the node IDs assigned to that
109        /// cluster, with their weights and votes
110        collectorClusters: [FlowClusterQC.Cluster],
111
112        /// The source of randomness to seed the leader selection algorithm with
113        /// for the upcoming epoch.
114        randomSource: String,
115
116        /// The deadlines of each phase in the DKG protocol to be completed in the upcoming
117        /// EpochSetup phase. Deadlines are specified in terms of a consensus view number.
118        /// When a DKG participant observes a finalized and sealed block with view greater
119        /// than the given deadline, it can safely transition to the next phase.
120        DKGPhase1FinalView: UInt64,
121        DKGPhase2FinalView: UInt64,
122        DKGPhase3FinalView: UInt64,
123
124        /// The target duration for the upcoming epoch, in seconds
125        targetDuration: UInt64,
126        /// The target end time for the upcoming epoch, specified in second-precision Unix time
127        targetEndTime: UInt64
128    )
129
130    /// The EpochCommit service event is emitted when we transition from the Epoch
131    /// Committed phase. It is emitted only when all preparation for the upcoming epoch
132    /// has been completed
133    access(all) event EpochCommit (
134
135        /// The counter for the upcoming epoch. Must be equal to the counter in the
136        /// previous EpochSetup event.
137        counter: UInt64,
138
139        /// The result of the QC aggregation process. Each element contains
140        /// all the nodes and votes received for a particular cluster
141        /// QC stands for quorum certificate that each cluster generates.
142        clusterQCs: [FlowClusterQC.ClusterQC],
143
144        /// The resulting public keys from the DKG process, encoded as by the flow-go
145        /// crypto library, then hex-encoded.
146        /// Group public key is the first element, followed by the individual keys
147        dkgPubKeys: [String],
148
149        dkgGroupKey: String,
150
151        dkgIdMapping: {String: Int},
152    )
153
154    /// The EpochRecover service event is emitted via the recoverEpoch governance transaction to execute an epoch recovery.
155    /// The epoch recovery process is used if the Epoch Preparation Protocol fails for any reason.
156    /// When this happens, the network enters Epoch Fallback Mode [EFM], wherein the Protocol State
157    /// extends the current epoch until an epoch recovery can take place. In general, while in EFM, the
158    /// Protocol State and the FlowEpoch smart contract may have conflicting views about the current
159    /// valid epoch state (epoch counter and phase). The epoch recovery process resolves this inconsistency
160    /// by injecting a new common shared state to proceed forward from. 
161    /// Concretely, the epoch recovery process fully specifies the parameters for a new epoch to transition into
162    /// (essentially all fields of the EpochSetup and EpochCommit events), called the recovery epoch.
163    /// The FlowEpoch smart contracts inserts the recovery epoch, starts it, and emits the EpochRecover event,
164    /// which contains the same data. When the EpochRecover event is processed by the protocol state, 
165    /// the Protocol State is updated to include the recovery epoch as well.
166    access(all) event EpochRecover (
167        /// The counter for the RecoveryEpoch.
168        /// This must be 1 greater than the current epoch counter, as reported by the Protocol State,
169        /// otherwise the EpochRecover event will be rejected (recovery process will fail).
170        counter: UInt64,
171
172        /// Identity table for the upcoming epoch with all node information.
173        /// including nodeID, staking key, networking key, networking address, role,
174        /// staking information, weight, and more.
175        nodeInfo: [FlowIDTableStaking.NodeInfo],
176
177        /// The first view (inclusive) of the RecoveryEpoch. This must be 1 greater
178        /// than the current epoch's final view, as reported by the Protocol State,
179        /// otherwise the EpochRecover event will be rejected (recovery process will fail).
180        firstView: UInt64,
181
182        /// The last view (inclusive) of the RecoveryEpoch.
183        finalView: UInt64,
184
185        /// The collector node cluster assignment for the RecoveryEpoch. Each element in the list
186        /// represents one cluster and contains all the node IDs assigned to that cluster.
187        clusterAssignments: [[String]],
188
189        /// The source of randomness to seed the leader selection algorithm with
190        /// for the upcoming epoch.
191        randomSource: String,
192
193        /// The deadlines of each phase in the DKG protocol to be completed in the upcoming
194        /// EpochSetup phase. Deadlines are specified in terms of a consensus view number.
195        /// When a DKG participant observes a finalized and sealed block with view greater
196        /// than the given deadline, it can safely transition to the next phase.
197        DKGPhase1FinalView: UInt64,
198        DKGPhase2FinalView: UInt64,
199        DKGPhase3FinalView: UInt64,
200
201        /// The target duration for the upcoming epoch, in seconds
202        targetDuration: UInt64,
203        /// The target end time for the upcoming epoch, specified in second-precision Unix time
204        targetEndTime: UInt64,
205
206        /// The cluster QCs passed in the recoverEpoch transaction. These are generated out-of-band
207        /// using the same procedure as during a spork.
208        /// CAUTION: Validity of the QCs is not explicitly verified during the recovery process. An
209        /// invalid cluster QC will prevent the respective cluster from starting its local consensus
210        /// and hence prevent it from functioning for the entire epoch. If all cluster QCs are invalid,
211        /// the blockchain cannot ingest transactions, which can only be resolved through a spork!
212        clusterQCVoteData: [FlowClusterQC.ClusterQCVoteData],
213
214        /// The DKG public keys and ID mapping for the recovery epoch. 
215        /// Currently, these are re-used from the last successful DKG.
216        /// CAUTION: Validity of the key vector is not explicitly verified during the recovery process.
217        /// Invalid DKG information has the potential to prevent Flow's main consensus from continuing,
218        /// which would halt the chain for good and can only be resolved through a spork.
219
220        dkgPubKeys: [String],
221        dkgGroupKey: String,
222        dkgIdMapping: {String: Int},
223    )
224
225    /// Contains specific metadata about a particular epoch
226    /// All historical epoch metadata is stored permanently
227    access(all) struct EpochMetadata {
228
229        /// The identifier for the epoch
230        access(all) let counter: UInt64
231
232        /// The seed used for generating the epoch setup
233        access(all) let seed: String
234
235        /// The first view of this epoch
236        access(all) let startView: UInt64
237
238        /// The last view of this epoch
239        access(all) let endView: UInt64
240
241        /// The last view of the staking auction
242        access(all) let stakingEndView: UInt64
243
244        /// The total rewards that are paid out for the epoch
245        access(all) var totalRewards: UFix64
246
247        /// The reward amounts that are paid to each individual node and its delegators
248        access(all) var rewardAmounts: [FlowIDTableStaking.RewardsBreakdown]
249
250        /// Tracks if rewards have been paid for this epoch
251        access(all) var rewardsPaid: Bool
252
253        /// The organization of collector node IDs into clusters
254        /// determined by a round robin sorting algorithm
255        access(all) let collectorClusters: [FlowClusterQC.Cluster]
256
257        /// The Quorum Certificates from the ClusterQC contract
258        access(all) var clusterQCs: [FlowClusterQC.ClusterQC]
259
260        /// The public keys associated with the Distributed Key Generation
261        /// process that consensus nodes participate in
262        /// The first element is the group public key, followed by n participant public keys.
263        /// NOTE: This data structure was updated to include a mapping from node ID to DKG index.
264        /// Because structures cannot be updated in Cadence, we include the groupPubKey and pubKeys
265        /// fields here (idMapping field is omitted).
266        access(all) var dkgKeys: [String]
267
268        init(counter: UInt64,
269             seed: String,
270             startView: UInt64,
271             endView: UInt64,
272             stakingEndView: UInt64,
273             totalRewards: UFix64,
274             collectorClusters: [FlowClusterQC.Cluster],
275             clusterQCs: [FlowClusterQC.ClusterQC],
276             dkgKeys: [String]) {
277
278            self.counter = counter
279            self.seed = seed
280            self.startView = startView
281            self.endView = endView
282            self.stakingEndView = stakingEndView
283            self.totalRewards = totalRewards
284            self.rewardAmounts = []
285            self.rewardsPaid = false
286            self.collectorClusters = collectorClusters
287            self.clusterQCs = clusterQCs
288            self.dkgKeys = dkgKeys
289        }
290
291        access(account) fun copy(): EpochMetadata {
292            return self
293        }
294
295        access(account) fun setTotalRewards(_ newRewards: UFix64) {
296            self.totalRewards = newRewards
297        }
298
299        access(account) fun setRewardAmounts(_ rewardBreakdown: [FlowIDTableStaking.RewardsBreakdown]) {
300            self.rewardAmounts = rewardBreakdown
301        }
302
303        access(account) fun setRewardsPaid(_ rewardsPaid: Bool) {
304            self.rewardsPaid = rewardsPaid
305        }
306
307        access(account) fun setClusterQCs(qcs: [FlowClusterQC.ClusterQC]) {
308            self.clusterQCs = qcs
309        }
310
311        /// Sets the DKG group key (keys[0]) and participant keys (keys[1:]) from the DKG result.
312        /// NOTE: This data structure was updated to include a mapping from node ID to DKG index.
313        /// Because structures cannot be updated in Cadence, we include the groupPubKey and pubKeys
314        /// fields here (idMapping field is omitted).
315        access(account) fun setDKGKeys(keys: [String]) {
316            self.dkgKeys = keys
317        }
318    }
319
320    /// Metadata that is managed and can be changed by the Admin
321    access(all) struct Config {
322        /// The number of views in an entire epoch
323        access(all) var numViewsInEpoch: UInt64
324
325        access(all) fun setNumViewsInEpoch(_ views: UInt64) {
326            self.numViewsInEpoch = views
327        }
328
329        /// The number of views in the staking auction
330        access(all) var numViewsInStakingAuction: UInt64
331
332        access(all) fun setNumViewsInStakingAuction(_ views: UInt64) {
333            self.numViewsInStakingAuction = views
334        }
335
336        /// The number of views in each dkg phase
337        access(all) var numViewsInDKGPhase: UInt64
338
339        access(all) fun setNumViewsInDKGPhase(_ views: UInt64) {
340            self.numViewsInDKGPhase = views
341        }
342
343        /// The number of collector clusters in each epoch
344        access(all) var numCollectorClusters: UInt16
345
346        access(all) fun setNumCollectorClusters(_ numClusters: UInt16) {
347            self.numCollectorClusters = numClusters
348        }
349
350        /// Tracks the rate at which the rewards payout increases every epoch
351        /// This value is multiplied by the FLOW total supply to get the next payout
352        access(all) var FLOWsupplyIncreasePercentage: UFix64
353
354        access(all) fun setFLOWsupplyIncreasePercentage(_ percentage: UFix64) {
355            self.FLOWsupplyIncreasePercentage = percentage
356        }
357
358        init(numViewsInEpoch: UInt64, numViewsInStakingAuction: UInt64, numViewsInDKGPhase: UInt64, numCollectorClusters: UInt16, FLOWsupplyIncreasePercentage: UFix64) {
359            self.numViewsInEpoch = numViewsInEpoch
360            self.numViewsInStakingAuction = numViewsInStakingAuction
361            self.numViewsInDKGPhase = numViewsInDKGPhase
362            self.numCollectorClusters = numCollectorClusters
363            self.FLOWsupplyIncreasePercentage = FLOWsupplyIncreasePercentage
364        }
365    }
366
367    /// Configuration for epoch timing.
368    /// Each epoch is assigned a target end time when it is setup (within the EpochSetup event).
369    /// The configuration defines a reference epoch counter and timestamp, which defines
370    /// all future target end times. If `targetEpochCounter` is an upcoming epoch, then
371    /// its target end time is given by:
372    ///
373    ///   targetEndTime = refTimestamp + duration * (targetEpochCounter-refCounter)
374    ///
375    access(all) struct EpochTimingConfig {
376        /// The duration of each epoch, in seconds
377        access(all) let duration: UInt64
378        /// The counter of the reference epoch
379        access(all) let refCounter: UInt64
380        /// The end time of the reference epoch, specified in second-precision Unix time
381        access(all) let refTimestamp: UInt64
382
383        /// Compute target switchover time based on offset from reference counter/switchover.
384        access(all) fun getTargetEndTimeForEpoch(_ targetEpochCounter: UInt64): UInt64 {
385            return self.refTimestamp + self.duration * (targetEpochCounter-self.refCounter)
386        }
387
388        init(duration: UInt64, refCounter: UInt64, refTimestamp: UInt64) {
389             self.duration = duration
390             self.refCounter = refCounter
391             self.refTimestamp = refTimestamp
392        }
393    }
394
395    /// Holds the `FlowEpoch.Config` struct with the configurable metadata
396    access(contract) let configurableMetadata: Config
397
398    /// Metadata that is managed by the smart contract
399    /// and cannot be changed by the Admin
400
401    /// Contains a historical record of the metadata from all previous epochs
402    /// indexed by epoch number
403
404    /// Returns the metadata from the specified epoch
405    /// or nil if it isn't found
406    /// Epoch Metadata is stored in account storage so the growing dictionary
407    /// does not have to be loaded every time the contract is loaded
408    access(all) fun getEpochMetadata(_ epochCounter: UInt64): EpochMetadata? {
409        if let metadataDictionary = self.account.storage.borrow<&{UInt64: EpochMetadata}>(from: self.metadataStoragePath) {
410            if let metadataRef = metadataDictionary[epochCounter] {
411                return metadataRef.copy()
412            }
413        }
414        return nil
415    }
416
417    /// Saves a modified EpochMetadata struct to the metadata in account storage
418    access(contract) fun saveEpochMetadata(_ newMetadata: EpochMetadata) {
419        pre {
420            self.currentEpochCounter == 0 ||
421            (newMetadata.counter >= self.currentEpochCounter - 1 &&
422            newMetadata.counter <= self.proposedEpochCounter()):
423                "Cannot modify epoch metadata from epochs after the proposed epoch or before the previous epoch"
424        }
425        if let metadataDictionary = self.account.storage.borrow<auth(Mutate) &{UInt64: EpochMetadata}>(from: self.metadataStoragePath) {
426            if let metadata = metadataDictionary[newMetadata.counter] {
427                assert (
428                    metadata.counter == newMetadata.counter,
429                    message: "Cannot save metadata with mismatching epoch counters"
430                )
431            }
432            metadataDictionary[newMetadata.counter] = newMetadata
433        }
434    }
435
436    /// Generates 128 bits of randomness using system random (derived from Random Beacon).
437    access(contract) fun generateRandomSource(): String {
438        post {
439            result.length == 32:
440                "FlowEpoch.generateRandomSource: Critical invariant violated! "
441                    .concat("Expected hex random source with length 32 (128 bits) but got length ")
442                    .concat(result.length.toString())
443                    .concat(" instead.")
444        }
445        var randomSource = String.encodeHex(revertibleRandom<UInt128>().toBigEndianBytes())
446        return randomSource
447    }
448
449    /// The counter, or ID, of the current epoch
450    access(all) var currentEpochCounter: UInt64
451
452    /// The current phase that the epoch is in
453    access(all) var currentEpochPhase: EpochPhase
454
455    /// Path where the `FlowEpoch.Admin` resource is stored
456    access(all) let adminStoragePath: StoragePath
457
458    /// Path where the `FlowEpoch.Heartbeat` resource is stored
459    access(all) let heartbeatStoragePath: StoragePath
460
461    /// Path where the `{UInt64: EpochMetadata}` dictionary is stored
462    access(all) let metadataStoragePath: StoragePath
463
464    /// Resource that can update some of the contract fields
465    access(all) resource Admin {
466        access(all) fun updateEpochViews(_ newEpochViews: UInt64) {
467            pre {
468                FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction"
469                FlowEpoch.isValidPhaseConfiguration(FlowEpoch.configurableMetadata.numViewsInStakingAuction,
470                    FlowEpoch.configurableMetadata.numViewsInDKGPhase,
471                    newEpochViews): "New Epoch Views must be greater than the sum of staking and DKG Phase views"
472            }
473
474            FlowEpoch.configurableMetadata.setNumViewsInEpoch(newEpochViews)
475        }
476
477        access(all) fun updateAuctionViews(_ newAuctionViews: UInt64) {
478            pre {
479                FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction"
480                FlowEpoch.isValidPhaseConfiguration(newAuctionViews,
481                    FlowEpoch.configurableMetadata.numViewsInDKGPhase,
482                    FlowEpoch.configurableMetadata.numViewsInEpoch): "Epoch Views must be greater than the sum of new staking and DKG Phase views"
483            }
484
485            FlowEpoch.configurableMetadata.setNumViewsInStakingAuction(newAuctionViews)
486        }
487
488        access(all) fun updateDKGPhaseViews(_ newPhaseViews: UInt64) {
489            pre {
490                FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction"
491                FlowEpoch.isValidPhaseConfiguration(FlowEpoch.configurableMetadata.numViewsInStakingAuction,
492                    newPhaseViews,
493                    FlowEpoch.configurableMetadata.numViewsInEpoch): "Epoch Views must be greater than the sum of staking and new DKG Phase views"
494            }
495
496            FlowEpoch.configurableMetadata.setNumViewsInDKGPhase(newPhaseViews)
497        }
498
499        access(all) fun updateEpochTimingConfig(_ newConfig: EpochTimingConfig) {
500            pre {
501                FlowEpoch.currentEpochCounter >= newConfig.refCounter: "Reference epoch must be before next epoch"
502            }
503            FlowEpoch.account.storage.load<EpochTimingConfig>(from: /storage/flowEpochTimingConfig)
504            FlowEpoch.account.storage.save(newConfig, to: /storage/flowEpochTimingConfig)
505        }
506
507        access(all) fun updateNumCollectorClusters(_ newNumClusters: UInt16) {
508            pre {
509                FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction"
510            }
511
512            FlowEpoch.configurableMetadata.setNumCollectorClusters(newNumClusters)
513        }
514
515        access(all) fun updateFLOWSupplyIncreasePercentage(_ newPercentage: UFix64) {
516            pre {
517                FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction"
518                newPercentage <= 1.0: "New value must be between zero and one"
519            }
520
521            FlowEpoch.configurableMetadata.setFLOWsupplyIncreasePercentage(newPercentage)
522        }
523
524        // Enable or disable automatic rewards calculations and payments
525        access(all) fun updateAutomaticRewardsEnabled(_ enabled: Bool) {
526            FlowEpoch.account.storage.load<Bool>(from: /storage/flowAutomaticRewardsEnabled)
527            FlowEpoch.account.storage.save(enabled, to: /storage/flowAutomaticRewardsEnabled)
528        }
529        /// Emits the EpochRecover service event. Inputs for the recovery epoch will be validated before the recover event is emitted.
530        /// This func should only be used during the epoch recovery governance intervention.
531        access(self) fun emitEpochRecoverEvent(epochCounter: UInt64,
532            startView: UInt64,
533            stakingEndView: UInt64,
534            endView: UInt64,
535            nodeIDs: [String],
536            clusterAssignments: [[String]],
537            randomSource: String,
538            targetDuration: UInt64,
539            targetEndTime: UInt64,
540            clusterQCVoteData: [FlowClusterQC.ClusterQCVoteData],
541            dkgPubKeys: [String],
542            dkgGroupKey: String,
543            dkgIdMapping: {String: Int},
544            ) {
545            self.recoverEpochPreChecks(
546                startView: startView, 
547                stakingEndView: stakingEndView, 
548                endView: endView, 
549                nodeIDs: nodeIDs,
550                numOfClusterAssignments: clusterAssignments.length,
551                numOfClusterQCVoteData: clusterQCVoteData.length,
552            )
553            /// Construct the identity table for the recovery epoch
554            let nodes: [FlowIDTableStaking.NodeInfo] = []
555            for nodeID in nodeIDs {
556                nodes.append(FlowIDTableStaking.NodeInfo(nodeID: nodeID))
557            }
558            
559            let numViewsInDKGPhase = FlowEpoch.configurableMetadata.numViewsInDKGPhase
560            let dkgPhase1FinalView = stakingEndView + numViewsInDKGPhase
561            let dkgPhase2FinalView = dkgPhase1FinalView + numViewsInDKGPhase
562            let dkgPhase3FinalView = dkgPhase2FinalView + numViewsInDKGPhase
563
564            /// emit EpochRecover event
565            emit FlowEpoch.EpochRecover(
566                counter: epochCounter,
567                nodeInfo: nodes,
568                firstView: startView,
569                finalView: endView,
570                clusterAssignments: clusterAssignments,
571                randomSource: randomSource,
572                DKGPhase1FinalView: dkgPhase1FinalView,
573                DKGPhase2FinalView: dkgPhase2FinalView,
574                DKGPhase3FinalView: dkgPhase3FinalView,
575                targetDuration: targetDuration,
576                targetEndTime: targetEndTime,
577                clusterQCVoteData: clusterQCVoteData,
578                dkgPubKeys: dkgPubKeys,
579                dkgGroupKey: dkgGroupKey,
580                dkgIdMapping: dkgIdMapping,
581            )
582        }
583
584        /// Performs sanity checks for the provided epoch configuration. It will ensure the following;
585        /// - There is a valid phase configuration.
586        /// - All nodes in the node ids list have a weight > 0.
587        access(self) fun recoverEpochPreChecks(startView: UInt64,
588            stakingEndView: UInt64,
589            endView: UInt64,
590            nodeIDs: [String],
591            numOfClusterAssignments: Int,
592            numOfClusterQCVoteData: Int) 
593        {
594            pre {
595                FlowEpoch.isValidPhaseConfiguration(stakingEndView-startView+1, FlowEpoch.configurableMetadata.numViewsInDKGPhase, endView-startView+1):
596                    "Invalid startView, stakingEndView, and endView configuration"
597            }
598
599            /// sanity check all nodes should have a weight > 0
600            for nodeID in nodeIDs {
601                assert(
602                    FlowIDTableStaking.NodeInfo(nodeID: nodeID).initialWeight > 0, 
603                    message: "FlowEpoch.Admin.recoverEpochPreChecks: All nodes in node ids list for recovery epoch must have a weight > 0. The node "
604                    .concat(nodeID).concat(" has a weight of 0.")
605                )
606            }
607
608            // sanity check we must receive qc vote data for each cluster
609            assert(
610                numOfClusterAssignments == numOfClusterQCVoteData, 
611                message: "FlowEpoch.Admin.recoverEpochPreChecks: The number of cluster assignments "
612                .concat(numOfClusterAssignments.toString()).concat(" does not match the number of cluster qc vote data ")
613                .concat(numOfClusterQCVoteData.toString())
614            )
615        }
616        
617        /// Stops epoch components. When we are in the StakingAuction phase the staking auction is stopped 
618        /// otherwise cluster qc voting and the dkg is stopped.
619        access(self) fun stopEpochComponents() {
620            if FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION {
621                /// Since we are resetting the epoch, we do not need to
622                /// start epoch setup also. We only need to end the staking auction
623                FlowEpoch.borrowStakingAdmin().endStakingAuction()
624            } else {
625                /// force reset the QC and DKG
626                FlowEpoch.borrowClusterQCAdmin().forceStopVoting()
627                FlowEpoch.borrowDKGAdmin().forceEndDKG()
628            }
629        }
630
631        /// Ends the currently active epoch and starts a new "recovery epoch" with the provided configuration.
632        /// This function is used to recover from Epoch Fallback Mode (EFM).
633        /// The "recovery epoch" config will be emitted in the EpochRecover service event.
634        /// The "recovery epoch" must have a counter exactly one greater than the current epoch counter.
635        /// 
636        /// This function differs from recoverCurrentEpoch because it increments the epoch counter and will calculate rewards. 
637        access(all) fun recoverNewEpoch(
638            recoveryEpochCounter: UInt64,
639            startView: UInt64,
640            stakingEndView: UInt64,
641            endView: UInt64,
642            targetDuration: UInt64,
643            targetEndTime: UInt64,
644            clusterAssignments: [[String]],
645            clusterQCVoteData: [FlowClusterQC.ClusterQCVoteData],
646            dkgPubKeys: [String],
647            dkgGroupKey: String,
648            dkgIdMapping: {String: Int},
649            nodeIDs: [String]) 
650        {
651            pre {
652                recoveryEpochCounter == FlowEpoch.proposedEpochCounter():
653                    "FlowEpoch.Admin.recoverNewEpoch: Recovery epoch counter must equal current epoch counter + 1. "
654                        .concat("Got recovery epoch counter (")
655                        .concat(recoveryEpochCounter.toString())
656                        .concat(") with current epoch counter (")
657                        .concat(FlowEpoch.currentEpochCounter.toString())
658                        .concat(").")
659            }
660
661            self.stopEpochComponents()
662            let randomSource = FlowEpoch.generateRandomSource()
663
664            /// Create new EpochMetadata for the recovery epoch with the new values
665            let newEpochMetadata = EpochMetadata(
666                /// Increment the epoch counter when recovering with a new epoch
667                counter: recoveryEpochCounter,
668                seed: randomSource,
669                startView: startView,
670                endView: endView,
671                stakingEndView: stakingEndView,
672                totalRewards: 0.0,     // will be overwritten in `calculateAndSetRewards` below
673                collectorClusters: [],
674                clusterQCs: [],
675                dkgKeys: [dkgGroupKey].concat(dkgPubKeys)
676            )
677
678            /// Save the new epoch meta data, it will be referenced as the epoch progresses
679            FlowEpoch.saveEpochMetadata(newEpochMetadata)
680
681            /// Calculate rewards for the current epoch and set the payout for the next epoch
682            FlowEpoch.calculateAndSetRewards()
683
684            /// Emit the EpochRecover service event.
685            /// This will be processed by the Protocol State, which will then exit EFM
686            /// and enter the recovery epoch at the specified start view.
687            self.emitEpochRecoverEvent(epochCounter: recoveryEpochCounter,
688                startView: startView,
689                stakingEndView: stakingEndView,
690                endView: endView,
691                nodeIDs: nodeIDs,
692                clusterAssignments: clusterAssignments,
693                randomSource: randomSource,
694                targetDuration: targetDuration,
695                targetEndTime: targetEndTime,
696                clusterQCVoteData: clusterQCVoteData,
697                dkgPubKeys: dkgPubKeys,
698                dkgGroupKey: dkgGroupKey,
699                dkgIdMapping: dkgIdMapping,
700            )
701
702            /// Start a new Epoch, which increments the current epoch counter
703            FlowEpoch.startNewEpoch()
704        }
705
706        /// Replaces the currently active epoch with a new "recovery epoch" with the provided configuration.
707        /// This function is used to recover from Epoch Fallback Mode (EFM).
708        /// The "recovery epoch" config will be emitted in the EpochRecover service event.
709        /// The "recovery epoch must have a counter exactly equal to the current epoch counter.
710        /// 
711        /// This function differs from recoverNewEpoch because it replaces the currently active epoch instead of starting a new epoch.
712        /// This function does not calculate or distribute rewards
713        /// Calling recoverCurrentEpoch multiple times does not cause multiple reward payouts.
714        /// Rewards for the recovery epoch will be calculated and paid out during the course of the epoch. 
715        /// CAUTION: This causes data loss by replacing the existing current epoch metadata with the inputs to this function.
716        /// This function exists to recover from potential race conditions that caused a prior recoverCurrentEpoch transaction to fail;
717        /// this allows operators to retry the recovery procedure, overwriting the prior failed attempt.
718        access(all) fun recoverCurrentEpoch(
719            recoveryEpochCounter: UInt64,
720            startView: UInt64,
721            stakingEndView: UInt64,
722            endView: UInt64,
723            targetDuration: UInt64,
724            targetEndTime: UInt64,
725            clusterAssignments: [[String]],
726            clusterQCVoteData: [FlowClusterQC.ClusterQCVoteData],
727            dkgPubKeys: [String],
728            dkgGroupKey: String,
729            dkgIdMapping: {String: Int},
730            nodeIDs: [String]) 
731        { 
732            pre {
733                recoveryEpochCounter == FlowEpoch.currentEpochCounter:
734                    "FlowEpoch.Admin.recoverCurrentEpoch: Recovery epoch counter must equal current epoch counter. "
735                        .concat("Got recovery epoch counter (")
736                        .concat(recoveryEpochCounter.toString())
737                        .concat(") with current epoch counter (")
738                        .concat(FlowEpoch.currentEpochCounter.toString())
739                        .concat(").")
740            }
741
742            self.stopEpochComponents()
743            FlowEpoch.currentEpochPhase = EpochPhase.STAKINGAUCTION
744            
745            let currentEpochMetadata = FlowEpoch.getEpochMetadata(recoveryEpochCounter)
746            /// Create new EpochMetadata for the recovery epoch with the new values.
747            /// This epoch metadata will overwrite the epoch metadata of the current epoch.
748            let recoveryEpochMetadata: FlowEpoch.EpochMetadata = EpochMetadata(
749                counter: recoveryEpochCounter,
750                seed: currentEpochMetadata!.seed,
751                startView: startView,
752                endView: endView,
753                stakingEndView: stakingEndView,
754                totalRewards: currentEpochMetadata!.totalRewards,
755                collectorClusters: [],
756                clusterQCs: [],
757                dkgKeys: [dkgGroupKey].concat(dkgPubKeys)
758            )
759
760            /// Save EpochMetadata for the recovery epoch, it will be referenced as the epoch progresses.
761            /// CAUTION: This overwrites the EpochMetadata already stored for the current epoch.
762            FlowEpoch.saveEpochMetadata(recoveryEpochMetadata)
763
764            /// Emit the EpochRecover service event.
765            /// This will be processed by the Protocol State, which will then exit EFM
766            /// and enter the recovery epoch at the specified start view.
767            self.emitEpochRecoverEvent( 
768                epochCounter: recoveryEpochCounter,
769                startView: startView,
770                stakingEndView: stakingEndView,
771                endView: endView,
772                nodeIDs: nodeIDs,
773                clusterAssignments: clusterAssignments,
774                randomSource: recoveryEpochMetadata.seed,
775                targetDuration: targetDuration,
776                targetEndTime: targetEndTime,
777                clusterQCVoteData: clusterQCVoteData,
778                dkgPubKeys: dkgPubKeys,
779                dkgGroupKey: dkgGroupKey,
780                dkgIdMapping: dkgIdMapping,
781            )
782        }
783
784        /// Ends the currently active epoch and starts a new one with the provided configuration.
785        /// The new epoch, after resetEpoch completes, has `counter = currentEpochCounter + 1`.
786        /// This function is used during sporks - since the consensus view is reset, and the protocol is
787        /// bootstrapped with a new initial state snapshot, this initializes FlowEpoch with the first epoch
788        /// of the new spork, as defined in that snapshot.
789        access(all) fun resetEpoch(
790            currentEpochCounter: UInt64,
791            randomSource: String,
792            startView: UInt64,
793            stakingEndView: UInt64,
794            endView: UInt64,
795            collectorClusters: [FlowClusterQC.Cluster],
796            clusterQCs: [FlowClusterQC.ClusterQC],
797            dkgPubKeys: [String])
798        {
799            pre {
800                currentEpochCounter == FlowEpoch.currentEpochCounter:
801                    "Cannot submit a current Epoch counter that does not match the current counter stored in the smart contract"
802                FlowEpoch.isValidPhaseConfiguration(stakingEndView-startView+1, FlowEpoch.configurableMetadata.numViewsInDKGPhase, endView-startView+1):
803                    "Invalid startView, stakingEndView, and endView configuration"
804            }
805
806            self.stopEpochComponents()
807
808            // Create new Epoch metadata for the next epoch
809            // with the new values
810            let newEpochMetadata = EpochMetadata(
811                    counter: currentEpochCounter + 1,
812                    seed: randomSource,
813                    startView: startView,
814                    endView: endView,
815                    stakingEndView: stakingEndView,
816                    totalRewards: 0.0, // will be overwritten in calculateAndSetRewards below
817                    collectorClusters: collectorClusters,
818                    clusterQCs: clusterQCs,
819                    dkgKeys: dkgPubKeys)
820
821            FlowEpoch.saveEpochMetadata(newEpochMetadata)
822
823            // Calculate rewards for the current epoch
824            // and set the payout for the next epoch
825            FlowEpoch.calculateAndSetRewards()
826
827            // Start a new Epoch, which increments the current epoch counter
828            FlowEpoch.startNewEpoch()
829        }
830    }
831
832    /// Resource that is controlled by the protocol and is used
833    /// to change the current phase of the epoch or reset the epoch if needed
834    access(all) resource Heartbeat {
835        /// Function that is called every block to advance the epoch
836        /// and change phase if the required conditions have been met
837        access(all) fun advanceBlock() {
838            switch FlowEpoch.currentEpochPhase {
839                case EpochPhase.STAKINGAUCTION:
840                    let currentBlock = getCurrentBlock()
841                    let currentEpochMetadata = FlowEpoch.getEpochMetadata(FlowEpoch.currentEpochCounter)!
842                    // Pay rewards only if automatic rewards are enabled
843                    // This will only actually happen once immediately after the epoch begins
844                    // because `payRewardsForPreviousEpoch()` will only pay rewards once
845                    if FlowEpoch.automaticRewardsEnabled() {
846                        self.payRewardsForPreviousEpoch()
847                    }
848                    if currentBlock.view >= currentEpochMetadata.stakingEndView {
849                        self.endStakingAuction()
850                    }
851                case EpochPhase.EPOCHSETUP:
852                    if FlowClusterQC.votingCompleted() && (FlowDKG.dkgCompleted() != nil) {
853                        self.calculateAndSetRewards()
854                        self.startEpochCommit()
855                    }
856                case EpochPhase.EPOCHCOMMIT:
857                    let currentBlock = getCurrentBlock()
858                    let currentEpochMetadata = FlowEpoch.getEpochMetadata(FlowEpoch.currentEpochCounter)!
859                    if currentBlock.view >= currentEpochMetadata.endView {
860                        self.endEpoch()
861                    }
862                default:
863                    return
864            }
865        }
866
867        /// Calls `FlowEpoch` functions to end the staking auction phase
868        /// and start the Epoch Setup phase
869        access(all) fun endStakingAuction() {
870            pre {
871                FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only end staking auction during the staking auction"
872            }
873
874            let proposedNodeIDs = FlowEpoch.endStakingAuction()
875
876            /// random source must be a hex string of 32 characters (i.e 16 bytes or 128 bits)
877            let randomSource = FlowEpoch.generateRandomSource()
878
879            FlowEpoch.startEpochSetup(proposedNodeIDs: proposedNodeIDs, randomSource: randomSource)
880        }
881
882        /// Calls `FlowEpoch` functions to end the Epoch Setup phase
883        /// and start the Epoch Setup Phase
884        access(all) fun startEpochCommit() {
885            pre {
886                FlowEpoch.currentEpochPhase == EpochPhase.EPOCHSETUP: "Can only end Epoch Setup during Epoch Setup"
887            }
888
889            FlowEpoch.startEpochCommit()
890        }
891
892        /// Calls `FlowEpoch` functions to end the Epoch Commit phase
893        /// and start the Staking Auction phase of a new epoch
894        access(all) fun endEpoch() {
895            pre {
896                FlowEpoch.currentEpochPhase == EpochPhase.EPOCHCOMMIT: "Can only end epoch during Epoch Commit"
897            }
898
899            FlowEpoch.startNewEpoch()
900        }
901
902        /// Needs to be called before the epoch is over
903        /// Calculates rewards for the current epoch and stores them in epoch metadata
904        access(all) fun calculateAndSetRewards() {
905            FlowEpoch.calculateAndSetRewards()
906        }
907
908        access(all) fun payRewardsForPreviousEpoch() {
909            FlowEpoch.payRewardsForPreviousEpoch()
910        }
911    }
912
913    /// Calculates a new token payout for the current epoch
914    /// and sets the new payout for the next epoch
915    access(account) fun calculateAndSetRewards() {
916
917        let stakingAdmin = self.borrowStakingAdmin()
918
919        // Calculate rewards for the current epoch that is about to end
920        // and save that reward breakdown in the epoch metadata for the current epoch
921        let rewardsSummary = stakingAdmin.calculateRewards()
922        let currentMetadata = self.getEpochMetadata(self.currentEpochCounter)!
923        currentMetadata.setRewardAmounts(rewardsSummary.breakdown)
924        currentMetadata.setTotalRewards(rewardsSummary.totalRewards)
925        self.saveEpochMetadata(currentMetadata)
926
927        if FlowEpoch.automaticRewardsEnabled() {
928            // Calculate the total supply of FLOW after the current epoch's payout
929            // the calculation includes the tokens that haven't been minted for the current epoch yet
930            let currentPayout = FlowIDTableStaking.getEpochTokenPayout()
931            let feeAmount = FlowFees.getFeeBalance()
932            var flowTotalSupplyAfterPayout = 0.0
933            if feeAmount >= currentPayout {
934                flowTotalSupplyAfterPayout = FlowToken.totalSupply
935            } else {
936                flowTotalSupplyAfterPayout = FlowToken.totalSupply + (currentPayout - feeAmount)
937            }
938
939            // Load the amount of bonus tokens from storage
940            let bonusTokens = FlowEpoch.getBonusTokens()
941
942            // Subtract bonus tokens from the total supply to get the real supply
943            if bonusTokens < flowTotalSupplyAfterPayout {
944                flowTotalSupplyAfterPayout = flowTotalSupplyAfterPayout - bonusTokens
945            }
946
947            // Calculate the payout for the next epoch
948            let proposedPayout = flowTotalSupplyAfterPayout * FlowEpoch.configurableMetadata.FLOWsupplyIncreasePercentage
949
950            // Set the new payout in the staking contract and proposed Epoch Metadata
951            self.borrowStakingAdmin().setEpochTokenPayout(proposedPayout)
952            let proposedMetadata = self.getEpochMetadata(self.proposedEpochCounter())
953                ?? panic("Cannot set rewards for the next epoch becuase it hasn't been proposed yet")
954            proposedMetadata.setTotalRewards(proposedPayout)
955            self.saveEpochMetadata(proposedMetadata)
956        }
957    }
958
959    /// Pays rewards to the nodes and delegators of the previous epoch
960    access(account) fun payRewardsForPreviousEpoch() {
961        if let previousEpochMetadata = self.getEpochMetadata(self.currentEpochCounter - 1) {
962            if !previousEpochMetadata.rewardsPaid {
963                let summary = FlowIDTableStaking.EpochRewardsSummary(totalRewards: previousEpochMetadata.totalRewards, breakdown: previousEpochMetadata.rewardAmounts)
964                self.borrowStakingAdmin().payRewards(forEpochCounter: previousEpochMetadata.counter, rewardsSummary: summary)
965                previousEpochMetadata.setRewardsPaid(true)
966                self.saveEpochMetadata(previousEpochMetadata)
967            }
968        }
969    }
970
971    /// Moves staking tokens between buckets,
972    /// and starts the new epoch staking auction
973    access(account) fun startNewEpoch() {
974
975        // End QC and DKG if they are still enabled
976        if FlowClusterQC.inProgress {
977            self.borrowClusterQCAdmin().stopVoting()
978        }
979        if FlowDKG.dkgEnabled {
980            self.borrowDKGAdmin().endDKG()
981        }
982
983        self.borrowStakingAdmin().moveTokens(newEpochCounter: self.proposedEpochCounter())
984
985        self.currentEpochPhase = EpochPhase.STAKINGAUCTION
986
987        // Update the epoch counters
988        self.currentEpochCounter = self.proposedEpochCounter()
989
990        let previousEpochMetadata = self.getEpochMetadata(self.currentEpochCounter - 1)!
991        let newEpochMetadata = self.getEpochMetadata(self.currentEpochCounter)!
992
993        emit EpochStart (
994            counter: self.currentEpochCounter,
995            firstView: newEpochMetadata.startView,
996            stakingAuctionEndView: newEpochMetadata.stakingEndView,
997            finalView: newEpochMetadata.endView,
998            totalStaked: FlowIDTableStaking.getTotalStaked(),
999            totalFlowSupply: FlowToken.totalSupply + previousEpochMetadata.totalRewards,
1000            totalRewards: FlowIDTableStaking.getEpochTokenPayout(),
1001        )
1002    }
1003
1004    /// Ends the staking Auction with all the proposed nodes approved
1005    access(account) fun endStakingAuction(): [String] {
1006        return self.borrowStakingAdmin().endStakingAuction()
1007    }
1008
1009    /// Starts the EpochSetup phase and emits the epoch setup event
1010    /// This has to be called directly after `endStakingAuction`
1011    access(account) fun startEpochSetup(proposedNodeIDs: [String], randomSource: String) {
1012
1013        // Holds the node Information of all the approved nodes
1014        var nodeInfoArray: [FlowIDTableStaking.NodeInfo] = []
1015
1016        // Holds node IDs of only collector nodes for QC
1017        var collectorNodeIDs: [String] = []
1018
1019        // Holds node IDs of only consensus nodes for DKG
1020        var consensusNodeIDs: [String] = []
1021
1022        // Get NodeInfo for all the nodes
1023        // Get all the collector and consensus nodes
1024        // to initialize the QC and DKG
1025        for id in proposedNodeIDs {
1026            let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: id)
1027
1028            nodeInfoArray.append(nodeInfo)
1029
1030            if nodeInfo.role == NodeRole.Collector.rawValue {
1031                collectorNodeIDs.append(nodeInfo.id)
1032            }
1033
1034            if nodeInfo.role == NodeRole.Consensus.rawValue {
1035                consensusNodeIDs.append(nodeInfo.id)
1036            }
1037        }
1038
1039        // Organize the collector nodes into clusters
1040        let collectorClusters = self.createCollectorClusters(nodeIDs: collectorNodeIDs)
1041
1042        // Start QC Voting with the supplied clusters
1043        self.borrowClusterQCAdmin().startVoting(clusters: collectorClusters)
1044
1045        // Start DKG with the consensus nodes
1046        self.borrowDKGAdmin().startDKG(nodeIDs: consensusNodeIDs)
1047
1048        let currentEpochMetadata = self.getEpochMetadata(self.currentEpochCounter)!
1049
1050        // Initialize the metadata for the next epoch
1051        // QC and DKG metadata will be filled in later
1052        let proposedEpochMetadata = EpochMetadata(counter: self.proposedEpochCounter(),
1053                                                seed: randomSource,
1054                                                startView: currentEpochMetadata.endView + UInt64(1),
1055                                                endView: currentEpochMetadata.endView + self.configurableMetadata.numViewsInEpoch,
1056                                                stakingEndView: currentEpochMetadata.endView + self.configurableMetadata.numViewsInStakingAuction,
1057                                                totalRewards: 0.0,
1058                                                collectorClusters: collectorClusters,
1059                                                clusterQCs: [],
1060                                                dkgKeys: [])
1061
1062        // Compute the target end time for the next epoch
1063        let timingConfig = self.getEpochTimingConfig()
1064        let proposedTargetDuration = timingConfig.duration
1065        let proposedTargetEndTime = timingConfig.getTargetEndTimeForEpoch(self.proposedEpochCounter())
1066
1067        self.saveEpochMetadata(proposedEpochMetadata)
1068
1069        self.currentEpochPhase = EpochPhase.EPOCHSETUP
1070
1071        let dkgPhase1FinalView = proposedEpochMetadata.stakingEndView + self.configurableMetadata.numViewsInDKGPhase
1072        let dkgPhase2FinalView = dkgPhase1FinalView + self.configurableMetadata.numViewsInDKGPhase
1073        let dkgPhase3FinalView = dkgPhase2FinalView + self.configurableMetadata.numViewsInDKGPhase
1074        emit EpochSetup(counter: proposedEpochMetadata.counter,
1075                        nodeInfo: nodeInfoArray,
1076                        firstView: proposedEpochMetadata.startView,
1077                        finalView: proposedEpochMetadata.endView,
1078                        collectorClusters: collectorClusters,
1079                        randomSource: randomSource,
1080                        DKGPhase1FinalView: dkgPhase1FinalView,
1081                        DKGPhase2FinalView:  dkgPhase2FinalView,
1082                        DKGPhase3FinalView:  dkgPhase3FinalView,
1083                        targetDuration: proposedTargetDuration,
1084                        targetEndTime: proposedTargetEndTime)
1085    }
1086
1087    /// Ends the EpochSetup phase when the QC and DKG are completed
1088    /// and emits the EpochCommit event with the results
1089    access(account) fun startEpochCommit() {
1090        if !FlowClusterQC.votingCompleted() || FlowDKG.dkgCompleted() == nil {
1091            return
1092        }
1093
1094        let clusters = FlowClusterQC.getClusters()
1095
1096        // Holds the quorum certificates for each cluster
1097        var clusterQCs: [FlowClusterQC.ClusterQC] = []
1098
1099        // iterate through all the clusters and create their certificate arrays
1100        for cluster in clusters {
1101            var certificate = cluster.generateQuorumCertificate()
1102                ?? panic("Could not generate the quorum certificate for this cluster")
1103
1104            clusterQCs.append(certificate)
1105        }
1106
1107        // Set cluster QCs in the proposed epoch metadata and stop QC voting
1108        let proposedEpochMetadata = self.getEpochMetadata(self.proposedEpochCounter())!
1109        proposedEpochMetadata.setClusterQCs(qcs: clusterQCs)
1110
1111        // Set DKG result in the proposed epoch metadata and stop DKG
1112        let dkgResult = FlowDKG.dkgCompleted()!
1113        // Construct a partial representation of the DKG result for storage in the epoch metadata.
1114        // See setDKGKeys documentation for context on why the node ID mapping is omitted here.
1115        let dkgPubKeys = [dkgResult.groupPubKey!].concat(dkgResult.pubKeys!)
1116        proposedEpochMetadata.setDKGKeys(keys: dkgPubKeys)
1117        self.saveEpochMetadata(proposedEpochMetadata)
1118
1119        self.currentEpochPhase = EpochPhase.EPOCHCOMMIT
1120
1121        emit EpochCommit(counter: self.proposedEpochCounter(),
1122                            clusterQCs: clusterQCs,
1123                            dkgPubKeys: dkgResult.pubKeys!,
1124                            dkgGroupKey: dkgResult.groupPubKey!,
1125                            dkgIdMapping: dkgResult.idMapping!)
1126    }
1127
1128    /// Borrow a reference to the FlowIDTableStaking Admin resource
1129    access(contract) view fun borrowStakingAdmin(): &FlowIDTableStaking.Admin {
1130        let adminCapability = self.account.storage.copy<Capability>(from: /storage/flowStakingAdminEpochOperations)
1131            ?? panic("Could not get capability from account storage")
1132
1133        // borrow a reference to the staking admin object
1134        let adminRef = adminCapability.borrow<&FlowIDTableStaking.Admin>()
1135            ?? panic("Could not borrow reference to staking admin")
1136
1137        return adminRef
1138    }
1139
1140    /// Borrow a reference to the ClusterQCs Admin resource
1141    access(contract) fun borrowClusterQCAdmin(): &FlowClusterQC.Admin {
1142        let adminCapability = self.account.storage.copy<Capability>(from: /storage/flowQCAdminEpochOperations)
1143            ?? panic("Could not get capability from account storage")
1144
1145        // borrow a reference to the QC admin object
1146        let adminRef = adminCapability.borrow<&FlowClusterQC.Admin>()
1147            ?? panic("Could not borrow reference to QC admin")
1148
1149        return adminRef
1150    }
1151
1152    /// Borrow a reference to the DKG Admin resource
1153    access(contract) fun borrowDKGAdmin(): &FlowDKG.Admin {
1154        let adminCapability = self.account.storage.copy<Capability>(from: /storage/flowDKGAdminEpochOperations)
1155            ?? panic("Could not get capability from account storage")
1156
1157        // borrow a reference to the dkg admin object
1158        let adminRef = adminCapability.borrow<&FlowDKG.Admin>()
1159            ?? panic("Could not borrow reference to dkg admin")
1160
1161        return adminRef
1162    }
1163
1164    /// Makes sure the set of phase lengths (in views) are valid.
1165    /// Sub-phases cannot be greater than the full epoch length.
1166    access(all) view fun isValidPhaseConfiguration(_ auctionLen: UInt64, _ dkgPhaseLen: UInt64, _ epochLen: UInt64): Bool {
1167        return (auctionLen + (3*dkgPhaseLen)) < epochLen
1168    }
1169
1170    /// Randomizes the list of collector node ID and uses a round robin algorithm
1171    /// to assign all collector nodes to equal sized clusters
1172    access(all) fun createCollectorClusters(nodeIDs: [String]): [FlowClusterQC.Cluster] {
1173        pre {
1174            UInt16(nodeIDs.length) >= self.configurableMetadata.numCollectorClusters: "Cannot have less collector nodes than clusters"
1175        }
1176        var shuffledIDs = self.randomize(nodeIDs)
1177
1178        // Holds cluster assignments for collector nodes
1179        let clusters: [FlowClusterQC.Cluster] = []
1180        var clusterIndex: UInt16 = 0
1181        let nodeWeightsDictionary: [{String: UInt64}] = []
1182        while clusterIndex < self.configurableMetadata.numCollectorClusters {
1183            nodeWeightsDictionary.append({})
1184            clusterIndex = clusterIndex + 1
1185        }
1186        clusterIndex = 0
1187
1188        for id in shuffledIDs {
1189
1190            let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: id)
1191
1192            nodeWeightsDictionary[clusterIndex][id] = nodeInfo.initialWeight
1193
1194            // Advance to the next cluster, or back to the first if we have gotten to the last one
1195            clusterIndex = clusterIndex + 1
1196            if clusterIndex == self.configurableMetadata.numCollectorClusters {
1197                clusterIndex = 0
1198            }
1199        }
1200
1201        // Create the clusters Array that is sent to the QC contract
1202        // and emitted in the EpochSetup event
1203        clusterIndex = 0
1204        while clusterIndex < self.configurableMetadata.numCollectorClusters {
1205            clusters.append(FlowClusterQC.Cluster(index: clusterIndex, nodeWeights: nodeWeightsDictionary[clusterIndex]!))
1206            clusterIndex = clusterIndex + 1
1207        }
1208
1209        return clusters
1210    }
1211
1212    /// A function to generate a random permutation of arr[]
1213    /// using the fisher yates shuffling algorithm
1214    access(all) fun randomize(_ array: [String]): [String] {
1215
1216        var i = array.length - 1
1217
1218        // Start from the last element and swap one by one. We don't
1219        // need to run for the first element that's why i > 0
1220        while i > 0
1221        {
1222            // Pick a random index from 0 to i
1223            /// TODO: this naive implementation is unnecessarily costly at runtime -- update with revertibleRandom<UInt64>(i+1) when available
1224            var randomNum = revertibleRandom<UInt64>()
1225            var randomIndex = randomNum % UInt64(i + 1)
1226
1227            // Swap arr[i] with the element at random index
1228            var temp = array[i]
1229            array[i] = array[randomIndex]
1230            array[randomIndex] = temp
1231
1232            i = i - 1
1233        }
1234
1235        return array
1236    }
1237
1238    /// Collector nodes call this function to get their QC Voter resource
1239    /// in order to participate the the QC generation for their cluster
1240    access(all) fun getClusterQCVoter(nodeStaker: &FlowIDTableStaking.NodeStaker): @FlowClusterQC.Voter {
1241        let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: nodeStaker.id)
1242
1243        assert (
1244            nodeInfo.role == NodeRole.Collector.rawValue,
1245            message: "Node operator must be a collector node to get a QC Voter object"
1246        )
1247
1248        let clusterQCAdmin = self.borrowClusterQCAdmin()
1249        return <-clusterQCAdmin.createVoter(nodeID: nodeStaker.id, stakingKey: nodeInfo.stakingKey)
1250    }
1251
1252    /// Consensus nodes call this function to get their DKG Participant resource
1253    /// in order to participate in the DKG for the next epoch
1254    access(all) fun getDKGParticipant(nodeStaker: &FlowIDTableStaking.NodeStaker): @FlowDKG.Participant {
1255        let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: nodeStaker.id)
1256
1257        assert (
1258            nodeInfo.role == NodeRole.Consensus.rawValue,
1259            message: "Node operator must be a consensus node to get a DKG Participant object"
1260        )
1261
1262        let dkgAdmin = self.borrowDKGAdmin()
1263        return <-dkgAdmin.createParticipant(nodeID: nodeStaker.id)
1264    }
1265
1266    /// Returns the metadata that is able to be configured by the admin
1267    access(all) view fun getConfigMetadata(): Config {
1268        return self.configurableMetadata
1269    }
1270
1271    /// Returns the config for epoch timing, which can be configured by the admin.
1272    /// Conceptually, this should be part of `Config`, however it was added later in an
1273    /// upgrade which requires new fields be specified separately from existing structs.
1274    access(all) fun getEpochTimingConfig(): EpochTimingConfig {
1275        return self.account.storage.copy<EpochTimingConfig>(from: /storage/flowEpochTimingConfig)!
1276    }
1277
1278    /// The proposed Epoch counter is always the current counter plus 1
1279    access(all) view fun proposedEpochCounter(): UInt64 {
1280        return self.currentEpochCounter + 1 as UInt64
1281    }
1282
1283    access(all) fun automaticRewardsEnabled(): Bool {
1284        return self.account.storage.copy<Bool>(from: /storage/flowAutomaticRewardsEnabled) ?? false
1285    }
1286
1287    /// Gets the current amount of bonus tokens left to be destroyed
1288    /// Bonus tokens are tokens that were allocated as a decentralization incentive
1289    /// that are currently in the process of being destroyed. The presence of bonus
1290    /// tokens throws off the intended calculation for the FLOW inflation rate
1291    /// so they are not included in that calculation
1292    /// Eventually, all the bonus tokens will be destroyed and
1293    /// this will not be needed anymore
1294    access(all) fun getBonusTokens(): UFix64 {
1295        return self.account.storage.copy<UFix64>(from: /storage/FlowBonusTokenAmount)
1296                ?? 0.0
1297    }
1298
1299    init (currentEpochCounter: UInt64,
1300          numViewsInEpoch: UInt64,
1301          numViewsInStakingAuction: UInt64,
1302          numViewsInDKGPhase: UInt64,
1303          numCollectorClusters: UInt16,
1304          FLOWsupplyIncreasePercentage: UFix64,
1305          randomSource: String,
1306          collectorClusters: [FlowClusterQC.Cluster],
1307          clusterQCs: [FlowClusterQC.ClusterQC],
1308          dkgPubKeys: [String]) {
1309        pre {
1310            FlowEpoch.isValidPhaseConfiguration(numViewsInStakingAuction, numViewsInDKGPhase, numViewsInEpoch):
1311                "Invalid startView and endView configuration"
1312        }
1313
1314        self.configurableMetadata = Config(numViewsInEpoch: numViewsInEpoch,
1315                                           numViewsInStakingAuction: numViewsInStakingAuction,
1316                                           numViewsInDKGPhase: numViewsInDKGPhase,
1317                                           numCollectorClusters: numCollectorClusters,
1318                                           FLOWsupplyIncreasePercentage: FLOWsupplyIncreasePercentage)
1319
1320        // Set a reasonable default for the epoch timing config targeting 1 block per second
1321        let defaultEpochTimingConfig = EpochTimingConfig(
1322            duration: numViewsInEpoch,
1323            refCounter: currentEpochCounter,
1324            refTimestamp: UInt64(getCurrentBlock().timestamp)+numViewsInEpoch)
1325        FlowEpoch.account.storage.save(defaultEpochTimingConfig, to: /storage/flowEpochTimingConfig)
1326
1327        self.currentEpochCounter = currentEpochCounter
1328        self.currentEpochPhase = EpochPhase.STAKINGAUCTION
1329        self.adminStoragePath = /storage/flowEpochAdmin
1330        self.heartbeatStoragePath = /storage/flowEpochHeartbeat
1331        self.metadataStoragePath = /storage/flowEpochMetadata
1332
1333        let epochMetadata: {UInt64: EpochMetadata} = {}
1334
1335        self.account.storage.save(epochMetadata, to: self.metadataStoragePath)
1336
1337        self.account.storage.save(<-create Admin(), to: self.adminStoragePath)
1338        self.account.storage.save(<-create Heartbeat(), to: self.heartbeatStoragePath)
1339
1340        // Create a private capability to the staking admin and store it in a different path
1341        // On Mainnet and Testnet, the Admin resources are stored in the service account, rather than here.
1342        // As a default, we store both the admin resources, and the capabilities linking to those resources, in the same account.
1343        // This ensures that this constructor produces a state which is compatible with the system chunk
1344        // so that newly created networks are functional without additional resource manipulation.
1345        let stakingAdminCapability = self.account.capabilities.storage.issue<&FlowIDTableStaking.Admin>(FlowIDTableStaking.StakingAdminStoragePath)
1346        self.account.storage.save<Capability<&FlowIDTableStaking.Admin>>(stakingAdminCapability, to: /storage/flowStakingAdminEpochOperations)
1347
1348        // Create a private capability to the qc admin
1349        // and store it in a different path
1350        let qcAdminCapability = self.account.capabilities.storage.issue<&FlowClusterQC.Admin>(FlowClusterQC.AdminStoragePath)
1351        self.account.storage.save<Capability<&FlowClusterQC.Admin>>(qcAdminCapability, to: /storage/flowQCAdminEpochOperations)
1352
1353        // Create a private capability to the dkg admin
1354        // and store it in a different path
1355        let dkgAdminCapability = self.account.capabilities.storage.issue<&FlowDKG.Admin>(FlowDKG.AdminStoragePath)
1356        self.account.storage.save<Capability<&FlowDKG.Admin>>(dkgAdminCapability, to: /storage/flowDKGAdminEpochOperations)
1357
1358        self.borrowStakingAdmin().startStakingAuction()
1359
1360        let currentBlock = getCurrentBlock()
1361
1362        let firstEpochMetadata = EpochMetadata(counter: self.currentEpochCounter,
1363                    seed: randomSource,
1364                    startView: currentBlock.view,
1365                    endView: currentBlock.view + self.configurableMetadata.numViewsInEpoch - 1,
1366                    stakingEndView: currentBlock.view + self.configurableMetadata.numViewsInStakingAuction - 1,
1367                    totalRewards: FlowIDTableStaking.getEpochTokenPayout(),
1368                    collectorClusters: collectorClusters,
1369                    clusterQCs: clusterQCs,
1370                    dkgKeys: dkgPubKeys)
1371
1372        self.saveEpochMetadata(firstEpochMetadata)
1373    }
1374}
1375