forked from onflow/flow-core-contracts
/
RandomBeaconHistory.cdc
191 lines (168 loc) · 7.88 KB
/
RandomBeaconHistory.cdc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/// RandomBeaconHistory (FLIP 123)
///
/// This contract stores the history of random sources generated by the Flow network. The defined Heartbeat resource is
/// updated by the Flow Service Account at the end of every block with that block's source of randomness.
///
/// While the source values are safely generated by the Random Beacon (non-predictable, unbiasable, verifiable) and transmitted into the execution
/// environment via the committing transaction, using the raw values from this contract does not guarantee non-revertible
/// randomness. The Hearbeat is intended to be used in conjunction with a
/// commit-reveal mechanism to provide an onchain source of non-revertible randomness.
// It is also recommended to use the source values with a pseudo-random number
// generator (PRNG) to generate an arbitrary-long sequence of random values.
//
// For usage of randomness where result abortion is not an issue, it is recommended
// to use the Cadence built-in function `revertibleRandom`, which is also based on
// the safe Random Beacon.
///
/// Read the full FLIP here: https://github.com/onflow/flips/pull/123
///
access(all) contract RandomBeaconHistory {
/// The height at which the first source of randomness was recorded
access(contract) var lowestHeight: UInt64?
/// Sequence of random sources recorded by the Heartbeat, stored as an array over a mapping to reduce storage
access(contract) let randomSourceHistory: [[UInt8]]
/// The path of the Heartbeat resource in the deployment account
access(all) let HeartbeatStoragePath: StoragePath
// Event emitted when missing SoR is backfilled on later heartbeat
access(all) event RandomHistoryBackfilled(blockHeight: UInt64, count: UInt64)
/* --- Hearbeat --- */
//
/// The Heartbeat resource containing each block's source of randomness in sequence
///
access(all) resource Heartbeat {
/// Callable by owner of the Heartbeat resource, Flow Service Account, records the provided random source
///
/// @param randomSourceHistory The random source to record
///
access(all) fun heartbeat(randomSourceHistory: [UInt8]) {
let currentBlockHeight = getCurrentBlock().height
if RandomBeaconHistory.lowestHeight == nil {
RandomBeaconHistory.lowestHeight = currentBlockHeight
}
RandomBeaconHistory.randomSourceHistory.append(randomSourceHistory)
//backfill if necessary
var blockCount:UInt64 = currentBlockHeight - RandomBeaconHistory.lowestHeight! + 1
var randomCount:UInt64 = UInt64(RandomBeaconHistory.randomSourceHistory.length)
var backfilled: UInt64 = 0
var randomSource = randomSourceHistory
while randomCount + backfilled < blockCount && backfilled < 100 {
// Hash Random Source with SHA3 to generate next Random Source for backfill
var randomSource = HashAlgorithm.SHA3_256.hash(randomSource)
// append new random source
RandomBeaconHistory.randomSourceHistory.append(randomSource)
backfilled = backfilled + 1
}
//emit event if some history backfilled
if backfilled>0{
emit RandomHistoryBackfilled(blockHeight: currentBlockHeight, count: backfilled)
}
}
}
/* --- RandomSourceHistory --- */
//
/// Represents a random source value for a given block height
///
access(all) struct RandomSource {
access(all) let blockHeight: UInt64
access(all) let value: [UInt8]
init(blockHeight: UInt64, value: [UInt8]) {
self.blockHeight = blockHeight
self.value = value
}
}
/* --- RandomSourceHistoryPage --- */
//
/// Contains RandomSource values ordered chronologically according to associated block height
///
access(all) struct RandomSourceHistoryPage {
access(all) let page: UInt64
access(all) let perPage: UInt64
access(all) let totalLength: UInt64
access(all) let values: [RandomSource]
init(page: UInt64, perPage: UInt64, totalLength: UInt64, values: [RandomSource]) {
self.page = page
self.perPage = perPage
self.totalLength = totalLength
self.values = values
}
}
/* --- Contract Methods --- */
//
/// Getter for the source of randomness at a given block height. Panics if the requested block height either
/// precedes or exceeds the recorded history. Note that a source of randomness for block n will not be accessible
/// until block n+1.
///
/// @param atBlockHeight The block height at which to retrieve the source of randomness
///
/// @return The source of randomness at the given block height as RandomSource struct
///
access(all) fun sourceOfRandomness(atBlockHeight blockHeight: UInt64): RandomSource {
pre {
self.lowestHeight != nil: "History has not yet been initialized"
blockHeight >= self.lowestHeight!: "Requested block height precedes recorded history"
blockHeight < getCurrentBlock().height: "Source of randomness not yet recorded"
}
let index = blockHeight - self.lowestHeight!
assert(
index >= 0 && index < UInt64(self.randomSourceHistory.length),
message: "Problem finding random source history index"
)
return RandomSource(blockHeight: blockHeight, value: self.randomSourceHistory[index])
}
/// Retrieves a page from the history of random sources, ordered chronologically
///
/// @param page: The page number to retrieve, 0-indexed
/// @param perPage: The number of random sources to include per page
///
/// @return A RandomSourceHistoryPage containing RandomSource values in choronological order according to
/// associated block height
///
access(all) view fun getRandomSourceHistoryPage(_ page: UInt64, perPage: UInt64): RandomSourceHistoryPage {
pre {
self.lowestHeight != nil: "History has not yet been initialized"
}
let values: [RandomSource] = []
let totalLength = UInt64(self.randomSourceHistory.length)
var startIndex = page * perPage
if startIndex > totalLength {
startIndex = totalLength
}
var endIndex = startIndex + perPage
if endIndex > totalLength {
endIndex = totalLength
}
// Return empty page if request exceeds last page
if startIndex == endIndex {
return RandomSourceHistoryPage(page: page, perPage: perPage, totalLength: totalLength, values: values)
}
// Iterate over history and construct page RandomSource values
let lowestHeight = self.lowestHeight!
for i, block in self.randomSourceHistory.slice(from: Int(startIndex), upTo: Int(endIndex)) {
values.append(
RandomSource(
blockHeight: lowestHeight + startIndex + UInt64(i),
value: self.randomSourceHistory[startIndex + UInt64(i)]
)
)
}
return RandomSourceHistoryPage(
page: page,
perPage: perPage,
totalLength: totalLength,
values: values
)
}
/// Getter for the block height at which the first source of randomness was recorded
///
/// @return The block height at which the first source of randomness was recorded
///
access(all) view fun getLowestHeight(): UInt64 {
return self.lowestHeight ?? panic("History has not yet been initialized")
}
init() {
self.lowestHeight = nil
self.randomSourceHistory = []
self.HeartbeatStoragePath = /storage/FlowRandomBeaconHistoryHeartbeat
self.account.save(<-create Heartbeat(), to: self.HeartbeatStoragePath)
}
}