From 9a8a829c462faf4a396b604f664095eaf45ade7d Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:37:14 -0700 Subject: [PATCH 1/3] expose PRG via RandomConsumer fulfillment --- contracts/RandomConsumer.cdc | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/contracts/RandomConsumer.cdc b/contracts/RandomConsumer.cdc index ec04cb9..88978f3 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 @@ -225,7 +226,7 @@ access(all) contract RandomConsumer { let reqUUID = request.uuid // Create PRG from the provided request & generate a random number - let prg = self._getPRGFromRequest(request: <-request) + let prg = self.fulfillWithPRG(request: <-request) let res = prg.nextUInt64() emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res) @@ -251,9 +252,9 @@ 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 prg = self.fulfillWithPRG(request: <-request) let res = RandomConsumer.getNumberInRange(prg: prg, min: min, max: max) emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res) @@ -261,15 +262,22 @@ 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 + /// @return A PRG object from which to generate random values in assocation with the fulfilled request /// - access(self) fun _getPRGFromRequest(request: @Request): Xorshift128plus.PRG { + access(Reveal) fun fulfillWithPRG(request: @Request): Xorshift128plus.PRG { + emit RandomnessFulfilledWithPRG(requestUUID: request.uuid) + let source = request._fulfill() let salt = request.uuid.toBigEndianBytes() Burner.burn(<-request) From 9dcf0c898c4fc8612eef7936824d3f310ec78130 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:35:18 -0700 Subject: [PATCH 2/3] fix RandomConsumer.Consumer PRG fulfillment event patterns --- contracts/RandomConsumer.cdc | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/contracts/RandomConsumer.cdc b/contracts/RandomConsumer.cdc index 88978f3..75c5075 100644 --- a/contracts/RandomConsumer.cdc +++ b/contracts/RandomConsumer.cdc @@ -226,7 +226,7 @@ access(all) contract RandomConsumer { let reqUUID = request.uuid // Create PRG from the provided request & generate a random number - let prg = self.fulfillWithPRG(request: <-request) + let prg = self._getPRGFromRequest(request: <-request) let res = prg.nextUInt64() emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res) @@ -254,7 +254,7 @@ access(all) contract RandomConsumer { let reqUUID = request.uuid // Create PRG from the provided request & generate a random number & generate a random number in the range - let prg = self.fulfillWithPRG(request: <-request) + let prg = self._getPRGFromRequest(request: <-request) let res = RandomConsumer.getNumberInRange(prg: prg, min: min, max: max) emit RandomnessFulfilled(requestUUID: reqUUID, randomResult: res) @@ -276,8 +276,23 @@ access(all) contract RandomConsumer { /// @return A PRG object from which to generate random values in assocation with the fulfilled request /// access(Reveal) fun fulfillWithPRG(request: @Request): Xorshift128plus.PRG { - emit RandomnessFulfilledWithPRG(requestUUID: request.uuid) + 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 from which this Consumer can generate a single random value to fulfill the request + /// + access(self) + fun _getPRGFromRequest(request: @Request): Xorshift128plus.PRG { let source = request._fulfill() let salt = request.uuid.toBigEndianBytes() Burner.burn(<-request) From 6928c7a1c481c1be93cdf836b076a3a5be8f35b5 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:46:16 -0700 Subject: [PATCH 3/3] add happy path test cases for RandomConsumer.cdc --- tests/random_consumer_tests.cdc | 65 +++++++++++++++++++ tests/scripts/get_request_blockheight.cdc | 10 +++ tests/scripts/request_can_fulfill.cdc | 10 +++ tests/transactions/create_consumer.cdc | 10 +++ tests/transactions/fulfill_random_request.cdc | 18 +++++ tests/transactions/request_randomness.cdc | 13 ++++ 6 files changed, 126 insertions(+) create mode 100644 tests/random_consumer_tests.cdc create mode 100644 tests/scripts/get_request_blockheight.cdc create mode 100644 tests/scripts/request_can_fulfill.cdc create mode 100644 tests/transactions/create_consumer.cdc create mode 100644 tests/transactions/fulfill_random_request.cdc create mode 100644 tests/transactions/request_randomness.cdc 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) + } +}