Smart Contract
RandomConsumer
A.a092c4aab33daeda.RandomConsumer
1import Burner from 0xf233dcee88fe0abe
2
3import RandomBeaconHistory from 0xe467b9dd11fa00df
4import Xorshift128plus from 0xa092c4aab33daeda
5
6/// This contract is intended to make it easy to consume randomness securely from the Flow protocol's random beacon. It provides
7/// a simple construct to commit to a request, and reveal the randomness in a secure manner as well as helper functions to
8/// generate random numbers in a range without bias.
9///
10/// See an example implementation in the repository: https://github.com/onflow/random-coin-toss
11///
12access(all) contract RandomConsumer {
13
14 /* --- PATHS --- */
15 //
16 /// Canonical path for Consumer storage
17 access(all) let ConsumerStoragePath: StoragePath
18
19 /* --- EVENTS --- */
20 //
21 access(all) event RandomnessRequested(requestUUID: UInt64, block: UInt64)
22 access(all) event RandomnessSourced(requestUUID: UInt64, block: UInt64, randomSource: [UInt8])
23 access(all) event RandomnessFulfilled(requestUUID: UInt64, randomResult: UInt64)
24 access(all) event RandomnessFulfilledWithPRG(requestUUID: UInt64)
25
26 ///////////////////
27 // PUBLIC FUNCTIONS
28 ///////////////////
29
30 /// Retrieves a revertible random number in the range [min, max]. By leveraging the Cadence's revertibleRandom
31 /// method, this function ensures that the random number is generated within range without risk of modulo bias.
32 ///
33 /// @param min: The minimum value of the range
34 /// @param max: The maximum value of the range
35 ///
36 /// @return A random number in the range [min, max]
37 ///
38 access(all) fun getRevertibleRandomInRange(min: UInt64, max: UInt64): UInt64 {
39 return min + revertibleRandom<UInt64>(modulo: max - min + 1)
40 }
41
42 /// Retrieves a random number in the range [min, max] using the provided PRG reference to source additional
43 /// randomness if needed. This method is implemented to avoid risk of modulo bias. Passing the PRG by reference
44 /// ensures that its state is advanced and numbers proceed down the PRG's random walk.
45 ///
46 /// @param prg: The PRG (passed by reference) to use for random number generation
47 /// @param min: The minimum value of the range
48 /// @param max: The maximum value of the range
49 ///
50 /// @return A random number in the range [min, max]
51 ///
52 access(all) fun getNumberInRange(prg: &Xorshift128plus.PRG, min: UInt64, max: UInt64): UInt64 {
53 pre {
54 min < max:
55 "RandomConsumer.getNumberInRange: Cannot get random number with the provided range! "
56 .concat(" The min must be less than the max. Provided min of ")
57 .concat(min.toString()).concat(" and max of ".concat(max.toString()))
58 }
59 let range = max - min // Calculate the inclusive range of the random number
60 let bitsRequired = UInt256(self._mostSignificantBit(range)) // Number of bits needed to cover the range
61 let mask: UInt256 = (1 << bitsRequired) - 1 // Create a bitmask to extract relevant bits
62
63 let shiftLimit: UInt256 = 256 / bitsRequired // Number of shifts needed to cover 256 bits
64 var shifts: UInt256 = 0 // Initialize shift counter
65
66 var candidate: UInt64 = 0 // Initialize candidate
67 var value: UInt256 = prg.nextUInt256() // Assign the first 256 bits of randomness
68
69 while true {
70 candidate = UInt64(value & mask) // Apply the bitmask to extract bits
71 if candidate <= range {
72 break
73 }
74
75 // Shift by the number of bits covered by the mask
76 value = value >> bitsRequired
77 shifts = shifts + 1
78
79 // Get a new value if we've exhausted the current one
80 if shifts == shiftLimit {
81 value = prg.nextUInt256()
82 shifts = 0
83 }
84 }
85
86 // Scale candidate to the range [min, max]
87 return min + candidate
88 }
89
90 /// Returns a new Consumer resource
91 ///
92 /// @return A Consumer resource
93 ///
94 access(all) fun createConsumer(): @Consumer {
95 return <-create Consumer()
96 }
97
98 /*
99 CONSUMER ADAPTERS
100
101 The following public methods are helpful for those who wish to simply request & fulfill randomness without
102 managing their own Consumer resource - e.g. request/fulfill randomness in transaction context without a contract
103 */
104
105 /// Makes a request for randomness at some block height greater than or equal to the current block height
106 ///
107 /// @param at: The block height at which the Request may be revealed. Upon fulfillment, the source of randomness
108 /// will be sourced from Flow's RandomnessBeaconHistory corresponding to the request's block height
109 ///
110 /// @return A Request resource which must be provided to fulfill the random request
111 access(all) fun requestFutureRandomness(at blockHeight: UInt64): @Request {
112 return <-self.borrowConsumer().requestFutureRandomness(at: blockHeight)
113 }
114
115 /// Fulfills a random request with a random UInt64 with randomness sourced from the corresponding Request.block
116 ///
117 /// @param request: The Request resource issued when randomness was requested
118 ///
119 /// @return A random UInt64
120 ///
121 access(all) fun fulfillRandomRequest(_ request: @Request): UInt64 {
122 return self.borrowConsumer().fulfillRandomRequest(<-request)
123 }
124
125 /// Fulfills a random request with a random UInt64 within inclusive range
126 ///
127 /// @param request: The Request resource issued when randomness was requested
128 /// @param min: The inclusive minimum of the range
129 /// @param max: The inclusive maximum of the range
130 ///
131 /// @return A random UInt64 within the requested range
132 ///
133 access(all) fun fulfillRandomRequestInRange(_ request: @Request, min: UInt64, max: UInt64): UInt64 {
134 return self.borrowConsumer().fulfillRandomInRange(request: <-request, min: min, max: max)
135 }
136
137 /// Fulfills a random request with a pseudo-random generator struct
138 ///
139 /// @param request: The Request resource issued when randomness was requested
140 ///
141 /// @return A PRG with randomness sourced corresponding to the the Request's block height and salted with the
142 /// Request's uuid
143 ///
144 access(all) fun fulfillRandomRequestWithPRG(_ request: @Request): Xorshift128plus.PRG {
145 return self.borrowConsumer().fulfillWithPRG(request: <-request)
146 }
147
148 ///////////////////
149 // CONSTRUCTS
150 ///////////////////
151
152 access(all) entitlement Commit
153 access(all) entitlement Reveal
154
155 /// Interface to allow for a Request to be contained within another resource. The existing default implementations
156 /// enable an implementing resource to simply list the conformance without any additional implementation aside from
157 /// the inner Request resource. However, implementations should properly consider the optional when interacting
158 /// with the inner resource outside of the default implementations. The post-conditions ensure that implementations
159 /// cannot act dishonestly even if they override the default implementations.
160 ///
161 access(all) resource interface RequestWrapper {
162 /// The Request contained within the resource
163 access(all) var request: @Request?
164
165 /// Returns the block height of the Request contained within the resource
166 ///
167 /// @return The block height of the Request or nil if no Request is contained
168 ///
169 access(all) view fun getRequestBlock(): UInt64? {
170 post {
171 result == nil || result! == self.request?.block:
172 "RandomConsumer.RequestWrapper.getRequestBlock(): Must return nil or the block height of RequestWrapper.request"
173 }
174 return self.request?.block ?? nil
175 }
176
177 /// Returns whether the Request contained within the resource can be fulfilled or not
178 ///
179 /// @return Whether the Request can be fulfilled
180 ///
181 access(all) view fun canFullfillRequest(): Bool {
182 post {
183 result == self.request?.canFullfill() ?? false:
184 "RandomConsumer.RequestWrapper.canFullfillRequest(): Must return the result of RequestWrapper.request.canFullfill()"
185 }
186 return self.request?.canFullfill() ?? false
187 }
188
189 /// Pops the Request from the resource and returns it
190 ///
191 /// @return The Request that was contained within the resource
192 ///
193 access(Reveal) fun popRequest(): @Request {
194 pre {
195 self.request != nil: "RandomConsumer.RequestWrapper.popRequest(): Request must not be nil before popRequest"
196 }
197 post {
198 self.request == nil:
199 "RandomConsumer.RequestWrapper.popRequest(): Request must be nil after popRequest"
200 result.uuid == before((self.request?.uuid)!):
201 "RandomConsumer.RequestWrapper.popRequest(): Request uuid must match result uuid"
202 }
203 let req <- self.request <- nil
204 return <- req!
205 }
206 }
207
208 /// A resource representing a request for randomness
209 ///
210 access(all) resource Request {
211 /// The block height at which the request was made
212 access(all) let block: UInt64
213 /// Whether the request has been fulfilled
214 access(all) var fulfilled: Bool
215
216 init(_ blockHeight: UInt64) {
217 pre {
218 getCurrentBlock().height <= blockHeight:
219 "Requested randomness for block \(blockHeight) which has passed. Can only request randomness sourced from future block heights."
220 }
221 self.block = blockHeight
222 self.fulfilled = false
223 }
224
225 /// Returns whether the request can be fulfilled as defined by whether it has already been fulfilled and the
226 /// created block height has been surpassed.
227 ///
228 /// @param: True if it can be fulfilled, false otherwise
229 ///
230 access(all) view fun canFullfill(): Bool {
231 return !self.fulfilled && getCurrentBlock().height > self.block
232 }
233
234 /// Returns the Flow's random source for the requested block height
235 ///
236 /// @return The random source for the requested block height containing at least 16 bytes (128 bits) of entropy
237 ///
238 access(contract) fun _fulfill(): [UInt8] {
239 pre {
240 !self.fulfilled:
241 "RandomConsumer.Request.fulfill(): The random request has already been fulfilled."
242 self.block < getCurrentBlock().height:
243 "RandomConsumer.Request.fulfill(): Cannot fulfill random request before the eligible block height of "
244 .concat((self.block + 1).toString())
245 }
246 self.fulfilled = true
247 let res = RandomBeaconHistory.sourceOfRandomness(atBlockHeight: self.block).value
248
249 emit RandomnessSourced(requestUUID: self.uuid, block: self.block, randomSource: res)
250
251 return res
252 }
253 }
254
255 /// This resource enables the easy implementation of secure randomness, implementing the commit-reveal pattern and
256 /// using a PRG to generate random numbers from the protocol's random source.
257 ///
258 access(all) resource Consumer {
259
260 /* ----- COMMIT STEP ----- */
261 //
262 /// Requests randomness, returning a Request resource
263 ///
264 /// @return A Request resource
265 ///
266 access(Commit) fun requestRandomness(): @Request {
267 post {
268 result.block == getCurrentBlock().height:
269 "Requested randomness for block height \(getCurrentBlock().height) but returned Request for randomness at block \(result.block)"
270 }
271 let currentHeight = getCurrentBlock().height
272 let req <-create Request(currentHeight)
273 emit RandomnessRequested(requestUUID: req.uuid, block: req.block)
274 return <-req
275 }
276
277 /// Requests randomness sourced from a future block height, returning a Request resource
278 ///
279 /// @param at: The future block height for which randomness should be sourced
280 ///
281 /// @return A Request resource
282 ///
283 access(Commit) fun requestFutureRandomness(at blockHeight: UInt64): @Request {
284 post {
285 blockHeight == result.block:
286 "Requested randomness for block height \(blockHeight) but returned Request for randomness at block \(result.block)"
287 }
288 let req <-create Request(blockHeight)
289 emit RandomnessRequested(requestUUID: req.uuid, block: req.block)
290 return <-req
291 }
292
293 /* ----- REVEAL STEP ----- */
294 //
295 /// Fulfills a random request, returning a random number
296 ///
297 /// @param request: The Request to fulfill
298 ///
299 /// @return A random number
300 ///
301 access(Reveal) fun fulfillRandomRequest(_ request: @Request): UInt64 {
302 let reqUUID = request.uuid
303
304 // Create PRG from the provided request & generate a random number
305 let prg = self._getPRGFromRequest(request: <-request)
306 let res = prg.nextUInt64()
307
308 emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res)
309 return res
310 }
311
312 /// Fulfills a random request, returning a random number in the range [min, max] without bias. Developers may be
313 /// tempted to use a simple modulo operation to generate random numbers in a range, but this can introduce bias
314 /// when the range is not a multiple of the modulus. This function ensures that the random number is generated
315 /// without bias using a variation on rejection sampling.
316 ///
317 /// @param request: The Request to fulfill
318 /// @param min: The minimum value of the range
319 /// @param max: The maximum value of the range
320 ///
321 /// @return A random number in the range [min, max]
322 ///
323 access(Reveal) fun fulfillRandomInRange(request: @Request, min: UInt64, max: UInt64): UInt64 {
324 pre {
325 min < max:
326 "RandomConsumer.Consumer.fulfillRandomInRange(): Cannot fulfill random number with the provided range! "
327 .concat(" The min must be less than the max. Provided min of ")
328 .concat(min.toString()).concat(" and max of ".concat(max.toString()))
329 }
330 let reqUUID = request.uuid
331
332 // Create PRG from the provided request & generate a random number & generate a random number in the range
333 let prg = self._getPRGFromRequest(request: <-request)
334 let prgRef: &Xorshift128plus.PRG = &prg
335 let res = RandomConsumer.getNumberInRange(prg: prgRef, min: min, max: max)
336
337 emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res)
338
339 return res
340 }
341
342 /// Creates a PRG from a Request, using the request's block height source of randomness and UUID as a salt.
343 /// This method fulfills the request, returning a PRG so that consumers can generate any number of random values
344 /// using the request's source of randomness, seeded with the request's UUID as a salt.
345 ///
346 /// NOTE: The intention in exposing this method is for consumers to be able to generate several random values
347 /// per request, and the returned PRG should be used in association to a single request. IOW, while the PRG is
348 /// a storable object, it should be treated as ephemeral, discarding once all values have been generated
349 /// corresponding to the fulfilled request.
350 ///
351 /// @param request: The Request to use for PRG creation
352 ///
353 /// @return A PRG object from which to generate random values in assocation with the fulfilled request
354 ///
355 access(Reveal) fun fulfillWithPRG(request: @Request): Xorshift128plus.PRG {
356 let reqUUID = request.uuid
357 let prg = self._getPRGFromRequest(request: <-request)
358
359 emit RandomnessFulfilledWithPRG(requestUUID: reqUUID)
360
361 return prg
362 }
363
364 /// Internal method to retrieve a PRG from a request. Doing so fulfills the request, and is intended for
365 /// internal functionality serving a single random value.
366 ///
367 /// @param request: The Request to use for PRG creation
368 ///
369 /// @return A PRG object from which this Consumer can generate a single random value to fulfill the request
370 ///
371 access(self)
372 fun _getPRGFromRequest(request: @Request): Xorshift128plus.PRG {
373 let source = request._fulfill()
374 let salt = request.uuid.toBigEndianBytes()
375 Burner.burn(<-request)
376
377 return Xorshift128plus.PRG(sourceOfRandomness: source, salt: salt)
378 }
379 }
380
381 /// Returns the most significant bit of a UInt64
382 ///
383 /// @param x: The UInt64 to find the most significant bit of
384 ///
385 /// @return The most significant bit of x
386 ///
387 access(self) view fun _mostSignificantBit(_ x: UInt64): UInt8 {
388 var bits: UInt8 = 0
389 var tmp: UInt64 = x
390 while tmp > 0 {
391 tmp = tmp >> 1
392 bits = bits + 1
393 }
394 return bits
395 }
396
397 /// Returns an authorized reference on the contract account's stored Consumer
398 ///
399 access(self)
400 fun borrowConsumer(): auth(Commit, Reveal) &Consumer {
401 let path = /storage/consumer
402 return self.account.storage.borrow<auth(Commit, Reveal) &Consumer>(from: path)
403 ?? panic("Consumer not found - ensure the Consumer has been initialized at \(path)")
404 }
405
406 init() {
407 self.ConsumerStoragePath = StoragePath(identifier: "RandomConsumer_".concat(self.account.address.toString()))!
408
409 self.account.storage.save(<-create Consumer(), to: /storage/consumer)
410 }
411}
412