Smart Contract

FlowDKG

A.8624b52f9ddcd04a.FlowDKG

Deployed

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

Dependents

0 imports
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