Smart Contract
FlowEpoch
A.8624b52f9ddcd04a.FlowEpoch
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