Smart Contract
FlowClusterQC
A.8624b52f9ddcd04a.FlowClusterQC
1
2/*
3*
4* Manages the process of collecting votes for the root quorum certificate of the upcoming
5* epoch for all collection node clusters assigned for the upcoming epoch.
6*
7* When collector nodes are first registered, they can request a Voter object from this contract.
8* They'll use this object for every subsequent epoch where they are a staked collector node.
9*
10* At the beginning of each EpochSetup phase, the admin initializes this contract with
11* the collector clusters for the upcoming epoch. Each collector node has a single vote
12* that is allocated for them and they can only call their `vote` function once.
13*
14* Once all the clusters have received enough identical votes to surpass their weight threshold,
15* The QC generation phase is finished and the admin will end the voting.
16* At any point, anyone can query the voting information for the clusters
17* by using the `getClusters` function.
18*
19* This contract is a member of a series of epoch smart contracts which coordinates the
20* process of transitioning between epochs in Flow.
21*/
22
23import Crypto
24
25access(all) contract FlowClusterQC {
26
27 // ================================================================================
28 // CONTRACT VARIABLES
29 // ================================================================================
30
31 /// Indicates whether votes are currently being collected.
32 /// If false, no node operator will be able to submit votes
33 access(all) var inProgress: Bool
34
35 /// The collection node clusters for the current epoch
36 access(account) var clusters: [Cluster]
37
38 /// Indicates if a voter resource has already been claimed by a node ID
39 /// from the identity table contract
40 /// Node IDs have to claim a voter once
41 /// one node will use the same specific ID and Voter resource for all time
42 /// `nil` means that there is no voting capability for the node ID
43 /// false means that a voter capability for the ID, but it hasn't been claimed
44 /// true means that the voter capability has been claimed by the node
45 access(account) var voterClaimed: {String: Bool}
46
47 /// Indicates what cluster a node is in for the current epoch
48 /// Value is a cluster index
49 access(contract) var nodeCluster: {String: UInt16}
50
51 // ================================================================================
52 // CONTRACT CONSTANTS
53 // ================================================================================
54
55 /// Canonical paths for admin and voter resources
56 access(all) let AdminStoragePath: StoragePath
57 access(all) let VoterStoragePath: StoragePath
58
59 /// Represents a collection node cluster for a given epoch.
60 access(all) struct Cluster {
61
62 /// The index of the cluster within the cluster array. This uniquely identifies
63 /// a cluster for a given epoch
64 access(all) let index: UInt16
65
66 /// Weights for each nodeID in the cluster
67 access(all) let nodeWeights: {String: UInt64}
68
69 /// The total node weight of all the nodes in the cluster
70 access(all) let totalWeight: UInt64
71
72 /// Votes that nodes claim at the beginning of each EpochSetup phase
73 /// Key is node ID from the identity table contract
74 /// Vote resources without signatures or messages for each node are stored here
75 /// at the beginning of each epoch setup phase.
76 /// When a node submits a vote, the vote function takes it out of this map,
77 /// adds their signature and message, then adds it back to this vote list.
78 /// If a node has voted, their `signature` and `message` field will be non-`nil`
79 /// If a node hasn't voted, their `signature` and `message` field will be `nil`
80 access(all) var generatedVotes: {String: Vote}
81
82 /// Tracks each unique vote and how much combined weight has been sent for the vote
83 access(all) var uniqueVoteMessageTotalWeights: {String: UInt64}
84
85 init(index: UInt16, nodeWeights: {String: UInt64}) {
86 self.index = index
87 self.nodeWeights = nodeWeights
88
89 var totalWeight: UInt64 = 0
90 for weight in nodeWeights.values {
91 totalWeight = totalWeight + weight
92 }
93 self.totalWeight = totalWeight
94 self.generatedVotes = {}
95 self.uniqueVoteMessageTotalWeights = {}
96 }
97
98 /// Returns the number of nodes in the cluster
99 access(all) fun size(): UInt16 {
100 return UInt16(self.nodeWeights.length)
101 }
102
103 /// Returns the minimum sum of vote weight required in order to be able to generate a
104 /// valid quorum certificate for this cluster.
105 access(all) view fun voteThreshold(): UInt64 {
106 if self.totalWeight == 0 {
107 return 0
108 }
109
110 let floorOneThird = self.totalWeight / UInt64(3) // integer division, includes floor
111
112 var res = UInt64(2) * floorOneThird
113
114 let divRemainder = self.totalWeight % UInt64(3)
115
116 if divRemainder <= UInt64(1) {
117 res = res + UInt64(1)
118 } else {
119 res = res + divRemainder
120 }
121
122 return res
123 }
124
125 /// Returns the status of this cluster's QC process
126 /// If there is a number of weight for identical votes exceeding the `voteThreshold`,
127 /// Then this cluster's QC generation is considered complete and this method returns
128 /// the vote message that reached quorum
129 /// If no vote is found to reach quorum, then `nil` is returned
130 access(all) view fun isComplete(): String? {
131 for message in self.uniqueVoteMessageTotalWeights.keys {
132 if self.uniqueVoteMessageTotalWeights[message]! >= self.voteThreshold() {
133 return message
134 }
135 }
136 return nil
137 }
138
139 /// Generates the Quorum Certificate for this cluster
140 /// If the cluster is not complete, this returns `nil`
141 access(all) fun generateQuorumCertificate(): ClusterQC? {
142
143 // Only generate the QC if the voting is complete for this cluster
144 if let quorumMessage = self.isComplete() {
145
146 // Create a new empty QC
147 var certificate: ClusterQC = ClusterQC(index: self.index, signatures: [], message: quorumMessage, voterIDs: [])
148
149 // Add the signatures, messages, and node IDs only for votes
150 // that match the votes that reached quorum
151 for vote in self.generatedVotes.values {
152
153 // Only count votes that were submitted
154 if let submittedMessage = vote.message {
155 if submittedMessage == quorumMessage {
156 certificate.addSignature(vote.signature!)
157 certificate.addVoterID(vote.nodeID)
158 }
159 }
160 }
161
162 return certificate
163 } else {
164 return nil
165 }
166 }
167
168 /// Gets a vote that was generated for a node ID
169 access(contract) view fun getGeneratedVote(nodeId: String): Vote? {
170 return self.generatedVotes[nodeId]
171 }
172
173 /// Sets the vote for the specified node ID
174 access(contract) fun setGeneratedVote(nodeId: String, vote: Vote) {
175 self.generatedVotes[nodeId] = vote
176 }
177
178 /// Gets the total weight commited for a unique vote
179 access(contract) view fun getUniqueVoteMessageTotalWeight(vote: String): UInt64? {
180 return self.uniqueVoteMessageTotalWeights[vote]
181 }
182
183 /// Sets the total weight for a unique vote
184 access(contract) fun setUniqueVoteMessageTotalWeight(vote: String, weight: UInt64) {
185 self.uniqueVoteMessageTotalWeights[vote] = weight
186 }
187 }
188
189 /// `Vote` represents a vote from one collection node.
190 /// It simply contains strings with the signed message
191 /// the hex encoded message itself. Votes are aggregated to build quorum certificates
192 access(all) struct Vote {
193
194 /// The node ID from the staking contract
195 access(all) var nodeID: String
196
197 /// The signed message from the node (using the nodes `stakingKey`)
198 access(all) var signature: String?
199
200 /// The hex-encoded message for the vote
201 access(all) var message: String?
202
203 /// The index of the cluster that this vote (and node) is in
204 access(all) let clusterIndex: UInt16
205
206 /// The weight of the vote (and node)
207 access(all) let weight: UInt64
208
209 view init(nodeID: String, clusterIndex: UInt16, voteWeight: UInt64) {
210 pre {
211 nodeID.length == 64: "Voter ID must be a valid length node ID"
212 }
213 self.signature = nil
214 self.message = nil
215 self.nodeID = nodeID
216 self.clusterIndex = clusterIndex
217 self.weight = voteWeight
218 }
219
220 access(all) fun setSignature(_ signature: String) {
221 self.signature = signature
222 }
223
224 access(all) fun setMessage(_ message: String) {
225 self.message = message
226 }
227 }
228
229 /// Represents the quorum certificate for a specific cluster
230 /// and all the nodes/votes in the cluster
231 access(all) struct ClusterQC {
232
233 /// The index of the qc in the cluster record
234 access(all) let index: UInt16
235
236 /// The vote signatures from all the nodes in the cluster
237 access(all) var voteSignatures: [String]
238
239 /// The vote message from all the valid voters in the cluster
240 access(all) var voteMessage: String
241
242 /// The node IDs that correspond to each vote
243 access(all) var voterIDs: [String]
244
245 view init(index: UInt16, signatures: [String], message: String, voterIDs: [String]) {
246 self.index = index
247 self.voteSignatures = signatures
248 self.voteMessage = message
249 self.voterIDs = voterIDs
250 }
251
252 access(all) fun addSignature(_ signature: String) {
253 self.voteSignatures.append(signature)
254 }
255
256 access(all) fun addVoterID(_ voterID: String) {
257 self.voterIDs.append(voterID)
258 }
259 }
260
261 /// Represents the aggregated signature for a cluster quorum certificate.
262 access(all) struct ClusterQCVoteData {
263 /// The aggregated signature, hex-encoded. Includes one vote for each node in `voterIDs`.
264 access(all) let aggregatedSignature: String
265
266 /// The node IDs that contributed their vote to the aggregated signature.
267 access(all) let voterIDs: [String]
268
269 init(aggregatedSignature: String, voterIDs: [String]) {
270 self.aggregatedSignature = aggregatedSignature
271 self.voterIDs = voterIDs
272 }
273 }
274
275 /// The Voter resource is generated for each collection node after they register.
276 /// Each resource instance is good for all future potential epochs, but will
277 /// only be valid if the node operator has been confirmed as a collector node for the next epoch.
278 access(all) resource Voter {
279
280 /// The nodeID of the voter (from the staking contract)
281 access(all) let nodeID: String
282
283 /// The staking key of the node (from the staking contract)
284 access(all) var stakingKey: String
285
286 init(nodeID: String, stakingKey: String) {
287 pre {
288 !FlowClusterQC.voterIsClaimed(nodeID): "Cannot create a Voter resource for a node ID that has already been claimed"
289 }
290
291 self.nodeID = nodeID
292 self.stakingKey = stakingKey
293 FlowClusterQC.voterClaimed[nodeID] = true
294 }
295
296 /// Submits the given vote. Can be called only once per epoch
297 ///
298 /// Params: voteSignature: Signed `voteMessage` with the nodes `stakingKey`
299 /// voteMessage: Hex-encoded message
300 ///
301 access(all) fun vote(voteSignature: String, voteMessage: String) {
302 pre {
303 FlowClusterQC.inProgress: "Voting phase is not in progress"
304 voteSignature.length > 0: "Vote signature must not be empty"
305 voteMessage.length > 0: "Vote message must not be empty"
306 !FlowClusterQC.nodeHasVoted(self.nodeID): "Vote must not have been cast already"
307 }
308
309 // Get the public key object from the stored key
310 let publicKey = PublicKey(
311 publicKey: self.stakingKey.decodeHex(),
312 signatureAlgorithm: SignatureAlgorithm.BLS_BLS12_381
313 )
314
315 // Check to see that the signature on the message is valid
316 let isValid = publicKey.verify(
317 signature: voteSignature.decodeHex(),
318 signedData: voteMessage.decodeHex(),
319 domainSeparationTag: "FLOW-Collector_Vote-V00-CS00-with-",
320 hashAlgorithm: HashAlgorithm.KMAC128_BLS_BLS12_381
321 )
322
323 // Assert the validity
324 assert (
325 isValid,
326 message: "Vote Signature cannot be verified"
327 )
328
329 // Get the cluster that this node belongs to
330 let clusterIndex = FlowClusterQC.nodeCluster[self.nodeID]
331 ?? panic("This node cannot vote during the current epoch")
332 let cluster = FlowClusterQC.clusters[clusterIndex]!
333
334 // Get this node's allocated vote
335 let vote = cluster.getGeneratedVote(nodeId: self.nodeID)!
336
337 // Set the signature and message fields
338 vote.setSignature(voteSignature)
339 vote.setMessage(voteMessage)
340
341 // Set the new total weight for the vote
342 let totalWeight = cluster.getUniqueVoteMessageTotalWeight(vote: voteMessage) ?? 0
343 var newWeight = totalWeight + vote.weight
344 cluster.setUniqueVoteMessageTotalWeight(vote: voteMessage, weight: newWeight)
345
346 // Save the modified vote and cluster back
347 cluster.setGeneratedVote(nodeId: self.nodeID, vote: vote)
348 FlowClusterQC.clusters[clusterIndex] = cluster
349 }
350
351 }
352
353 /// Interface that only contains operations that are part
354 /// of the regular automated functioning of the epoch process
355 /// These are accessed by the `FlowEpoch` contract through a capability
356 access(all) resource interface EpochOperations {
357 access(all) fun createVoter(nodeID: String, stakingKey: String): @Voter
358 access(all) fun startVoting(clusters: [Cluster])
359 access(all) fun stopVoting()
360 access(all) fun forceStopVoting()
361 }
362
363 /// The Admin resource provides the ability to create to Voter resource objects,
364 /// begin voting, and end voting for an epoch
365 access(all) resource Admin: EpochOperations {
366
367 /// Creates a new Voter resource for a collection node
368 /// This function will be publicly accessible in the FlowEpoch
369 /// contract, which will restrict the creation to only collector nodes
370 access(all) fun createVoter(nodeID: String, stakingKey: String): @Voter {
371 return <-create Voter(nodeID: nodeID, stakingKey: stakingKey)
372 }
373
374 /// Configures the contract for the next epoch's clusters
375 ///
376 /// NOTE: This will be called by the top-level FlowEpochs contract upon
377 /// transitioning to the Epoch Setup Phase.
378 ///
379 /// CAUTION: calling this erases the votes for the current/previous epoch.
380 access(all) fun startVoting(clusters: [Cluster]) {
381 FlowClusterQC.inProgress = true
382 FlowClusterQC.clusters = clusters
383
384 var clusterIndex: UInt16 = 0
385 for cluster in clusters {
386
387 // Create a new Vote struct for each participating node
388 for nodeID in cluster.nodeWeights.keys {
389 cluster.setGeneratedVote(nodeId: nodeID,vote: Vote(nodeID: nodeID, clusterIndex: clusterIndex, voteWeight: cluster.nodeWeights[nodeID]!))
390 FlowClusterQC.nodeCluster[nodeID] = clusterIndex
391 }
392
393 FlowClusterQC.clusters[clusterIndex] = cluster
394 clusterIndex = clusterIndex + UInt16(1)
395 }
396 }
397
398 /// Stops voting for the current epoch. Can only be called once a 2/3
399 /// majority of each cluster has submitted a vote.
400 access(all) fun stopVoting() {
401 pre {
402 FlowClusterQC.votingCompleted(): "Voting must be complete before it can be stopped"
403 }
404 FlowClusterQC.inProgress = false
405 }
406
407 /// Force a stop of the voting period
408 /// Should only be used if the protocol halts and needs to be reset
409 access(all) fun forceStopVoting() {
410 FlowClusterQC.inProgress = false
411 }
412 }
413
414 /// Returns a boolean telling if the voter is registered for the current voting phase
415 access(all) view fun voterIsRegistered(_ nodeID: String): Bool {
416 return FlowClusterQC.nodeCluster[nodeID] != nil
417 }
418
419 /// Returns a boolean telling if the node has claimed their `Voter` resource object
420 /// The object can only be claimed once, but if the node destroys their `Voter` object,
421 /// It could be claimed again
422 access(all) view fun voterIsClaimed(_ nodeID: String): Bool {
423 return FlowClusterQC.voterClaimed[nodeID] != nil
424 }
425
426 /// Returns whether this voter has successfully submitted a vote for this epoch.
427 access(all) view fun nodeHasVoted(_ nodeID: String): Bool {
428
429 // Get the cluster that this node belongs to
430 if let clusterIndex = FlowClusterQC.nodeCluster[nodeID] {
431 let cluster = FlowClusterQC.clusters[clusterIndex]
432
433 // If the node is registered for this epoch,
434 // check to see if they have voted
435 if cluster.nodeWeights[nodeID] != nil {
436 return cluster.generatedVotes[nodeID]!.signature != nil
437 }
438 }
439
440 return false
441 }
442
443 /// Gets all of the collector clusters for the current epoch
444 access(all) view fun getClusters(): [Cluster] {
445 return self.clusters
446 }
447
448 /// Returns true if we have collected enough votes for all clusters.
449 access(all) view fun votingCompleted(): Bool {
450 for cluster in FlowClusterQC.clusters {
451 if cluster.isComplete() == nil {
452 return false
453 }
454 }
455 return true
456 }
457
458 init() {
459 self.AdminStoragePath = /storage/flowEpochsQCAdmin
460 self.VoterStoragePath = /storage/flowEpochsQCVoter
461
462 self.inProgress = false
463
464 self.clusters = []
465 self.voterClaimed = {}
466 self.nodeCluster = {}
467
468 self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
469 }
470}