Smart Contract
NodeVersionBeacon
A.e467b9dd11fa00df.NodeVersionBeacon
1/// The NodeVersionBeacon contract holds the past and future protocol versions.
2/// that should be used to execute/handle blocks at aa given block height.
3///
4/// The service account holds the NodeVersionBeacon.Heartbeat resource
5/// which is responsible for emitting the VersionBeacon event.
6/// The event contains the current version and all the upcoming versions.
7/// The event is emitted every time the version table is updated
8/// or a version boundary is reached.
9///
10/// The NodeVersionBeacon.Admin resource is used to add new version boundaries
11/// or change existing future version boundaries. Future version boundaries can only be
12/// changed if they occur after the current block height + versionUpdateFreezePeriod.
13/// This is to ensure that nodes have enough time to react to version table changes.
14/// The versionUpdateFreezePeriod can also be changed by the admin resource, but only if
15/// there are no upcoming version boundaries within the current versionUpdateFreezePeriod or
16/// the new versionUpdateFreezePeriod.
17///
18/// The contract itself can be used to query the current version and the next upcoming version.
19access(all) contract NodeVersionBeacon {
20
21 /// =========================
22 /// Execution State Versioning
23 /// =========================
24
25 /// Struct representing software version as Semantic Version
26 /// along with helper functions
27 /// For reference, see https://semver.org/
28 access(all) struct Semver {
29 /// Components defining a semantic version
30 access(all) let major: UInt8
31 access(all) let minor: UInt8
32 access(all) let patch: UInt8
33 access(all) let preRelease: String?
34
35 init(major: UInt8, minor: UInt8, patch: UInt8, preRelease: String?) {
36 self.major = major
37 self.minor = minor
38 self.patch = patch
39 self.preRelease = preRelease
40 }
41
42 /// Returns version in Semver format (e.g. v<major>.<minor>.<patch>-<preRelease>)
43 /// as a String
44 access(all) view fun toString(): String {
45 let semverCoreString = self.major.toString()
46 .concat(".")
47 .concat(
48 self.minor.toString()
49 ).concat(".")
50 .concat(
51 self.patch.toString()
52 )
53 // Concat pre-release if it exists & return
54 if self.preRelease != nil {
55 return semverCoreString.concat("-").concat(self.preRelease!)
56 }
57
58 return semverCoreString
59 }
60
61 /* Custom Comparators */
62
63 /// Returns true if Semver core is greater than
64 /// passed Semver core and false otherwise
65 access(all) view fun coreGreaterThan(_ other: Semver): Bool {
66 if (self.major != other.major) {
67 return self.major > other.major
68 }
69
70 if (self.minor != other.minor) {
71 return self.minor > other.minor
72 }
73
74 if (self.patch != other.patch) {
75 return self.patch > other.patch
76 }
77
78 return false
79 }
80
81 /// Returns true if Semver core is greater than or
82 /// equal to passed Semver core and false otherwise
83 access(all) view fun coreGreaterThanOrEqualTo(_ other: Semver): Bool {
84 return self.coreGreaterThan(other) || self.coreEqualTo(other)
85 }
86
87 /// Returns true if Semver core is less than
88 /// passed Semver core and false otherwise
89 access(all) view fun coreLessThan(_ other: Semver): Bool {
90 return !self.coreGreaterThanOrEqualTo(other)
91 }
92
93 /// Returns true if Semver core is less than or
94 /// equal to passed Semver core and false otherwise
95 access(all) view fun coreLessThanOrEqualTo(_ other: Semver): Bool {
96 return !self.coreGreaterThan(other)
97 }
98
99 /// Returns true if Semver is equal to passed
100 /// Semver core and false otherwise
101 access(all) view fun coreEqualTo(_ other: Semver): Bool {
102 return self.major == other.major && self.minor == other.minor && self.patch == other.patch
103 }
104
105 /// Returns true if Semver is *exactly* equal to passed
106 /// Semver and false otherwise
107 access(all) view fun strictEqualTo(_ other: Semver): Bool {
108 return self.coreEqualTo(other) && self.preRelease == other.preRelease
109 }
110 }
111
112 /// Returns the v0.0.0 version.
113 access(all) fun zeroSemver(): Semver {
114 return Semver(major: 0, minor: 0, patch: 0, preRelease: nil)
115 }
116
117 /// Struct for emitting the current and incoming versions along with their block
118 access(all) struct VersionBoundary {
119 access(all) let blockHeight: UInt64
120 access(all) let version: Semver
121
122 init(blockHeight: UInt64, version: Semver){
123 self.blockHeight = blockHeight
124 self.version = version
125 }
126 }
127
128 /// Returns the zero boundary. Used as a sentinel value
129 /// for versions before the version beacon contract.
130 /// Simplifies edge case code.
131 /// The zero boundary is at block height 0 and has version v0.0.0.
132 /// It is always the first element in the versionBoundaryBlockList.
133 access(all) fun zeroVersionBoundary(): VersionBoundary {
134 let zeroVersion = self.zeroSemver()
135 return VersionBoundary(
136 blockHeight: 0,
137 version: zeroVersion
138 )
139 }
140
141 /// A service event emitted when the version table is updated.
142 /// The version is the software version which must be used for executing a height range of blocks.
143 /// The version pertains to Execution and Verification Nodes.
144 /// The table contains the current version and all the upcoming versions sorted by block height.
145 /// The sequence increases by one each time an event is emitted.
146 /// It can be used to verify no events were missed.
147 access(all) event VersionBeacon(
148 versionBoundaries: [VersionBoundary],
149 sequence: UInt64
150 )
151
152 /// Event emitted any time the version boundary freeze period is updated.
153 /// freeze period is measured in blocks (from the current block).
154 access(all) event NodeVersionBoundaryFreezePeriodChanged(freezePeriod: UInt64)
155
156 /// Canonical storage path for the NodeVersionBeacon.Admin resource.
157 access(all) let AdminStoragePath: StoragePath
158
159 /// Canonical storage path for the NodeVersionBeacon.Heartbeat resource.
160 access(all) let HeartbeatStoragePath: StoragePath
161
162 /// Block height indexed version boundaries.
163 access(contract) let versionBoundary: {UInt64: VersionBoundary}
164
165 /// Sorted Array containing version boundary block heights.
166 access(contract) var versionBoundaryBlockList: [UInt64]
167
168 /// Index in the versionBoundaryBlockList of the next upcoming version boundary,
169 /// or nil if no upcoming version boundary.
170 access(contract) var firstUpcomingBoundary: UInt64?
171
172 /// versionUpdateFreezePeriod is the number of blocks (past the current one) for which version boundary
173 /// changes are not allowed. This is to ensure that nodes have enough time to react to
174 /// version table changes.
175 access(contract) var versionBoundaryFreezePeriod: UInt64
176
177 /// Boolean flag for keeping track if a VersionBeacon event needs to be emitted on next heartbeat.
178 access(contract) var emitEventOnNextHeartbeat: Bool
179
180 /// A counter that increases every time the Version beacon event is emitted.
181 access(contract) var nextVersionBeaconEventSequence: UInt64
182
183 /// Admin resource that manages version boundaries
184 /// maintained in this contract.
185 access(all) resource Admin {
186 /// Adds or updates a version boundary.
187 access(all) fun setVersionBoundary(versionBoundary: VersionBoundary) {
188 pre {
189 versionBoundary.blockHeight > getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod
190 : "Cannot set/update a version boundary for past blocks or blocks in the near future."
191 }
192 // Set the flag to true so the event will be emitted next time emitChanges is called
193 NodeVersionBeacon.emitEventOnNextHeartbeat = true
194
195 let exists = NodeVersionBeacon.versionBoundary[versionBoundary.blockHeight] != nil
196 NodeVersionBeacon.versionBoundary[versionBoundary.blockHeight] = versionBoundary
197
198 if exists {
199 // this was an update so nothing else needs to be done
200 return
201 }
202
203 // We have to insert the block height into the ordered list.
204 // This is an inefficient algorithm, but it is not expected that the list of
205 // upcoming versions will be long.
206 var i = NodeVersionBeacon.versionBoundaryBlockList.length
207 while i > 1 && NodeVersionBeacon.versionBoundaryBlockList[i-1] > versionBoundary.blockHeight {
208 i = i - 1
209 }
210 NodeVersionBeacon.versionBoundaryBlockList.insert(at: i, versionBoundary.blockHeight)
211
212 // no need to change the firstUpcomingBoundary unless it was nil
213 // case 1: index points to a lower block height then the one inserted
214 // => it should remain pointing at that index
215 // case 2: index points to the entry that was replaced by this insert
216 // => it should remain pointing at this new entry, since it is before the old one
217 // case 3: index was pointing to an entry later than the insert FixedPoint
218 // => this is illegal and cannot happen since there are entries with lower block heights.
219 if NodeVersionBeacon.firstUpcomingBoundary == nil {
220 NodeVersionBeacon.firstUpcomingBoundary = UInt64(NodeVersionBeacon.versionBoundaryBlockList.length - 1)
221 }
222 }
223
224 /// Deletes an upcoming version boundary.
225 access(all) fun deleteVersionBoundary(blockHeight: UInt64) {
226 pre {
227 blockHeight > getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod
228 : "Cannot delete a version for past blocks or blocks in the near future."
229 NodeVersionBeacon.versionBoundary.containsKey(blockHeight): "No boundary defined at that blockHeight."
230 }
231 // Set the flag to true so the event will be emitted next time emitChanges is called
232 NodeVersionBeacon.emitEventOnNextHeartbeat = true
233
234 // Remove the version mapping and upcomingBlockBoundaries
235 NodeVersionBeacon.versionBoundary.remove(key: blockHeight)
236
237 // We have to remove the block height from the ordered list.
238 // This is an inefficient algorithm, but it is not expected that the list of
239 // upcoming versions will be long.
240 var i = NodeVersionBeacon.versionBoundaryBlockList.length - 1
241 while i > 0 && NodeVersionBeacon.versionBoundaryBlockList[i] > blockHeight {
242 i = i - 1
243 }
244 assert(NodeVersionBeacon.versionBoundaryBlockList[i] == blockHeight,
245 message: "version boundary exists in map, so it should also exist in the ordered list")
246
247 NodeVersionBeacon.versionBoundaryBlockList.remove(at: i)
248
249 // the index has to be fixed, but you cannot change records before the index
250 // so the only case to be addressed is that the index is pointing off the list,
251 // because the list is now shorter.
252 if NodeVersionBeacon.firstUpcomingBoundary != nil &&
253 NodeVersionBeacon.firstUpcomingBoundary! >= UInt64(NodeVersionBeacon.versionBoundaryBlockList.length) {
254 NodeVersionBeacon.firstUpcomingBoundary = nil
255 }
256
257 }
258
259 /// Updates the number of blocks in which version boundaries are frozen.
260 access(all) fun setVersionBoundaryFreezePeriod(newFreezePeriod: UInt64) {
261 post {
262 NodeVersionBeacon.versionBoundaryFreezePeriod == newFreezePeriod: "Update buffer was not properly set!"
263 }
264
265 // Get current block height.
266 let currentBlockHeight = getCurrentBlock().height
267
268 // No boundaries defined beyond current block, safe to make changes
269 if NodeVersionBeacon.firstUpcomingBoundary == nil {
270 NodeVersionBeacon.versionBoundaryFreezePeriod = newFreezePeriod
271 return
272 }
273
274 let nextBlockBoundary = NodeVersionBeacon.versionBoundaryBlockList[NodeVersionBeacon.firstUpcomingBoundary!]
275
276 // Ensure that the we're not currently within the old or new freeze period
277 // of the next block height boundary
278 assert(
279 currentBlockHeight + NodeVersionBeacon.versionBoundaryFreezePeriod < nextBlockBoundary &&
280 currentBlockHeight + newFreezePeriod < nextBlockBoundary,
281 message: "Updating buffer now breaks version boundary update expectations. Try updating buffer after next version boundary."
282 )
283
284 NodeVersionBeacon.versionBoundaryFreezePeriod = newFreezePeriod
285
286 emit NodeVersionBoundaryFreezePeriodChanged(freezePeriod: newFreezePeriod)
287 }
288
289 /// Emits the given protocol state version upgrade event.
290 /// If the version and active view are valid, this will cause the Protocol State
291 /// to upgrade its model version when the event is incorporated.
292 /// If either the version or active view are invalid, the service event will be
293 /// ignored and will have no effect. All validation is performed when the service
294 /// event is incorporated by the Protocol State.
295 /// It is safe to emit the same ProtocolStateVersionUpgrade event multiple times,
296 /// as only version upgrade will occur.
297 access(all) fun emitProtocolStateVersionUpgrade(newProtocolVersion: UInt64, activeView: UInt64) {
298 emit ProtocolStateVersionUpgrade(newProtocolVersion: newProtocolVersion, activeView: activeView)
299 }
300 }
301
302 /// Heartbeat resource that emits the version beacon event and keeps track of upcoming versions.
303 /// This resource should always be held only by the service account,
304 /// because the service account should be the only one emitting the event,
305 /// and only during the system transaction
306 access(all) resource Heartbeat {
307 // heartbeat is called during the system transaction every block.
308 access(all) fun heartbeat() {
309 self.checkFirstUpcomingBoundary()
310
311 if (!NodeVersionBeacon.emitEventOnNextHeartbeat) {
312 return
313 }
314 NodeVersionBeacon.emitEventOnNextHeartbeat = false
315
316 self.emitVersionBeaconEvent(versionBoundaries: NodeVersionBeacon.getCurrentVersionBoundaries())
317 }
318
319 access(self) fun emitVersionBeaconEvent(versionBoundaries : [VersionBoundary]) {
320
321 emit VersionBeacon(versionBoundaries: versionBoundaries,
322 sequence: NodeVersionBeacon.nextVersionBeaconEventSequence)
323 // After emitting the event increase the event sequence number and set the flag to false
324 // so the event won't be emitted on the next block if there isn't any changes to the table
325 NodeVersionBeacon.nextVersionBeaconEventSequence = NodeVersionBeacon.nextVersionBeaconEventSequence + 1
326
327 }
328
329 /// Check if the index pointing to the next version boundary needs to be moved.
330 access(self) fun checkFirstUpcomingBoundary() {
331 if NodeVersionBeacon.firstUpcomingBoundary == nil {
332 return
333 }
334
335 let currentBlockHeight = getCurrentBlock().height
336 var boundaryIndex = NodeVersionBeacon.firstUpcomingBoundary!
337 while boundaryIndex < UInt64(NodeVersionBeacon.versionBoundaryBlockList.length)
338 && NodeVersionBeacon.versionBoundaryBlockList[boundaryIndex] <= currentBlockHeight {
339 boundaryIndex = boundaryIndex + 1
340 }
341
342 if boundaryIndex == NodeVersionBeacon.firstUpcomingBoundary! {
343 // no change
344 return
345 }
346
347 if boundaryIndex >= UInt64(NodeVersionBeacon.versionBoundaryBlockList.length) {
348 NodeVersionBeacon.firstUpcomingBoundary = nil
349 } else {
350 NodeVersionBeacon.firstUpcomingBoundary = boundaryIndex
351 }
352
353 // If we passed a boundary re-emit the VersionBeacon event
354 NodeVersionBeacon.emitEventOnNextHeartbeat = true
355 }
356 }
357
358 /// getCurrentVersionBoundaries returns the current version boundaries.
359 /// this is the same list as the one emitted by the VersionBeacon event.
360 access(all) fun getCurrentVersionBoundaries(): [VersionBoundary] {
361 let tableUpdates: [VersionBoundary] = []
362
363 if NodeVersionBeacon.firstUpcomingBoundary == nil {
364 // no future boundaries. Just return the last one.
365 // this is safe, there is at least one record in the versionBoundaryBlockList
366 tableUpdates.append(NodeVersionBeacon.versionBoundary[
367 NodeVersionBeacon.versionBoundaryBlockList[NodeVersionBeacon.versionBoundaryBlockList.length - 1]
368 ]!)
369 return tableUpdates
370 }
371
372 // -1 to include the version the node should currently be on
373 var start = (NodeVersionBeacon.firstUpcomingBoundary ?? UInt64(NodeVersionBeacon.versionBoundaryBlockList.length)) - 1
374 let end = UInt64(NodeVersionBeacon.versionBoundaryBlockList.length)
375
376 if start < 0 {
377 // this is the case when the current index is at 0
378 start = 0
379 }
380
381 var i = start
382
383 while i < end {
384 let block = NodeVersionBeacon.versionBoundaryBlockList[i]
385 tableUpdates.append(NodeVersionBeacon.versionBoundary[block]!)
386 i = i + 1
387 }
388
389 return tableUpdates
390 }
391
392 /// Returns the versionBoundaryFreezePeriod
393 access(all) view fun getVersionBoundaryFreezePeriod(): UInt64 {
394 return NodeVersionBeacon.versionBoundaryFreezePeriod
395 }
396
397 /// Returns the sequence number of the next version beacon event
398 /// This can be used to verify that no version beacon events were missed.
399 access(all) view fun getNextVersionBeaconSequence(): UInt64 {
400 return self.nextVersionBeaconEventSequence
401 }
402
403 /// Function that returns the version that was defined at the most
404 /// recent block height boundary. May return zero boundary.
405 access(all) fun getCurrentVersionBoundary(): VersionBoundary {
406 var current: UInt64 = 0
407
408 // index is never 0 since version 0 is always in the past
409 if let index = NodeVersionBeacon.firstUpcomingBoundary {
410 assert(index > 0, message: "index should never be 0 since version 0 is always in the past")
411 current = self.versionBoundaryBlockList[index-1]
412 } else {
413 current = UInt64(NodeVersionBeacon.versionBoundaryBlockList.length - 1)
414 }
415
416 let block = self.versionBoundaryBlockList[current]
417
418 // Return the version mapped to the last historical block height boundary
419 return self.versionBoundary[block]!
420 }
421
422 access(all) fun getNextVersionBoundary() : VersionBoundary? {
423 if let index = NodeVersionBeacon.firstUpcomingBoundary {
424 let block = self.versionBoundaryBlockList[index]
425 return self.versionBoundary[block]
426 } else {
427 return nil
428 }
429 }
430
431 /// Checks whether given version was compatible at the given historical block height
432 access(all) view fun getVersionBoundary(effectiveAtBlockHeight: UInt64): VersionBoundary {
433 let block = self.searchForClosestHistoricalBlockBoundary(blockHeight: effectiveAtBlockHeight)
434
435 return self.versionBoundary[block]!
436 }
437
438 access(all) struct VersionBoundaryPage {
439 access(all) let page: Int
440 access(all) let perPage: Int
441 access(all) let totalLength: Int
442 access(all) let values : [VersionBoundary]
443
444 view init(page: Int, perPage: Int, totalLength: Int, values: [VersionBoundary]) {
445 self.page = page
446 self.perPage = perPage
447 self.totalLength = totalLength
448 self.values = values
449 }
450
451 }
452
453 /// Returns a page of version boundaries
454 /// page is zero based
455 /// results are sorted by block height
456 access(all) fun getVersionBoundariesPage(page: Int, perPage: Int) : VersionBoundaryPage {
457 pre {
458 page >= 0: "page must be greater than or equal to 0"
459 perPage > 0: "perPage must be greater than 0"
460 }
461
462 let totalLength = NodeVersionBeacon.versionBoundaryBlockList.length
463 var startIndex = page * perPage
464 if startIndex > totalLength {
465 startIndex = totalLength
466 }
467 var endIndex = startIndex + perPage
468 if endIndex > totalLength {
469 endIndex = totalLength
470 }
471 let values: [VersionBoundary] = []
472 if startIndex == endIndex {
473 return VersionBoundaryPage(page: page, perPage: perPage, totalLength: totalLength, values: values)
474 }
475 for block in self.versionBoundaryBlockList.slice(from: startIndex, upTo: endIndex) {
476 values.append(NodeVersionBeacon.versionBoundary[block]!)
477 }
478 return VersionBoundaryPage(page: page, perPage: perPage, totalLength: totalLength, values: values)
479 }
480
481
482 /// Binary search algorithm to find closest value key in versionTable that is <= target value
483 access(contract) view fun searchForClosestHistoricalBlockBoundary(blockHeight: UInt64): UInt64 {
484 // Return last block boundary if target is beyond
485 let length = self.versionBoundaryBlockList.length
486 if blockHeight >= self.versionBoundaryBlockList[length - 1] {
487 return self.versionBoundaryBlockList[length - 1]
488 }
489
490 // Define search bounds
491 var left = 0
492 var right = length
493 // Loop until search pointers cross
494 while left < right {
495 var mid = (left + right) / 2
496 if self.versionBoundaryBlockList[mid] == blockHeight {
497 return self.versionBoundaryBlockList[mid]
498 }
499 if blockHeight < self.versionBoundaryBlockList[mid] {
500 if mid > 0 && blockHeight > self.versionBoundaryBlockList[mid -1] {
501 return self.versionBoundaryBlockList[mid - 1]
502 }
503 right = mid
504 } else {
505 if mid < (length - 1) && blockHeight < self.versionBoundaryBlockList[mid + 1] {
506 return self.versionBoundaryBlockList[mid]
507 }
508 left = mid + 1
509 }
510 }
511 // Return zero version if nothing found
512 return self.versionBoundaryBlockList[0]
513 }
514
515 /// =========================
516 /// Protocol State Versioning
517 /// =========================
518
519 /// A service event which is emitted to indicate that the Protocol State version is being upgraded.
520 /// This acts as a signal to begin using the upgraded Protocol State version
521 /// after this service event is sealed, and after view `activeView` is entered.
522 /// Nodes running a software version which does not support `newProtocolVersion`
523 /// will stop processing new blocks when they reach view `activeAtView`.
524 access(all) event ProtocolStateVersionUpgrade(newProtocolVersion: UInt64, activeView: UInt64)
525
526 init(versionUpdateFreezePeriod: UInt64) {
527 self.AdminStoragePath = /storage/NodeVersionBeaconAdmin
528 self.HeartbeatStoragePath = /storage/NodeVersionBeaconHeartbeat
529
530 // insert a zero-th version to make the API simpler and more robust
531 let zero = NodeVersionBeacon.zeroVersionBoundary()
532
533 self.versionBoundary = {zero.blockHeight:zero}
534 self.versionBoundaryBlockList = [zero.blockHeight]
535 self.versionBoundaryFreezePeriod = versionUpdateFreezePeriod
536 self.firstUpcomingBoundary = nil
537 self.nextVersionBeaconEventSequence = 0
538
539 // emit the event on the first heartbeat to send the zero version
540 self.emitEventOnNextHeartbeat = true
541
542 self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
543 self.account.storage.save(<-create Heartbeat(), to: self.HeartbeatStoragePath)
544 }
545}
546