|
|
@@ -6,6 +6,7 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
|
|
|
import "@pythnetwork/entropy-sdk-solidity/EntropyErrors.sol";
|
|
|
import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol";
|
|
|
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
|
|
|
+import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol";
|
|
|
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
|
|
|
import "./EntropyState.sol";
|
|
|
|
|
|
@@ -163,21 +164,15 @@ abstract contract Entropy is IEntropy, EntropyState {
|
|
|
require(sent, "withdrawal to msg.sender failed");
|
|
|
}
|
|
|
|
|
|
- // As a user, request a random number from `provider`. Prior to calling this method, the user should
|
|
|
- // generate a random number x and keep it secret. The user should then compute hash(x) and pass that
|
|
|
- // as the userCommitment argument. (You may call the constructUserCommitment method to compute the hash.)
|
|
|
- //
|
|
|
- // This method returns a sequence number. The user should pass this sequence number to
|
|
|
- // their chosen provider (the exact method for doing so will depend on the provider) to retrieve the provider's
|
|
|
- // number. The user should then call fulfillRequest to construct the final random number.
|
|
|
- //
|
|
|
- // This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value.
|
|
|
- // Note that excess value is *not* refunded to the caller.
|
|
|
- function request(
|
|
|
+ // requestHelper allocates and returns a new request for the given provider.
|
|
|
+ // Note: This method will revert unless the caller provides a sufficient fee
|
|
|
+ // (at least getFee(provider)) as msg.value.
|
|
|
+ function requestHelper(
|
|
|
address provider,
|
|
|
bytes32 userCommitment,
|
|
|
- bool useBlockHash
|
|
|
- ) public payable override returns (uint64 assignedSequenceNumber) {
|
|
|
+ bool useBlockhash,
|
|
|
+ bool isRequestWithCallback
|
|
|
+ ) internal returns (EntropyStructs.Request storage req) {
|
|
|
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
|
|
provider
|
|
|
];
|
|
|
@@ -185,7 +180,7 @@ abstract contract Entropy is IEntropy, EntropyState {
|
|
|
revert EntropyErrors.NoSuchProvider();
|
|
|
|
|
|
// Assign a sequence number to the request
|
|
|
- assignedSequenceNumber = providerInfo.sequenceNumber;
|
|
|
+ uint64 assignedSequenceNumber = providerInfo.sequenceNumber;
|
|
|
if (assignedSequenceNumber >= providerInfo.endSequenceNumber)
|
|
|
revert EntropyErrors.OutOfRandomness();
|
|
|
providerInfo.sequenceNumber += 1;
|
|
|
@@ -200,10 +195,7 @@ abstract contract Entropy is IEntropy, EntropyState {
|
|
|
// Store the user's commitment so that we can fulfill the request later.
|
|
|
// Warning: this code needs to overwrite *every* field in the request, because the returned request can be
|
|
|
// filled with arbitrary data.
|
|
|
- EntropyStructs.Request storage req = allocRequest(
|
|
|
- provider,
|
|
|
- assignedSequenceNumber
|
|
|
- );
|
|
|
+ req = allocRequest(provider, assignedSequenceNumber);
|
|
|
req.provider = provider;
|
|
|
req.sequenceNumber = assignedSequenceNumber;
|
|
|
req.numHashes = SafeCast.toUint32(
|
|
|
@@ -216,51 +208,87 @@ abstract contract Entropy is IEntropy, EntropyState {
|
|
|
req.requester = msg.sender;
|
|
|
|
|
|
req.blockNumber = SafeCast.toUint64(block.number);
|
|
|
- req.useBlockhash = useBlockHash;
|
|
|
+ req.useBlockhash = useBlockhash;
|
|
|
+ req.isRequestWithCallback = isRequestWithCallback;
|
|
|
+ }
|
|
|
|
|
|
+ // As a user, request a random number from `provider`. Prior to calling this method, the user should
|
|
|
+ // generate a random number x and keep it secret. The user should then compute hash(x) and pass that
|
|
|
+ // as the userCommitment argument. (You may call the constructUserCommitment method to compute the hash.)
|
|
|
+ //
|
|
|
+ // This method returns a sequence number. The user should pass this sequence number to
|
|
|
+ // their chosen provider (the exact method for doing so will depend on the provider) to retrieve the provider's
|
|
|
+ // number. The user should then call fulfillRequest to construct the final random number.
|
|
|
+ //
|
|
|
+ // This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value.
|
|
|
+ // Note that excess value is *not* refunded to the caller.
|
|
|
+ function request(
|
|
|
+ address provider,
|
|
|
+ bytes32 userCommitment,
|
|
|
+ bool useBlockHash
|
|
|
+ ) public payable override returns (uint64 assignedSequenceNumber) {
|
|
|
+ EntropyStructs.Request storage req = requestHelper(
|
|
|
+ provider,
|
|
|
+ userCommitment,
|
|
|
+ useBlockHash,
|
|
|
+ false
|
|
|
+ );
|
|
|
+ assignedSequenceNumber = req.sequenceNumber;
|
|
|
emit Requested(req);
|
|
|
}
|
|
|
|
|
|
- // Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof
|
|
|
- // against the corresponding commitments in the in-flight request. If both values are validated, this function returns
|
|
|
- // the corresponding random number.
|
|
|
+ // Request a random number. The method expects the provider address and a secret random number
|
|
|
+ // in the arguments. It returns a sequence number.
|
|
|
//
|
|
|
- // Note that this function can only be called once per in-flight request. Calling this function deletes the stored
|
|
|
- // request information (so that the contract doesn't use a linear amount of storage in the number of requests).
|
|
|
- // If you need to use the returned random number more than once, you are responsible for storing it.
|
|
|
+ // The address calling this function should be a contract that inherits from the IEntropyConsumer interface.
|
|
|
+ // The `entropyCallback` method on that interface will receive a callback with the generated random number.
|
|
|
//
|
|
|
- // This function must be called by the same `msg.sender` that originally requested the random number. This check
|
|
|
- // prevents denial-of-service attacks where another actor front-runs the requester's reveal transaction.
|
|
|
- function reveal(
|
|
|
+ // This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value.
|
|
|
+ // Note that excess value is *not* refunded to the caller.
|
|
|
+ function requestWithCallback(
|
|
|
address provider,
|
|
|
- uint64 sequenceNumber,
|
|
|
- bytes32 userRandomness,
|
|
|
- bytes32 providerRevelation
|
|
|
- ) public override returns (bytes32 randomNumber) {
|
|
|
- EntropyStructs.Request storage req = findRequest(
|
|
|
+ bytes32 userRandomNumber
|
|
|
+ ) public payable override returns (uint64) {
|
|
|
+ EntropyStructs.Request storage req = requestHelper(
|
|
|
provider,
|
|
|
- sequenceNumber
|
|
|
+ constructUserCommitment(userRandomNumber),
|
|
|
+ // If useBlockHash is set to true, it allows a scenario in which the provider and miner can collude.
|
|
|
+ // If we remove the blockHash from this, the provider would have no choice but to provide its committed
|
|
|
+ // random number. Hence, useBlockHash is set to false.
|
|
|
+ false,
|
|
|
+ true
|
|
|
+ );
|
|
|
+
|
|
|
+ emit RequestedWithCallback(
|
|
|
+ provider,
|
|
|
+ req.requester,
|
|
|
+ req.sequenceNumber,
|
|
|
+ userRandomNumber,
|
|
|
+ req
|
|
|
);
|
|
|
- // Check that there is an active request for the given provider / sequence number.
|
|
|
- if (
|
|
|
- req.sequenceNumber == 0 ||
|
|
|
- req.provider != provider ||
|
|
|
- req.sequenceNumber != sequenceNumber
|
|
|
- ) revert EntropyErrors.NoSuchRequest();
|
|
|
|
|
|
- if (req.requester != msg.sender) revert EntropyErrors.Unauthorized();
|
|
|
+ return req.sequenceNumber;
|
|
|
+ }
|
|
|
|
|
|
+ // This method validates the provided user's revelation and provider's revelation against the corresponding
|
|
|
+ // commitment in the in-flight request. If both values are validated, this method will update the provider
|
|
|
+ // current commitment and returns the generated random number.
|
|
|
+ function revealHelper(
|
|
|
+ EntropyStructs.Request storage req,
|
|
|
+ bytes32 userRevelation,
|
|
|
+ bytes32 providerRevelation
|
|
|
+ ) internal returns (bytes32 randomNumber, bytes32 blockHash) {
|
|
|
bytes32 providerCommitment = constructProviderCommitment(
|
|
|
req.numHashes,
|
|
|
providerRevelation
|
|
|
);
|
|
|
- bytes32 userCommitment = constructUserCommitment(userRandomness);
|
|
|
+ bytes32 userCommitment = constructUserCommitment(userRevelation);
|
|
|
if (
|
|
|
keccak256(bytes.concat(userCommitment, providerCommitment)) !=
|
|
|
req.commitment
|
|
|
) revert EntropyErrors.IncorrectRevelation();
|
|
|
|
|
|
- bytes32 blockHash = bytes32(uint256(0));
|
|
|
+ blockHash = bytes32(uint256(0));
|
|
|
if (req.useBlockhash) {
|
|
|
bytes32 _blockHash = blockhash(req.blockNumber);
|
|
|
|
|
|
@@ -277,28 +305,110 @@ abstract contract Entropy is IEntropy, EntropyState {
|
|
|
}
|
|
|
|
|
|
randomNumber = combineRandomValues(
|
|
|
- userRandomness,
|
|
|
+ userRevelation,
|
|
|
providerRevelation,
|
|
|
blockHash
|
|
|
);
|
|
|
|
|
|
+ EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
|
|
+ req.provider
|
|
|
+ ];
|
|
|
+ if (providerInfo.currentCommitmentSequenceNumber < req.sequenceNumber) {
|
|
|
+ providerInfo.currentCommitmentSequenceNumber = req.sequenceNumber;
|
|
|
+ providerInfo.currentCommitment = providerRevelation;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof
|
|
|
+ // against the corresponding commitments in the in-flight request. If both values are validated, this function returns
|
|
|
+ // the corresponding random number.
|
|
|
+ //
|
|
|
+ // Note that this function can only be called once per in-flight request. Calling this function deletes the stored
|
|
|
+ // request information (so that the contract doesn't use a linear amount of storage in the number of requests).
|
|
|
+ // If you need to use the returned random number more than once, you are responsible for storing it.
|
|
|
+ //
|
|
|
+ // This function must be called by the same `msg.sender` that originally requested the random number. This check
|
|
|
+ // prevents denial-of-service attacks where another actor front-runs the requester's reveal transaction.
|
|
|
+ function reveal(
|
|
|
+ address provider,
|
|
|
+ uint64 sequenceNumber,
|
|
|
+ bytes32 userRevelation,
|
|
|
+ bytes32 providerRevelation
|
|
|
+ ) public override returns (bytes32 randomNumber) {
|
|
|
+ EntropyStructs.Request storage req = findActiveRequest(
|
|
|
+ provider,
|
|
|
+ sequenceNumber
|
|
|
+ );
|
|
|
+
|
|
|
+ if (req.isRequestWithCallback) {
|
|
|
+ revert EntropyErrors.InvalidRevealCall();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (req.requester != msg.sender) {
|
|
|
+ revert EntropyErrors.Unauthorized();
|
|
|
+ }
|
|
|
+ bytes32 blockHash;
|
|
|
+ (randomNumber, blockHash) = revealHelper(
|
|
|
+ req,
|
|
|
+ userRevelation,
|
|
|
+ providerRevelation
|
|
|
+ );
|
|
|
emit Revealed(
|
|
|
req,
|
|
|
- userRandomness,
|
|
|
+ userRevelation,
|
|
|
providerRevelation,
|
|
|
blockHash,
|
|
|
randomNumber
|
|
|
);
|
|
|
-
|
|
|
clearRequest(provider, sequenceNumber);
|
|
|
+ }
|
|
|
|
|
|
- EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
|
|
- provider
|
|
|
- ];
|
|
|
- if (providerInfo.currentCommitmentSequenceNumber < sequenceNumber) {
|
|
|
- providerInfo.currentCommitmentSequenceNumber = sequenceNumber;
|
|
|
- providerInfo.currentCommitment = providerRevelation;
|
|
|
+ // Fulfill a request for a random number and call back the requester. This method validates the provided userRandomness
|
|
|
+ // and provider's revelation against the corresponding commitment in the in-flight request. If both values are validated,
|
|
|
+ // this function calls the requester's entropyCallback method with the sequence number and the random number as arguments.
|
|
|
+ //
|
|
|
+ // Note that this function can only be called once per in-flight request. Calling this function deletes the stored
|
|
|
+ // request information (so that the contract doesn't use a linear amount of storage in the number of requests).
|
|
|
+ // If you need to use the returned random number more than once, you are responsible for storing it.
|
|
|
+ //
|
|
|
+ // Anyone can call this method to fulfill a request, but the callback will only be made to the original requester.
|
|
|
+ function revealWithCallback(
|
|
|
+ address provider,
|
|
|
+ uint64 sequenceNumber,
|
|
|
+ bytes32 userRandomNumber,
|
|
|
+ bytes32 providerRevelation
|
|
|
+ ) public override {
|
|
|
+ EntropyStructs.Request storage req = findActiveRequest(
|
|
|
+ provider,
|
|
|
+ sequenceNumber
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!req.isRequestWithCallback) {
|
|
|
+ revert EntropyErrors.InvalidRevealCall();
|
|
|
}
|
|
|
+ bytes32 blockHash;
|
|
|
+ bytes32 randomNumber;
|
|
|
+ (randomNumber, blockHash) = revealHelper(
|
|
|
+ req,
|
|
|
+ userRandomNumber,
|
|
|
+ providerRevelation
|
|
|
+ );
|
|
|
+
|
|
|
+ address callAddress = req.requester;
|
|
|
+
|
|
|
+ emit RevealedWithCallback(
|
|
|
+ req,
|
|
|
+ userRandomNumber,
|
|
|
+ providerRevelation,
|
|
|
+ randomNumber
|
|
|
+ );
|
|
|
+
|
|
|
+ clearRequest(provider, sequenceNumber);
|
|
|
+
|
|
|
+ IEntropyConsumer(callAddress)._entropyCallback(
|
|
|
+ sequenceNumber,
|
|
|
+ randomNumber
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
function getProviderInfo(
|
|
|
@@ -408,6 +518,23 @@ abstract contract Entropy is IEntropy, EntropyState {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Find an in-flight active request for given the provider and the sequence number.
|
|
|
+ // This method returns a reference to the request, and will revert if the request is
|
|
|
+ // not active.
|
|
|
+ function findActiveRequest(
|
|
|
+ address provider,
|
|
|
+ uint64 sequenceNumber
|
|
|
+ ) internal view returns (EntropyStructs.Request storage req) {
|
|
|
+ req = findRequest(provider, sequenceNumber);
|
|
|
+
|
|
|
+ // Check there is an active request for the given provider and sequence number.
|
|
|
+ if (
|
|
|
+ !isActive(req) ||
|
|
|
+ req.provider != provider ||
|
|
|
+ req.sequenceNumber != sequenceNumber
|
|
|
+ ) revert EntropyErrors.NoSuchRequest();
|
|
|
+ }
|
|
|
+
|
|
|
// Find an in-flight request.
|
|
|
// Note that this method can return requests that are not currently active. The caller is responsible for checking
|
|
|
// that the returned request is active (if they care).
|