Skip to content

Commit

Permalink
Merge pull request #30 from onflow/gio/expose-prg
Browse files Browse the repository at this point in the history
Expose PRG as fulfillment option supporting many random values per Request
  • Loading branch information
sisyphusSmiling authored Nov 19, 2024
2 parents 2668665 + 6928c7a commit 8f521f7
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 9 deletions.
41 changes: 32 additions & 9 deletions contracts/RandomConsumer.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ access(all) contract RandomConsumer {
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)
access(all) event RandomnessFulfilledWithPRG(requestUUID: UInt64)

///////////////////
// PUBLIC FUNCTIONS
Expand All @@ -38,7 +39,7 @@ 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
/// 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
Expand Down Expand Up @@ -80,11 +81,11 @@ access(all) contract RandomConsumer {
shifts = 0
}
}

// Scale candidate to the range [min, max]
return min + candidate
}

/// Returns a new Consumer resource
///
/// @return A Consumer resource
Expand Down Expand Up @@ -251,7 +252,7 @@ access(all) contract RandomConsumer {
.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)
Expand All @@ -261,15 +262,37 @@ access(all) contract RandomConsumer {
return res
}

/* --- INTERNAL --- */
//
/// Creates a PRG from a Request, using the request's block height source of randomness and UUID as a salt
/// Creates a PRG from a Request, using the request's block height source of randomness and UUID as a salt.
/// This method fulfills the request, returning a PRG so that consumers can generate any number of random values
/// using the request's source of randomness, seeded with the request's UUID as a salt.
///
/// NOTE: The intention in exposing this method is for consumers to be able to generate several random values
/// per request, and the returned PRG should be used in association to a single request. IOW, while the PRG is
/// a storable object, it should be treated as ephemeral, discarding once all values have been generated
/// corresponding to the fulfilled request.
///
/// @param request: The Request to use for PRG creation
///
/// @return A PRG object from which to generate random values in assocation with the fulfilled request
///
access(Reveal) fun fulfillWithPRG(request: @Request): Xorshift128plus.PRG {
let reqUUID = request.uuid
let prg = self._getPRGFromRequest(request: <-request)

emit RandomnessFulfilledWithPRG(requestUUID: reqUUID)

return prg
}

/// Internal method to retrieve a PRG from a request. Doing so fulfills the request, and is intended for
/// internal functionality serving a single random value.
///
/// @param request: The Request to use for PRG creation
///
/// @return A PRG object
/// @return A PRG object from which this Consumer can generate a single random value to fulfill the request
///
access(self) fun _getPRGFromRequest(request: @Request): Xorshift128plus.PRG {
access(self)
fun _getPRGFromRequest(request: @Request): Xorshift128plus.PRG {
let source = request._fulfill()
let salt = request.uuid.toBigEndianBytes()
Burner.burn(<-request)
Expand Down
65 changes: 65 additions & 0 deletions tests/random_consumer_tests.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Test
import BlockchainHelpers
import "test_helpers.cdc"

import "RandomConsumer"

access(all) let serviceAccount = Test.serviceAccount()
access(all) let randomConsumer = Test.getAccount(0x0000000000000007)

access(all)
fun setup() {
var err = Test.deployContract(
name: "Xorshift128plus",
path: "../contracts/Xorshift128plus.cdc",
arguments: []
)
Test.expect(err, Test.beNil())
err = Test.deployContract(
name: "RandomConsumer",
path: "../contracts/RandomConsumer.cdc",
arguments: []
)
Test.expect(err, Test.beNil())
}

access(all)
fun testRequestRandomnessSucceeds() {
let signer = Test.createAccount()

let consumerSetup = executeTransaction("./transactions/create_consumer.cdc", [], signer)
Test.expect(consumerSetup, Test.beSucceeded())

let requestStoragePath = /storage/RandomConsumerRequest
let requestRes = executeTransaction("./transactions/request_randomness.cdc", [requestStoragePath], signer)
Test.expect(requestRes, Test.beSucceeded())

let expectedHeight = getCurrentBlockHeight()

let requestHeightRes = executeScript("./scripts/get_request_blockheight.cdc", [signer.address, requestStoragePath])
let requestCanFulfillRes = executeScript("./scripts/request_can_fulfill.cdc", [signer.address, requestStoragePath])
Test.expect(requestHeightRes, Test.beSucceeded())
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)

}

access(all)
fun testFulfillRandomnessSucceeds() {
let signer = Test.createAccount()

let consumerSetup = executeTransaction("./transactions/create_consumer.cdc", [], signer)
Test.expect(consumerSetup, Test.beSucceeded())

let requestStoragePath = /storage/RandomConsumerRequest
let requestRes = executeTransaction("./transactions/request_randomness.cdc", [requestStoragePath], signer)
Test.expect(requestRes, Test.beSucceeded())

let fulfillRes = executeTransaction("./transactions/fulfill_random_request.cdc", [requestStoragePath], signer)
Test.expect(fulfillRes, Test.beSucceeded())
}
10 changes: 10 additions & 0 deletions tests/scripts/get_request_blockheight.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "RandomConsumer"

access(all)
fun main(address: Address, storagePath: StoragePath): UInt64 {
return getAuthAccount<auth(BorrowValue) &Account>(address).storage
.borrow<&RandomConsumer.Request>(
from: storagePath
)?.block
?? panic("No Request found")
}
10 changes: 10 additions & 0 deletions tests/scripts/request_can_fulfill.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "RandomConsumer"

access(all)
fun main(address: Address, storagePath: StoragePath): Bool {
return getAuthAccount<auth(BorrowValue) &Account>(address).storage
.borrow<&RandomConsumer.Request>(
from: storagePath
)?.canFullfill()
?? panic("No Request found")
}
10 changes: 10 additions & 0 deletions tests/transactions/create_consumer.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "RandomConsumer"

transaction {
prepare (signer: auth(BorrowValue, SaveValue) &Account) {
if signer.storage.type(at: RandomConsumer.ConsumerStoragePath) != nil {
panic("Consumer already stored")
}
signer.storage.save(<-RandomConsumer.createConsumer(), to: RandomConsumer.ConsumerStoragePath)
}
}
18 changes: 18 additions & 0 deletions tests/transactions/fulfill_random_request.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import "RandomConsumer"

transaction(storagePath: StoragePath) {
let consumer: auth(RandomConsumer.Reveal) &RandomConsumer.Consumer
let request: @RandomConsumer.Request

prepare (signer: auth(BorrowValue, LoadValue) &Account) {
self.consumer = signer.storage.borrow<auth(RandomConsumer.Reveal) &RandomConsumer.Consumer>(
from: RandomConsumer.ConsumerStoragePath
) ?? panic("Consumer not found in storage")
self.request <- signer.storage.load<@RandomConsumer.Request>(from: storagePath)
?? panic("No Request found at provided storage path")
}

execute {
let rand = self.consumer.fulfillRandomRequest(<-self.request)
}
}
13 changes: 13 additions & 0 deletions tests/transactions/request_randomness.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import "RandomConsumer"

transaction(storagePath: StoragePath) {
prepare (signer: auth(BorrowValue, SaveValue) &Account) {
if signer.storage.type(at: storagePath) != nil {
panic("Object already stored in provided storage path")
}
let consumer = signer.storage.borrow<auth(RandomConsumer.Commit) &RandomConsumer.Consumer>(
from: RandomConsumer.ConsumerStoragePath
) ?? panic("Consumer not found in storage")
signer.storage.save(<-consumer.requestRandomness(), to: storagePath)
}
}

0 comments on commit 8f521f7

Please sign in to comment.