Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix RandomConsumer.getNumberInRange() #32

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions contracts/RandomConsumer.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ access(all) contract RandomConsumer {
///////////////////

/// 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.
/// method, this function ensures that the random number is generated within range without risk of modulo bias.
///
/// @param min: The minimum value of the range
/// @param max: The maximum value of the range
Expand All @@ -39,21 +39,22 @@ access(all) contract RandomConsumer {
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
/// Retrieves a random number in the range [min, max] using the provided PRG reference to source additional
/// randomness if needed. This method is implemented to avoid risk of modulo bias. Passing the PRG by reference
/// ensures that its state is advanced and numbers proceed down the PRG's random walk.
///
/// @param prg: The PRG to use for random number generation
/// @param prg: The PRG (passed by reference) 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 {
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()))
"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
Expand Down Expand Up @@ -255,7 +256,8 @@ access(all) contract RandomConsumer {

// 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)
let prgRef: &Xorshift128plus.PRG = &prg
let res = RandomConsumer.getNumberInRange(prg: prgRef, min: min, max: max)

emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res)

Expand Down
1 change: 1 addition & 0 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"source": "./contracts/RandomConsumer.cdc",
"aliases": {
"emulator": "f8d6e0586b0a20c7",
"mainnet": "45caec600164c9e6",
"testing": "0000000000000007",
"testnet": "ed24dbe901028c5c"
}
Expand Down
10 changes: 9 additions & 1 deletion tests/random_consumer_tests.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ fun testRequestRandomnessSucceeds() {
Test.expect(requestCanFulfillRes, Test.beSucceeded())
let requestHeight = requestHeightRes.returnValue! as! UInt64
let requestCanFulfill = requestCanFulfillRes.returnValue! as! Bool


Test.assertEqual(expectedHeight, requestHeight)
Test.assertEqual(false, requestCanFulfill)
Expand All @@ -63,3 +62,12 @@ fun testFulfillRandomnessSucceeds() {
let fulfillRes = executeTransaction("./transactions/fulfill_random_request.cdc", [requestStoragePath], signer)
Test.expect(fulfillRes, Test.beSucceeded())
}

access(all)
fun testGetNumberInRangeUpdatesStateSucceeds() {
let diffPRGRes = executeScript("./scripts/prg_state_advances_on_range_results.cdc", [])
Test.expect(diffPRGRes, Test.beSucceeded())

let diff = diffPRGRes.returnValue! as! Bool
Test.assertEqual(true, diff)
}
30 changes: 30 additions & 0 deletions tests/scripts/prg_state_advances_on_range_results.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import "RandomBeaconHistory"
import "RandomConsumer"
import "Xorshift128plus"

/// Test script to ensure PRG state advances across calls to
access(all)
fun main(): Bool {
// Instantiate PRG
let sor = RandomBeaconHistory.sourceOfRandomness(atBlockHeight: getCurrentBlock().height - 1)
let salt = UInt64.max
let prg = Xorshift128plus.PRG(sourceOfRandomness: sor.value, salt: salt.toBigEndianBytes())
// Reference new PRG
let prgRef: &Xorshift128plus.PRG = &prg

// Call params
let min: UInt64 = 0
let max: UInt64 = 100

// Ensure PRG state changes between calls
let preState0 = prg.state0
let preState1 = prg.state1

RandomConsumer.getNumberInRange(prg: prgRef, min: min, max: max)

let postState0 = prg.state0
let postState1 = prg.state1

// Ensure state on the PRG struct changed as a result of the getNumberInRange() call
return preState0 != postState0 && preState1 != postState1
}
Loading