Smart Contract
FlowDKG
A.8624b52f9ddcd04a.FlowDKG
1/*
2*
3* Manages the process of generating a group key with the participation of all the consensus nodes
4* for the upcoming epoch.
5*
6* When consensus nodes are first confirmed, they can request a Participant object from this contract
7* They'll use this object for every subsequent epoch that they are a staked consensus node.
8*
9* At the beginning of each EpochSetup phase, the admin initializes this contract with
10* the list of consensus nodes for the upcoming epoch. Each consensus node
11* can post as many messages as they want to the DKG "whiteboard" with the `Participant.postMessage()` method,
12* but each node can only submit a final submission once per epoch via the `Participant.sendFinalSubmission() method.
13*
14* Once a >50% threshold of consensus nodes have submitted the exact same set of keys,
15* the DKG phase is technically finished.
16* Anyone can query the state of the submissions with the FlowDKG.getFinalSubmissions()
17* or FlowDKG.dkgCompleted() methods.
18* Consensus nodes can continue to submit final messages even after the required amount have been submitted though.
19*
20* This contract is a member of a series of epoch smart contracts which coordinates the
21* process of transitioning between epochs in Flow.
22*/
23
24access(all) contract FlowDKG {
25
26 // ================================================================================
27 // DKG EVENTS
28 // ================================================================================
29
30 /// Emitted when the admin enables the DKG
31 access(all) event StartDKG()
32
33 /// Emitted when the admin ends the DKG (one DKG instance).
34 /// The event includes the canonical result submission if the DKG succeeded,
35 /// or nil if the DKG failed or was stopped before completion.
36 access(all) event EndDKG(finalSubmission: ResultSubmission?)
37
38 /// Emitted when a consensus node has posted a message to the DKG whiteboard
39 access(all) event BroadcastMessage(nodeID: String, content: String)
40
41 // ================================================================================
42 // CONTRACT VARIABLES
43 // ================================================================================
44
45 /// The length of keys that have to be submitted as a final submission
46 access(all) let submissionKeyLength: Int
47
48 /// Indicates if the DKG is enabled or not
49 access(all) var dkgEnabled: Bool
50
51 /// Indicates if a Participant resource has already been claimed by a node ID
52 /// from the identity table contract
53 /// Node IDs have to claim a participant once
54 /// one node will use the same specific ID and Participant resource for all time
55 /// `nil` or false means that there is no voting capability for the node ID
56 /// true means that the participant capability has been claimed by the node
57 access(account) var nodeClaimed: {String: Bool}
58
59 /// Record of whiteboard messages for the current epoch
60 /// This is reset at the beginning of every DKG instance (once per epoch)
61 access(account) var whiteboardMessages: [Message]
62
63 // DEPRECATED FIELDS (replaced by SubmissionTracker)
64 access(account) var finalSubmissionByNodeID: {String: [String?]} // deprecated and unused
65 access(account) var uniqueFinalSubmissions: [[String?]] // deprecated and unused
66 access(account) var uniqueFinalSubmissionCount: {Int: UInt64} // deprecated and unused
67
68 // ================================================================================
69 // CONTRACT CONSTANTS
70 // ================================================================================
71
72 // Canonical paths for admin and participant resources
73 access(all) let AdminStoragePath: StoragePath
74 access(all) let ParticipantStoragePath: StoragePath
75 access(all) let ParticipantPublicPath: PublicPath
76
77 /// Struct to represent a single whiteboard message
78 access(all) struct Message {
79
80 /// The ID of the node who submitted the message
81 access(all) let nodeID: String
82
83 /// The content of the message
84 /// We make no assumptions or assertions about the content of the message
85 access(all) let content: String
86
87 init(nodeID: String, content: String) {
88 self.nodeID = nodeID
89 self.content = content
90 }
91 }
92
93 // Checks whether the ResultSubmission constructor arguments satisfy Invariant (1):
94 // (1) either all fields are nil (empty submission) or no fields are nil
95 // A valid empty submission has all fields nil, and is used when the submittor locally failed the DKG.
96 // All non-empty submissions must have all non-nil fields.
97 access(all) view fun checkEmptySubmissionInvariant(groupPubKey: String?, pubKeys: [String]?, idMapping: {String:Int}?): Bool {
98 // If any fields are nil, then this represents a empty submission and all fields must be nil
99 if groupPubKey == nil && pubKeys == nil && idMapping == nil {
100 return true
101 }
102 // Otherwise, all fields must be non-nil
103 return groupPubKey != nil && pubKeys != nil && idMapping != nil
104 }
105
106 // Checks the group public key (part of a ResultSubmission) for validity.
107 // A valid public key in this context is either: (1) a hex-encoded 96 bytes, or (2) nil.
108 access(all) view fun isValidGroupKey(_ groupKey: String?): Bool {
109 if groupKey == nil {
110 // By Invariant (1), This is a nil/empty submission (see checkEmptySubmissionInvariant)
111 return true
112 }
113 return groupKey!.length == FlowDKG.submissionKeyLength
114 }
115
116 // Checks a list of participant public keys (part of a ResultSubmission) for validity.
117 // A valid public key in this context is either: (1) a hex-encoded 96 bytes, or (2) nil.
118 access(all) view fun isValidPubKeys(_ pubKeys: [String]?): Bool {
119 if pubKeys == nil {
120 // By Invariant (1), This is a nil/empty submission (see checkEmptySubmissionInvariant)
121 return true
122 }
123 for key in pubKeys! {
124 // keys must be exactly 96 bytes
125 if key.length != FlowDKG.submissionKeyLength {
126 return false
127 }
128 }
129 return true
130 }
131
132 // Checks that an id mapping (part of ResultSubmission) contains one entry per public key.
133 access(all) view fun isValidIDMapping(pubKeys: [String]?, idMapping: {String: Int}?): Bool {
134 if pubKeys == nil {
135 // By Invariant (1), This is a nil/empty submission (see checkEmptySubmissionInvariant)
136 return true
137 }
138 return pubKeys!.length == idMapping!.keys.length
139 }
140
141 // ResultSubmission represents a result submission from one DKG participant.
142 // Each submission includes a group public key and an ordered list of participant public keys.
143 // A submission may be empty, in which case all fields are nil - this is used to represent a local DKG failure.
144 // All non-empty submission will have all non-nil, valid fields.
145 // By convention, all keys are encoded as lowercase hex strings, though it is not enforced here.
146 //
147 // INVARIANTS:
148 // (1) either all fields are nil (empty submission) or no fields are nil
149 // (2) all key strings are the expected length for BLS public keys
150 // (3) all non-empty submissions have one participant key per node ID in idMapping
151 access(all) struct ResultSubmission {
152 // The group public key for the beacon committee resulting from the DKG.
153 access(all) let groupPubKey: String?
154 // An ordered list of individual public keys for the beacon committee resulting from the DKG.
155 access(all) let pubKeys: [String]?
156 // A mapping from node ID to DKG index.
157 // There must be exactly one key per authorized DKG participant.
158 // The set of values should form the set {0, 1, 2, ... n-1}, where n is the number of
159 // authorized DKG participant, however this is not enforced here.
160 access(all) let idMapping: {String:Int}?
161
162 init(groupPubKey: String?, pubKeys: [String]?, idMapping: {String:Int}?) {
163 pre {
164 FlowDKG.checkEmptySubmissionInvariant(groupPubKey: groupPubKey, pubKeys: pubKeys, idMapping: idMapping):
165 "FlowDKG.ResultSubmission.init: violates empty submission invariant - ResultSubmission fields must be all nil or all non-nil"
166 FlowDKG.isValidGroupKey(groupPubKey):
167 "FlowDKG.ResultSubmission.init: invalid group key - must be nil or hex-encoded 96-byte string"
168 FlowDKG.isValidPubKeys(pubKeys):
169 "FlowDKG.ResultSubmission.init: invalid participant key - must be nil or hex-encoded 96-byte string"
170 FlowDKG.isValidIDMapping(pubKeys: pubKeys, idMapping: idMapping):
171 "FlowDKG.ResultSubmission.init: invalid ID mapping - must be same size as pubKeys"
172 }
173 self.groupPubKey = groupPubKey
174 self.pubKeys = pubKeys
175 self.idMapping = idMapping
176 }
177
178 // Returns true if this ResultSubmission instance represents an empty submission.
179 // Since the constructor enforces invariant (1), we only need to check one field.
180 access(all) view fun isEmpty(): Bool {
181 return self.groupPubKey == nil
182 }
183
184 // Checks whether the input is equivalent to this ResultSubmission.
185 // Submissions must have identical keys, in the same order, and identical index mappings.
186 // Empty submissions are considered equal.
187 access(all) fun equals(_ other: FlowDKG.ResultSubmission): Bool {
188 if self.groupPubKey != other.groupPubKey {
189 return false
190 }
191 if self.pubKeys != other.pubKeys {
192 return false
193 }
194 if self.idMapping != other.idMapping {
195 return false
196 }
197 return true
198 }
199
200 // Checks whether this ResultSubmission COULD BE a valid submission for the given DKG committee.
201 access(all) view fun isValidForCommittee(authorized: [String]): Bool {
202 if self.isEmpty() {
203 return true
204 }
205
206 // Must have one public key per DKG participant
207 if authorized.length != self.pubKeys!.length {
208 return false
209 }
210 // Must have a DKG index mapped for each DKG participant
211 for nodeID in authorized {
212 if self.idMapping![nodeID] == nil {
213 return false
214 }
215 }
216 return true
217 }
218 }
219
220 // SubmissionTracker tracks all state related to result submissions.
221 // It is intended for internal use by the FlowDKG contract as a private singleton.
222 // Future modifications MUST NOT make this singleton instance of SubmissionTracker publicly accessible.
223 access(all) struct SubmissionTracker {
224 // Set of authorized participants for this DKG instance (the "DKG committee")
225 // Keys are node IDs, as registered in the FlowIDTableStaking contract.
226 // NOTE: all values are true - this is structured as a map for O(1) lookup; conceptually it is a set.
227 access(all) var authorized: {String: Bool}
228 // List of unique submissions, in submission order
229 access(all) var uniques: [FlowDKG.ResultSubmission]
230 // Maps node ID of authorized participants to an index within "uniques"
231 access(all) var byNodeID: {String: Int}
232 // Maps index within "uniques" to count of submissions
233 access(all) var counts: {Int: UInt64}
234
235 init() {
236 self.authorized = {}
237 self.uniques = []
238 self.byNodeID = {}
239 self.counts = {}
240 }
241
242 // Called each time a new DKG instance starts, to reset SubmissionTracker state.
243 // NOTE: we could also re-instantiate a new SubmissionTracker each time, but this pattern makes
244 // storage management simpler because you never load/save the tracker after construction or upgrade.
245 access(all) fun reset(nodeIDs: [String]) {
246 self.authorized = {}
247 self.uniques = []
248 self.byNodeID = {}
249 self.counts = {}
250 for nodeID in nodeIDs {
251 self.authorized[nodeID] = true
252 }
253 }
254
255 // Adds the result submission for the DKG participant identified by nodeID.
256 // The DKG participant must be authorized for the current epoch, and each participant may submit only once.
257 // CAUTION: This method should only be called by Participant, which enforces that Participant.nodeID is passed in.
258 access(all) fun addSubmission(nodeID: String, submission: ResultSubmission) {
259 pre {
260 self.authorized[nodeID] != nil:
261 "FlowDKG.addSubmission: Submittor (node ID: "
262 .concat(nodeID)
263 .concat(") is not authorized for this DKG instance.")
264 self.byNodeID[nodeID] == nil:
265 "FlowDKG.SubmissionTracker.addSubmission: Submittor (node ID: "
266 .concat(nodeID)
267 .concat(") may only submit once and has already submitted")
268 submission.isValidForCommittee(authorized: self.authorized.keys):
269 "FlowDKG.SubmissionTracker.addSubmission: Submission must contain exactly one public key per authorized participant"
270 }
271
272 // 1) Check whether this submission is equivalent to an existing submission (typical case)
273 var submissionIndex = 0
274 while submissionIndex < self.uniques.length {
275 if submission.equals(self.uniques[submissionIndex]) {
276 self.byNodeID[nodeID] = submissionIndex
277 self.counts[submissionIndex] = self.counts[submissionIndex]! + 1
278 return
279 }
280 submissionIndex = submissionIndex + 1
281 }
282
283 // 2) This submission differs from all existing submissions (or is the first), so add a new unique submission.
284 // NOTE: at this point submissionIndex == self.uniques.length (the index of the submission we are adding)
285 self.uniques.append(submission)
286 self.byNodeID[nodeID] = submissionIndex
287 self.counts[submissionIndex] = 1
288 }
289
290 // Returns the non-empty result which was submitted by at least threshold+1 DKG participants.
291 // If no result received enough submissions, returns nil.
292 // Callers should use a threshold that is greater than or equal to half the DKG committee size.
293 access(all) view fun submissionExceedsThreshold(_ threshold: UInt64): ResultSubmission? {
294 post {
295 result == nil || !result!.isEmpty():
296 "FlowDKG.SubmissionTracker.submissionExceedsThreshold: If a submission is returned, it must be non-empty"
297 }
298 var submissionIndex = 0
299 while submissionIndex < self.uniques.length {
300 if self.counts[submissionIndex]! > threshold {
301 let submission = self.uniques[submissionIndex]
302 // exclude empty submissions, as these are ineligible for considering the DKG completed
303 if submission.isEmpty() {
304 submissionIndex = submissionIndex + 1
305 continue
306 }
307 // return the non-empty submission submitted by >threshold DKG participants
308 return submission
309 }
310 submissionIndex = submissionIndex + 1
311 }
312 return nil
313 }
314
315 // Returns the result submitted by the node with the given ID.
316 // Returns nil if the node is not authorized or has not submitted for the current DKG.
317 access(all) view fun getSubmissionByNodeID(_ nodeID: String): ResultSubmission? {
318 if let submissionIndex = self.byNodeID[nodeID] {
319 return self.uniques[submissionIndex]
320 }
321 return nil
322 }
323
324 // Returns all unique submissions for the current DKG instance.
325 access(all) view fun getUniqueSubmissions(): [ResultSubmission] {
326 return self.uniques
327 }
328 }
329
330 /// The Participant resource is generated for each consensus node when they register.
331 /// Each resource instance is good for all future potential epochs, but will
332 /// only be valid if the node operator has been confirmed as a consensus node for the next epoch.
333 access(all) resource Participant {
334
335 /// The node ID of the participant
336 access(all) let nodeID: String
337
338 init(nodeID: String) {
339 pre {
340 FlowDKG.participantIsClaimed(nodeID) == nil:
341 "FlowDKG.Participant.init: Cannot create Participant resource for a node ID ("
342 .concat(nodeID)
343 .concat(") that has already been claimed")
344 }
345 self.nodeID = nodeID
346 FlowDKG.nodeClaimed[nodeID] = true
347 }
348
349 /// Posts a whiteboard message to the contract
350 access(all) fun postMessage(_ content: String) {
351 pre {
352 FlowDKG.participantIsRegistered(self.nodeID):
353 "FlowDKG.Participant.postMessage: Cannot post whiteboard message. Sender (node ID: "
354 .concat(self.nodeID)
355 .concat(") is not registered for the current DKG instance")
356 content.length > 0:
357 "FlowDKG.Participant.postMessage: Cannot post empty message to the whiteboard"
358 FlowDKG.dkgEnabled:
359 "FlowDKG.Participant.postMessage: Cannot post whiteboard message when DKG is disabled"
360 }
361
362 // create the message struct
363 let message = Message(nodeID: self.nodeID, content: content)
364
365 // add the message to the message record
366 FlowDKG.whiteboardMessages.append(message)
367
368 emit BroadcastMessage(nodeID: self.nodeID, content: content)
369
370 }
371
372 /// Sends the final key vector submission.
373 /// Can only be called by consensus nodes that are registered
374 /// and can only be called once per consensus node per epoch
375 access(all) fun sendFinalSubmission(_ submission: ResultSubmission) {
376 pre {
377 FlowDKG.dkgEnabled:
378 "FlowDKG.Participant.postMessage: Cannot send final submission when DKG is disabled"
379 }
380 FlowDKG.borrowSubmissionTracker().addSubmission(nodeID: self.nodeID, submission: submission)
381 }
382 }
383
384 /// Interface that only contains operations that are part
385 /// of the regular automated functioning of the epoch process
386 /// These are accessed by the `FlowEpoch` contract through a capability
387 access(all) resource interface EpochOperations {
388 access(all) fun createParticipant(nodeID: String): @Participant
389 access(all) fun startDKG(nodeIDs: [String])
390 access(all) fun endDKG()
391 access(all) fun forceEndDKG()
392 }
393
394 /// The Admin resource provides the ability to begin and end voting for an epoch
395 access(all) resource Admin: EpochOperations {
396
397 /// Sets the optional safe DKG success threshold
398 /// Set the threshold to nil if it isn't needed
399 access(all) fun setSafeSuccessThreshold(newThresholdPercentage: UFix64?) {
400 pre {
401 !FlowDKG.dkgEnabled:
402 "FlowDKG.Admin.setSafeSuccessThreshold: Cannot set the DKG success threshold while the DKG is enabled"
403 newThresholdPercentage == nil || newThresholdPercentage! < 1.0:
404 "FlowDKG.Admin.setSafeSuccessThreshold: Invalid input. Safe threshold percentage must be in [0,1)"
405 }
406
407 FlowDKG.account.storage.load<UFix64>(from: /storage/flowDKGSafeThreshold)
408
409 // If newThresholdPercentage is nil, we exit here. Since we loaded from
410 // storage previously, this results in /storage/flowDKGSafeThreshold being empty
411 if let percentage = newThresholdPercentage {
412 FlowDKG.account.storage.save<UFix64>(percentage, to: /storage/flowDKGSafeThreshold)
413 }
414 }
415
416 /// Creates a new Participant resource for a consensus node
417 access(all) fun createParticipant(nodeID: String): @Participant {
418 let participant <-create Participant(nodeID: nodeID)
419 FlowDKG.nodeClaimed[nodeID] = true
420 return <-participant
421 }
422
423 /// Resets all the fields for tracking the current DKG process
424 /// and sets the given node IDs as registered
425 access(all) fun startDKG(nodeIDs: [String]) {
426 pre {
427 FlowDKG.dkgEnabled == false:
428 "FlowDKG.Admin.startDKG: Cannot start the DKG when it is already running"
429 }
430
431 // Clear all per-instance DKG state
432 FlowDKG.whiteboardMessages = []
433 FlowDKG.borrowSubmissionTracker().reset(nodeIDs: nodeIDs)
434 FlowDKG.uniqueFinalSubmissions = [] // deprecated and unused
435 FlowDKG.uniqueFinalSubmissionCount = {} // deprecated and unused
436
437 FlowDKG.dkgEnabled = true
438
439 emit StartDKG()
440 }
441
442 /// Disables the DKG and closes the opportunity for messages and submissions
443 /// until the next time the DKG is enabled
444 access(all) fun endDKG() {
445 pre {
446 FlowDKG.dkgEnabled == true:
447 "FlowDKG.Admin.endDKG: Cannot end the DKG when it is already disabled"
448 }
449 let dkgResult = FlowDKG.dkgCompleted()
450 assert(
451 dkgResult != nil,
452 message: "FlowDKG.Admin.endDKG: Cannot end the DKG without a canonical final ResultSubmission"
453 )
454
455 FlowDKG.dkgEnabled = false
456
457 emit EndDKG(finalSubmission: dkgResult)
458 }
459
460 /// Ends the DKG without checking if it is completed
461 /// Should only be used if something goes wrong with the DKG,
462 /// the protocol halts, or needs to be reset for some reason
463 access(all) fun forceEndDKG() {
464 FlowDKG.dkgEnabled = false
465
466 emit EndDKG(finalSubmission: FlowDKG.dkgCompleted())
467 }
468 }
469
470 /// Returns true if a node is registered as a consensus node for the proposed epoch
471 access(all) view fun participantIsRegistered(_ nodeID: String): Bool {
472 return FlowDKG.mustBorrowSubmissionTracker().authorized[nodeID] != nil
473 }
474
475 /// Returns true if a consensus node has claimed their Participant resource
476 /// which is valid for all future epochs where the node is registered
477 access(all) view fun participantIsClaimed(_ nodeID: String): Bool? {
478 return FlowDKG.nodeClaimed[nodeID]
479 }
480
481 /// Gets an array of all the whiteboard messages
482 /// that have been submitted by all nodes in the DKG
483 access(all) view fun getWhiteBoardMessages(): [Message] {
484 return self.whiteboardMessages
485 }
486
487 /// Returns whether this node has successfully submitted a final submission for this epoch.
488 access(all) view fun nodeHasSubmitted(_ nodeID: String): Bool {
489 return self.mustBorrowSubmissionTracker().byNodeID[nodeID] != nil
490 }
491
492 /// Gets the specific final submission for a node ID
493 /// If the node hasn't submitted or registered, this returns `nil`
494 access(all) view fun getNodeFinalSubmission(_ nodeID: String): ResultSubmission? {
495 return self.mustBorrowSubmissionTracker().getSubmissionByNodeID(nodeID)
496 }
497
498 /// Get the list of all the consensus node IDs participating
499 access(all) view fun getConsensusNodeIDs(): [String] {
500 return *self.mustBorrowSubmissionTracker().authorized.keys
501 }
502
503 /// Get the array of all the unique final submissions
504 access(all) view fun getFinalSubmissions(): [ResultSubmission] {
505 return self.mustBorrowSubmissionTracker().getUniqueSubmissions()
506 }
507
508 /// Get the count of the final submissions array
509 access(all) view fun getFinalSubmissionCount(): {Int: UInt64} {
510 return *self.mustBorrowSubmissionTracker().counts
511 }
512
513 /// Gets the native threshold that the submission count needs to exceed to be considered complete [t=floor((n-1)/2)]
514 /// This function returns the NON-INCLUSIVE lower bound of honest participants.
515 /// For the DKG to succeed, the number of honest participants must EXCEED this threshold value.
516 ///
517 /// Example:
518 /// We have 10 DKG nodes (n=10)
519 /// The threshold value is t=floor(10-1)/2) (t=4)
520 /// There must be AT LEAST 5 honest nodes for the DKG to succeed
521 /// The function must match the threshold computation on the protocol side: https://github.com/onflow/flow-go/blob/master/module/signature/threshold.go#L7
522 access(all) view fun getNativeSuccessThreshold(): UInt64 {
523 let n = self.getConsensusNodeIDs().length
524 // avoid initializing the threshold to 0 when n=2
525 if n == 2 {
526 return 1
527 }
528 return UInt64((n-1)/2)
529 }
530
531 /// Gets the safe threshold that the submission count needs to exceed to be considered complete.
532 /// (always greater than or equal to the native success threshold)
533 ///
534 /// This function returns the NON-INCLUSIVE lower bound of honest participants. If this function
535 /// returns threshold t, there must be AT LEAST t+1 honest nodes for the DKG to succeed.
536 access(all) view fun getSafeSuccessThreshold(): UInt64 {
537 var threshold = self.getNativeSuccessThreshold()
538
539 // Get the safety rate percentage
540 if let safetyRate = self.getSafeThresholdPercentage() {
541
542 let safeThreshold = UInt64(safetyRate * UFix64(self.getConsensusNodeIDs().length))
543
544 if safeThreshold > threshold {
545 threshold = safeThreshold
546 }
547 }
548
549 return threshold
550 }
551
552 /// Gets the safe threshold percentage. This value must be either nil (semantically: 0) or in [0, 1.0)
553 /// This safe threshold is used to artificially increase the DKG participation requirements to
554 /// ensure a lower-bound number of Random Beacon Committee members (beyond the bare minimum required
555 /// by the DKG protocol).
556 access(all) view fun getSafeThresholdPercentage(): UFix64? {
557 let safetyRate = self.account.storage.copy<UFix64>(from: /storage/flowDKGSafeThreshold)
558 return safetyRate
559 }
560
561 // Borrows the singleton SubmissionTracker from storage, creating it if none exists.
562 access(contract) fun borrowSubmissionTracker(): &FlowDKG.SubmissionTracker {
563 // The singleton SubmissionTracker already exists in storage - return a reference to it.
564 if let tracker = self.account.storage.borrow<&SubmissionTracker>(from: /storage/flowDKGFinalSubmissionTracker) {
565 return tracker
566 }
567 // The singleton SubmissionTracker has not been created yet - create it and return a reference.
568 // This codepath should be executed at most once per FlowDKG instance and only if it was upgraded from an older version.
569 self.account.storage.save(SubmissionTracker(), to: /storage/flowDKGFinalSubmissionTracker)
570 return self.mustBorrowSubmissionTracker()
571 }
572
573 // Borrows the singleton SubmissionTracker from storage; panics if none exists.
574 access(contract) view fun mustBorrowSubmissionTracker(): &FlowDKG.SubmissionTracker {
575 return self.account.storage.borrow<&SubmissionTracker>(from: /storage/flowDKGFinalSubmissionTracker) ??
576 panic("FlowDKG.mustBorrowSubmissionTracker: Critical invariant violated! No SubmissionTracker instance stored at /storage/flowDKGFinalSubmissionTracker")
577 }
578
579 /// Returns the final set of keys if any one set of keys has strictly more than (nodes-1)/2 submissions
580 /// Returns nil if not found (incomplete)
581 access(all) fun dkgCompleted(): ResultSubmission? {
582 if !self.dkgEnabled { return nil }
583
584 let threshold = self.getSafeSuccessThreshold()
585 return self.borrowSubmissionTracker().submissionExceedsThreshold(threshold)
586 }
587
588 init() {
589 self.submissionKeyLength = 192 // 96 bytes, hex-encoded
590
591 self.AdminStoragePath = /storage/flowEpochsDKGAdmin
592 self.ParticipantStoragePath = /storage/flowEpochsDKGParticipant
593 self.ParticipantPublicPath = /public/flowEpochsDKGParticipant
594
595 self.dkgEnabled = false
596
597 self.finalSubmissionByNodeID = {} // deprecated
598 self.uniqueFinalSubmissionCount = {} // deprecated
599 self.uniqueFinalSubmissions = [] // deprecated
600
601 self.nodeClaimed = {}
602 self.whiteboardMessages = []
603
604 self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
605 self.account.storage.save(SubmissionTracker(), to: /storage/flowDKGFinalSubmissionTracker)
606 }
607}
608
609