From d7e721a7bc1b9b1e77c375af875caaef0981368b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:42:35 -0600 Subject: [PATCH] update to provide secure random range support --- solidity/src/CadenceArchWrapper.sol | 10 +- solidity/src/CadenceRandomConsumer.sol | 194 ++++++++++++++++++++++--- solidity/src/CoinToss.sol | 9 +- solidity/test/CoinToss.t.sol | 2 +- 4 files changed, 184 insertions(+), 31 deletions(-) diff --git a/solidity/src/CadenceArchWrapper.sol b/solidity/src/CadenceArchWrapper.sol index 9756797..9a53d82 100644 --- a/solidity/src/CadenceArchWrapper.sol +++ b/solidity/src/CadenceArchWrapper.sol @@ -45,17 +45,13 @@ abstract contract CadenceArchWrapper { * @param flowHeight The Flow block height for which to get the random source. * @return randomSource The random source for the given Flow block height. */ - function _getRandomSource(uint64 flowHeight) internal view returns (uint64) { + function _getRandomSource(uint64 flowHeight) internal view returns (bytes32) { (bool ok, bytes memory data) = cadenceArch.staticcall(abi.encodeWithSignature("getRandomSource(uint64)", flowHeight)); require(ok, "Unsuccessful call to Cadence Arch pre-compile when fetching random source"); - // Decode the result as bytes32 and then cast it to uint64 + // Decode the result as bytes32 & return bytes32 result = abi.decode(data, (bytes32)); - - // Convert the bytes32 result to uint64 by casting, taking the least significant 8 bytes - uint64 output = uint64(uint256(result)); - - return output; + return result; } } diff --git a/solidity/src/CadenceRandomConsumer.sol b/solidity/src/CadenceRandomConsumer.sol index 0387d11..fe9b2dd 100644 --- a/solidity/src/CadenceRandomConsumer.sol +++ b/solidity/src/CadenceRandomConsumer.sol @@ -15,17 +15,62 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { uint64 flowHeight; // The EVM block height at which the request was made uint256 evmHeight; + // Whether the request has been fulfilled + bool fulfilled; } // Events - event RandomnessRequested(uint256 requestId, uint64 flowHeight, uint256 evmHeight); - event RandomnessFulfilled(uint256 requestId, uint64 flowHeight, uint256 evmHeight, uint256 randomResult); + event RandomnessRequested(uint256 indexed requestId, uint64 flowHeight, uint256 evmHeight); + event RandomnessSourced(uint256 indexed requestId, uint64 flowHeight, uint256 evmHeight, bytes32 randomSource); + event RandomnessFulfilled(uint256 indexed requestId, uint64 randomResult); // A list of requests where each request is identified by its index in the array Request[] private _requests; // A counter to keep track of the number of requests made, serving as the request ID uint256 private _requestCounter; + /////////////////// + // PUBLIC FUNCTIONS + /////////////////// + + /** + * @dev This method checks if a request can be fulfilled. + * + * @param requestId The ID of the randomness request to check. + * @return canFulfill Whether the request can be fulfilled. + */ + function canFulfillRequest(uint256 requestId) public view returns (bool) { + uint256 requestIndex = requestId - 1; + if (requestIndex >= _requests.length) { + return false; + } + Request memory request = _requests[requestIndex]; + uint64 flowHeight = _flowBlockHeight(); + return !request.fulfilled && request.flowHeight < flowHeight; + } + + ///////////////////// + // INTERNAL FUNCTIONS + ///////////////////// + + /** + * @dev This method returns a ***REVERTIBLE** random number in the range [min, max]. + * NOTE: The fact that this method is revertible means that it should only be used in cases where the caller is + * trusted not to revert on the result. + * + * @param min The minimum value of the range (inclusive). + * @param max The maximum value of the range (inclusive). + * @return random The random number in the range [min, max]. + */ + function _getRevertibleRandomInRange(uint64 min, uint64 max) internal view returns (uint64) { + uint256 randomValue = _aggregateRevertibleRandom256(); + return _getNumberInRange(randomValue, min, max); + } + + /** + * ----- COMMIT STEP ----- + */ + /** * @dev This method serves as the commit step in this contract's commit-reveal scheme * @@ -45,7 +90,7 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { // Store the heights at which the request was made. Note that the Flow block height and EVM block height are // not the same. But since Flow blocks ultimately determine usage of secure randomness, we gate requests by // Flow block height. - Request memory request = Request(_flowBlockHeight(), block.number); + Request memory request = Request(_flowBlockHeight(), block.number, false); // Store the request in the list of requests _requests.push(request); @@ -56,6 +101,83 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { return requestId; } + /** + * ----- REVEAL STEP ----- + */ + + /** + * @dev This method fulfills a random request and returns a random number as a uint64. + * + * @param requestId The ID of the randomness request to fulfill. + * @return randomResult The random number. + */ + function _fulfillRandomRequest(uint256 requestId) internal returns (uint64) { + bytes32 randomValue = _fulfillRandomness(requestId); + uint64 randomResult = uint64(uint256(keccak256(abi.encodePacked(randomValue, requestId)))); + + emit RandomnessFulfilled(requestId, randomResult); + + return randomResult; + } + + /** + * @dev This method fulfills a random request and returns a random number in the range [min, max]. + * + * @param requestId The ID of the randomness request to fulfill. + * @param min The minimum value of the range (inclusive). + * @param max The maximum value of the range (inclusive). + * @return randomResult The random number in the range [min, max]. + */ + function _fulfillRandomInRange(uint256 requestId, uint64 min, uint64 max) internal returns (uint64) { + // Ensure that the request is fulfilled at a Flow block height greater than the one at which the request was made + // Get the random source for the Flow block at which the request was made + bytes32 randomSource = _fulfillRandomness(requestId); + // Pack the randomResult into a uint256, hashing with the requestId to vary results across shared block heights. + uint256 randomValue = uint256(keccak256(abi.encodePacked(randomSource, requestId))); + + uint64 randomResult = _getNumberInRange(randomValue, min, max); + + emit RandomnessFulfilled(requestId, randomResult); + + return randomResult; + } + + //////////////////// + // PRIVATE FUNCTIONS + //////////////////// + + /** + * @dev This method returns a random number in the range [min, max]. + * + * @param min The minimum value of the range (inclusive). + * @param max The maximum value of the range (inclusive). + * @return random The random number in the range [min, max]. + */ + function _getNumberInRange(uint256 randomValue, uint64 min, uint64 max) private pure returns (uint64) { + require(max > min, "Max must be greater than min"); + + uint64 range = max - min + 1; + uint64 bitsRequired = _mostSignificantBit(range - 1); // Number of bits needed to cover the range + // uint256 randomValue = _aggregateRandom256(); // Aggregate 256 bits from 4 calls to _revertibleRandom() + uint256 mask = (1 << bitsRequired) - 1; // Create a bitmask to extract relevant bits + uint64 candidate = 0; // Initialize candidate + + while (true) { + candidate = uint64(randomValue & mask); // Apply bitmask to extract bits + // If candidate is in range, break and return + if (candidate < range) { + break; + } + // Shift to the next chunk of bits + randomValue = randomValue >> bitsRequired; + require(randomValue > 0, "Random number source exhausted"); + } + + uint64 randomResult = candidate + min; // Scale candidate to the range [min, max] + require(randomResult >= min && randomResult <= max, "Random number out of range"); + return randomResult; + } + /** * @dev This method serves as the reveal step in this contract's commit-reveal scheme * @@ -69,27 +191,63 @@ abstract contract CadenceRandomConsumer is CadenceArchWrapper { * @param requestId The ID of the randomness request to fulfill. * @return randomResult The random value generated from the Flow block. */ - function _fulfillRandomness(uint32 requestId) internal returns (uint64) { + function _fulfillRandomness(uint256 requestId) private returns (bytes32) { // Get the request details. Recall that request IDs are 1-indexed to allow for 0 to be used as an invalid value - uint32 requestIndex = requestId - 1; + uint256 requestIndex = requestId - 1; require(requestIndex < _requests.length, "Invalid request ID - value exceeds the number of existing requests"); + + // Access & validate the request Request memory request = _requests[requestIndex]; + _validateRequest(request); + request.fulfilled = true; // Mark the request as fulfilled - // Ensure that the request is fulfilled at a Flow block height greater than the one at which the request was made - uint64 flowHeight = _flowBlockHeight(); - require(request.flowHeight < flowHeight, "Cannot fulfill request until subsequent Flow network block height"); + // Get the random source for the Flow block at which the request was made, emit & return + bytes32 randomSource = _getRandomSource(request.flowHeight); - // Get the random source for the Flow block at which the request was made - uint64 randomResult = _getRandomSource(request.flowHeight); // returns bytes32 - // Pack the randomResult into a uint64, hashing with the requestId to vary results across shared block heights. - // The random seed returned from Cadence Arch is only 32 bytes. Here only 8 bytes were required, but if an - // implementing contract requires more random bytes, the source should be expanded into any arbitrary number of - // bytes using a PRG. - randomResult = uint64(uint256(keccak256(abi.encodePacked(randomResult, requestId)))); + emit RandomnessSourced(requestId, request.flowHeight, request.evmHeight, randomSource); - emit RandomnessFulfilled(requestId, request.flowHeight, request.evmHeight, randomResult); + return randomSource; + } - // Return the random result - return randomResult; + /** + * @dev This method aggregates 256 bits of randomness by calling _revertibleRandom() 4 times. + * + * @return randomValue The aggregated 256 bits of randomness. + */ + function _aggregateRevertibleRandom256() private view returns (uint256) { + // Call _revertibleRandom() 4 times to aggregate 256 bits of randomness + uint256 randomValue = uint256(_revertibleRandom()); + randomValue |= (uint256(_revertibleRandom()) << 64); + randomValue |= (uint256(_revertibleRandom()) << 128); + randomValue |= (uint256(_revertibleRandom()) << 192); + return randomValue; + } + + /** + * @dev This method returns the most significant bit of a uint64. + * + * @param x The input value. + * @return bits The most significant bit of the input value. + */ + function _mostSignificantBit(uint64 x) private pure returns (uint64) { + uint64 bits = 0; + while (x > 0) { + x >>= 1; + bits++; + } + return bits; + } + + /** + * @dev This method validates a given request, ensuring that it has not been fulfilled and that the Flow block height + * has passed. + * + * @param request The request to validate. + */ + function _validateRequest(Request memory request) private view { + require(!request.fulfilled, "Request already fulfilled"); + require( + request.flowHeight < _flowBlockHeight(), "Cannot fulfill request until subsequent Flow network block height" + ); } } diff --git a/solidity/src/CoinToss.sol b/solidity/src/CoinToss.sol index 3efd1fc..7cdb633 100644 --- a/solidity/src/CoinToss.sol +++ b/solidity/src/CoinToss.sol @@ -16,8 +16,8 @@ contract CoinToss is CadenceRandomConsumer { // A mapping to store the value sent by the user for each request mapping(uint256 => uint256) public openRequests; - event CoinFlipped(address indexed user, uint256 requestId, uint256 amount); - event CoinRevealed(address indexed user, uint256 requestId, uint8 coinFace, uint256 prize); + event CoinFlipped(address indexed user, uint256 indexed requestId, uint256 amount); + event CoinRevealed(address indexed user, uint256 indexed requestId, uint8 coinFace, uint256 prize); /** * @dev Checks if a user has an open request. @@ -54,9 +54,8 @@ contract CoinToss is CadenceRandomConsumer { // delete the open request from the coinTosses mapping delete coinTosses[msg.sender]; - // fulfill the random request - uint64 randomResult = _fulfillRandomness(uint32(requestId)); - uint8 coinFace = uint8(randomResult % 2); + // fulfill the random request within the inclusive range [0, 1] + uint8 coinFace = uint8(_fulfillRandomInRange(requestId, 0, 1)); // get the value sent in the flipCoin function & remove the request from the openRequests mapping uint256 amount = openRequests[requestId]; diff --git a/solidity/test/CoinToss.t.sol b/solidity/test/CoinToss.t.sol index 55ae667..445e21a 100644 --- a/solidity/test/CoinToss.t.sol +++ b/solidity/test/CoinToss.t.sol @@ -88,7 +88,7 @@ contract CoinTossTest is Test { vm.mockCall( coinToss.cadenceArch(), abi.encodeWithSignature("getRandomSource(uint64)", mockFlowBlockHeight), - abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000001)) + abi.encode(bytes32(0x0000000000000000000000000000000000000000000000000000000000000006)) ); vm.prank(user); coinToss.revealCoin();