Skip to content

Commit

Permalink
update to provide secure random range support
Browse files Browse the repository at this point in the history
  • Loading branch information
sisyphusSmiling committed Sep 21, 2024
1 parent 92e7520 commit d7e721a
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 31 deletions.
10 changes: 3 additions & 7 deletions solidity/src/CadenceArchWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
194 changes: 176 additions & 18 deletions solidity/src/CadenceRandomConsumer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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);
Expand All @@ -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
*
Expand All @@ -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"
);
}
}
9 changes: 4 additions & 5 deletions solidity/src/CoinToss.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion solidity/test/CoinToss.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit d7e721a

Please sign in to comment.