Smart Contract

RandomConsumer

A.a092c4aab33daeda.RandomConsumer

Valid From

138,375,743

Deployed

1w ago
Feb 15, 2026, 11:01:02 PM UTC

Dependents

0 imports
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