-
Notifications
You must be signed in to change notification settings - Fork 4
/
RandomConsumer.cdc
295 lines (256 loc) · 11.7 KB
/
RandomConsumer.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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import "Burner"
import "RandomBeaconHistory"
import "Xorshift128plus"
/// This contract is intended to make it easy to consume randomness securely from the Flow protocol's random beacon. It provides
/// a simple construct to commit to a request, and reveal the randomness in a secure manner as well as helper functions to
/// generate random numbers in a range without bias.
///
/// See an example implementation in the repository: https://github.com/onflow/random-coin-toss
///
access(all) contract RandomConsumer {
/* --- PATHS --- */
//
/// Canonical path for Consumer storage
access(all) let ConsumerStoragePath: StoragePath
/* --- EVENTS --- */
//
access(all) event RandomnessRequested(requestUUID: UInt64, block: UInt64)
access(all) event RandomnessSourced(requestUUID: UInt64, block: UInt64, randomSource: [UInt8])
access(all) event RandomnessFulfilled(requestUUID: UInt64, randomResult: UInt64)
///////////////////
// PUBLIC FUNCTIONS
///////////////////
/// Retrieves a revertible random number in the range [min, max]. By leveraging the Cadence's revertibleRandom
/// method, this function ensures that the random number is generated within range without risk of bias.
///
/// @param min: The minimum value of the range
/// @param max: The maximum value of the range
///
/// @return A random number in the range [min, max]
///
access(all) fun getRevertibleRandomInRange(min: UInt64, max: UInt64): UInt64 {
return min + revertibleRandom<UInt64>(modulo: max - min + 1)
}
/// Retrieves a random number in the range [min, max] using the provided PRG
/// to source additional randomness if needed
///
/// @param prg: The PRG to use for random number generation
/// @param min: The minimum value of the range
/// @param max: The maximum value of the range
///
/// @return A random number in the range [min, max]
///
access(all) fun getNumberInRange(prg: Xorshift128plus.PRG, min: UInt64, max: UInt64): UInt64 {
pre {
min < max:
"RandomConsumer.getNumberInRange: Cannot get random number with the provided range! "
.concat(" The min must be less than the max. Provided min of ")
.concat(min.toString()).concat(" and max of ".concat(max.toString()))
}
let range = max - min // Calculate the inclusive range of the random number
let bitsRequired = UInt256(self._mostSignificantBit(range)) // Number of bits needed to cover the range
let mask: UInt256 = (1 << bitsRequired) - 1 // Create a bitmask to extract relevant bits
let shiftLimit: UInt256 = 256 / bitsRequired // Number of shifts needed to cover 256 bits
var shifts: UInt256 = 0 // Initialize shift counter
var candidate: UInt64 = 0 // Initialize candidate
var value: UInt256 = prg.nextUInt256() // Assign the first 256 bits of randomness
while true {
candidate = UInt64(value & mask) // Apply the bitmask to extract bits
if candidate <= range {
break
}
// Shift by the number of bits covered by the mask
value = value >> bitsRequired
shifts = shifts + 1
// Get a new value if we've exhausted the current one
if shifts == shiftLimit {
value = prg.nextUInt256()
shifts = 0
}
}
// Scale candidate to the range [min, max]
return min + candidate
}
/// Returns a new Consumer resource
///
/// @return A Consumer resource
///
access(all) fun createConsumer(): @Consumer {
return <-create Consumer()
}
///////////////////
// CONSTRUCTS
///////////////////
access(all) entitlement Commit
access(all) entitlement Reveal
/// Interface to allow for a Request to be contained within another resource. The existing default implementations
/// enable an implementing resource to simply list the conformance without any additional implementation aside from
/// the inner Request resource. However, implementations should properly consider the optional when interacting
/// with the inner resource outside of the default implementations. The post-conditions ensure that implementations
/// cannot act dishonestly even if they override the default implementations.
///
access(all) resource interface RequestWrapper {
/// The Request contained within the resource
access(all) var request: @Request?
/// Returns the block height of the Request contained within the resource
///
/// @return The block height of the Request or nil if no Request is contained
///
access(all) view fun getRequestBlock(): UInt64? {
post {
result == nil || result! == self.request?.block:
"RandomConsumer.RequestWrapper.getRequestBlock(): Must return nil or the block height of RequestWrapper.request"
}
return self.request?.block ?? nil
}
/// Returns whether the Request contained within the resource can be fulfilled or not
///
/// @return Whether the Request can be fulfilled
///
access(all) view fun canFullfillRequest(): Bool {
post {
result == self.request?.canFullfill() ?? false:
"RandomConsumer.RequestWrapper.canFullfillRequest(): Must return the result of RequestWrapper.request.canFullfill()"
}
return self.request?.canFullfill() ?? false
}
/// Pops the Request from the resource and returns it
///
/// @return The Request that was contained within the resource
///
access(Reveal) fun popRequest(): @Request {
pre {
self.request != nil: "RandomConsumer.RequestWrapper.popRequest(): Request must not be nil before popRequest"
}
post {
self.request == nil:
"RandomConsumer.RequestWrapper.popRequest(): Request must be nil after popRequest"
result.uuid == before((self.request?.uuid)!):
"RandomConsumer.RequestWrapper.popRequest(): Request uuid must match result uuid"
}
let req <- self.request <- nil
return <- req!
}
}
/// A resource representing a request for randomness
///
access(all) resource Request {
/// The block height at which the request was made
access(all) let block: UInt64
/// Whether the request has been fulfilled
access(all) var fulfilled: Bool
init() {
self.block = getCurrentBlock().height
self.fulfilled = false
}
access(all) view fun canFullfill(): Bool {
return !self.fulfilled && getCurrentBlock().height >= self.block
}
/// Returns the Flow's random source for the requested block height
///
/// @return The random source for the requested block height containing at least 16 bytes (128 bits) of entropy
///
access(contract) fun _fulfill(): [UInt8] {
pre {
!self.fulfilled:
"RandomConsumer.Request.fulfill(): The random request has already been fulfilled."
self.block < getCurrentBlock().height:
"RandomConsumer.Request.fulfill(): Cannot fulfill random request before the eligible block height of "
.concat((self.block + 1).toString())
}
self.fulfilled = true
let res = RandomBeaconHistory.sourceOfRandomness(atBlockHeight: self.block).value
emit RandomnessSourced(requestUUID: self.uuid, block: self.block, randomSource: res)
return res
}
}
/// This resource enables the easy implementation of secure randomness, implementing the commit-reveal pattern and
/// using a PRG to generate random numbers from the protocol's random source.
///
access(all) resource Consumer {
/* ----- COMMIT STEP ----- */
//
/// Requests randomness, returning a Request resource
///
/// @return A Request resource
///
access(Commit) fun requestRandomness(): @Request {
let req <-create Request()
emit RandomnessRequested(requestUUID: req.uuid, block: req.block)
return <-req
}
/* ----- REVEAL STEP ----- */
//
/// Fulfills a random request, returning a random number
///
/// @param request: The Request to fulfill
///
/// @return A random number
///
access(Reveal) fun fulfillRandomRequest(_ request: @Request): UInt64 {
let reqUUID = request.uuid
// Create PRG from the provided request & generate a random number
let prg = self._getPRGFromRequest(request: <-request)
let res = prg.nextUInt64()
emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res)
return res
}
/// Fulfills a random request, returning a random number in the range [min, max] without bias. Developers may be
/// tempted to use a simple modulo operation to generate random numbers in a range, but this can introduce bias
/// when the range is not a multiple of the modulus. This function ensures that the random number is generated
/// without bias using a variation on rejection sampling.
///
/// @param request: The Request to fulfill
/// @param min: The minimum value of the range
/// @param max: The maximum value of the range
///
/// @return A random number in the range [min, max]
///
access(Reveal) fun fulfillRandomInRange(request: @Request, min: UInt64, max: UInt64): UInt64 {
pre {
min < max:
"RandomConsumer.Consumer.fulfillRandomInRange(): Cannot fulfill random number with the provided range! "
.concat(" The min must be less than the max. Provided min of ")
.concat(min.toString()).concat(" and max of ".concat(max.toString()))
}
let reqUUID = request.uuid
// Create PRG from the provided request & generate a random number & generate a random number in the range
let prg = self._getPRGFromRequest(request: <-request)
let res = RandomConsumer.getNumberInRange(prg: prg, min: min, max: max)
emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res)
return res
}
/* --- INTERNAL --- */
//
/// Creates a PRG from a Request, using the request's block height source of randomness and UUID as a salt
///
/// @param request: The Request to use for PRG creation
///
/// @return A PRG object
///
access(self) fun _getPRGFromRequest(request: @Request): Xorshift128plus.PRG {
let source = request._fulfill()
let salt = request.uuid.toBigEndianBytes()
Burner.burn(<-request)
return Xorshift128plus.PRG(sourceOfRandomness: source, salt: salt)
}
}
/// Returs the most significant bit of a UInt64
///
/// @param x: The UInt64 to find the most significant bit of
///
/// @return The most significant bit of x
///
access(self) view fun _mostSignificantBit(_ x: UInt64): UInt8 {
var bits: UInt8 = 0
var tmp: UInt64 = x
while tmp > 0 {
tmp = tmp >> 1
bits = bits + 1
}
return bits
}
init() {
self.ConsumerStoragePath = StoragePath(identifier: "RandomConsumer_".concat(self.account.address.toString()))!
}
}