diff --git a/contracts/RandomConsumer.cdc b/contracts/RandomConsumer.cdc index e66b659..e350d12 100644 --- a/contracts/RandomConsumer.cdc +++ b/contracts/RandomConsumer.cdc @@ -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 @@ -38,7 +39,7 @@ access(all) contract RandomConsumer { return min + revertibleRandom(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 @@ -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 @@ -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) @@ -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) diff --git a/tests/random_consumer_tests.cdc b/tests/random_consumer_tests.cdc new file mode 100644 index 0000000..a64ee3d --- /dev/null +++ b/tests/random_consumer_tests.cdc @@ -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()) +} diff --git a/tests/scripts/get_request_blockheight.cdc b/tests/scripts/get_request_blockheight.cdc new file mode 100644 index 0000000..fef227c --- /dev/null +++ b/tests/scripts/get_request_blockheight.cdc @@ -0,0 +1,10 @@ +import "RandomConsumer" + +access(all) +fun main(address: Address, storagePath: StoragePath): UInt64 { + return getAuthAccount(address).storage + .borrow<&RandomConsumer.Request>( + from: storagePath + )?.block + ?? panic("No Request found") +} diff --git a/tests/scripts/request_can_fulfill.cdc b/tests/scripts/request_can_fulfill.cdc new file mode 100644 index 0000000..46c151d --- /dev/null +++ b/tests/scripts/request_can_fulfill.cdc @@ -0,0 +1,10 @@ +import "RandomConsumer" + +access(all) +fun main(address: Address, storagePath: StoragePath): Bool { + return getAuthAccount(address).storage + .borrow<&RandomConsumer.Request>( + from: storagePath + )?.canFullfill() + ?? panic("No Request found") +} diff --git a/tests/transactions/create_consumer.cdc b/tests/transactions/create_consumer.cdc new file mode 100644 index 0000000..7b4cd28 --- /dev/null +++ b/tests/transactions/create_consumer.cdc @@ -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) + } +} diff --git a/tests/transactions/fulfill_random_request.cdc b/tests/transactions/fulfill_random_request.cdc new file mode 100644 index 0000000..e2f1d2b --- /dev/null +++ b/tests/transactions/fulfill_random_request.cdc @@ -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( + 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) + } +} diff --git a/tests/transactions/request_randomness.cdc b/tests/transactions/request_randomness.cdc new file mode 100644 index 0000000..55de364 --- /dev/null +++ b/tests/transactions/request_randomness.cdc @@ -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( + from: RandomConsumer.ConsumerStoragePath + ) ?? panic("Consumer not found in storage") + signer.storage.save(<-consumer.requestRandomness(), to: storagePath) + } +}