|
@@ -6,6 +6,7 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
|
|
|
import "@pythnetwork/entropy-sdk-solidity/EntropyErrors.sol";
|
|
import "@pythnetwork/entropy-sdk-solidity/EntropyErrors.sol";
|
|
|
import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol";
|
|
import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol";
|
|
|
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
|
|
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
|
|
|
|
|
+import "@openzeppelin/contracts/utils/math/SafeCast.sol";
|
|
|
import "./EntropyState.sol";
|
|
import "./EntropyState.sol";
|
|
|
|
|
|
|
|
// Entropy implements a secure 2-party random number generation procedure. The protocol
|
|
// Entropy implements a secure 2-party random number generation procedure. The protocol
|
|
@@ -74,10 +75,26 @@ import "./EntropyState.sol";
|
|
|
// cases where the user chooses not to reveal.
|
|
// cases where the user chooses not to reveal.
|
|
|
contract Entropy is IEntropy, EntropyState {
|
|
contract Entropy is IEntropy, EntropyState {
|
|
|
// TODO: Use an upgradeable proxy
|
|
// TODO: Use an upgradeable proxy
|
|
|
- constructor(uint pythFeeInWei, address defaultProvider) {
|
|
|
|
|
|
|
+ constructor(
|
|
|
|
|
+ uint128 pythFeeInWei,
|
|
|
|
|
+ address defaultProvider,
|
|
|
|
|
+ bool prefillRequestStorage
|
|
|
|
|
+ ) {
|
|
|
_state.accruedPythFeesInWei = 0;
|
|
_state.accruedPythFeesInWei = 0;
|
|
|
_state.pythFeeInWei = pythFeeInWei;
|
|
_state.pythFeeInWei = pythFeeInWei;
|
|
|
_state.defaultProvider = defaultProvider;
|
|
_state.defaultProvider = defaultProvider;
|
|
|
|
|
+
|
|
|
|
|
+ if (prefillRequestStorage) {
|
|
|
|
|
+ // Write some data to every storage slot in the requests array such that new requests
|
|
|
|
|
+ // use a more consistent amount of gas.
|
|
|
|
|
+ // Note that these requests are not live because their sequenceNumber is 0.
|
|
|
|
|
+ for (uint8 i = 0; i < NUM_REQUESTS; i++) {
|
|
|
|
|
+ EntropyStructs.Request storage req = _state.requests[i];
|
|
|
|
|
+ req.provider = address(1);
|
|
|
|
|
+ req.blockNumber = 1234;
|
|
|
|
|
+ req.commitment = hex"0123";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Register msg.sender as a randomness provider. The arguments are the provider's configuration parameters
|
|
// Register msg.sender as a randomness provider. The arguments are the provider's configuration parameters
|
|
@@ -86,7 +103,7 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
//
|
|
//
|
|
|
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
|
|
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
|
|
|
function register(
|
|
function register(
|
|
|
- uint feeInWei,
|
|
|
|
|
|
|
+ uint128 feeInWei,
|
|
|
bytes32 commitment,
|
|
bytes32 commitment,
|
|
|
bytes calldata commitmentMetadata,
|
|
bytes calldata commitmentMetadata,
|
|
|
uint64 chainLength,
|
|
uint64 chainLength,
|
|
@@ -121,7 +138,7 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
// Withdraw a portion of the accumulated fees for the provider msg.sender.
|
|
// Withdraw a portion of the accumulated fees for the provider msg.sender.
|
|
|
// Calling this function will transfer `amount` wei to the caller (provided that they have accrued a sufficient
|
|
// Calling this function will transfer `amount` wei to the caller (provided that they have accrued a sufficient
|
|
|
// balance of fees in the contract).
|
|
// balance of fees in the contract).
|
|
|
- function withdraw(uint256 amount) public override {
|
|
|
|
|
|
|
+ function withdraw(uint128 amount) public override {
|
|
|
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
|
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
|
|
msg.sender
|
|
msg.sender
|
|
|
];
|
|
];
|
|
@@ -166,24 +183,33 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
providerInfo.sequenceNumber += 1;
|
|
providerInfo.sequenceNumber += 1;
|
|
|
|
|
|
|
|
// Check that fees were paid and increment the pyth / provider balances.
|
|
// Check that fees were paid and increment the pyth / provider balances.
|
|
|
- uint requiredFee = getFee(provider);
|
|
|
|
|
|
|
+ uint128 requiredFee = getFee(provider);
|
|
|
if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee();
|
|
if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee();
|
|
|
providerInfo.accruedFeesInWei += providerInfo.feeInWei;
|
|
providerInfo.accruedFeesInWei += providerInfo.feeInWei;
|
|
|
- _state.accruedPythFeesInWei += (msg.value - providerInfo.feeInWei);
|
|
|
|
|
|
|
+ _state.accruedPythFeesInWei += (SafeCast.toUint128(msg.value) -
|
|
|
|
|
+ providerInfo.feeInWei);
|
|
|
|
|
|
|
|
// Store the user's commitment so that we can fulfill the request later.
|
|
// Store the user's commitment so that we can fulfill the request later.
|
|
|
- EntropyStructs.Request storage req = _state.requests[
|
|
|
|
|
- requestKey(provider, assignedSequenceNumber)
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ // 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.provider = provider;
|
|
req.provider = provider;
|
|
|
req.sequenceNumber = assignedSequenceNumber;
|
|
req.sequenceNumber = assignedSequenceNumber;
|
|
|
- req.userCommitment = userCommitment;
|
|
|
|
|
- req.providerCommitment = providerInfo.currentCommitment;
|
|
|
|
|
- req.providerCommitmentSequenceNumber = providerInfo
|
|
|
|
|
- .currentCommitmentSequenceNumber;
|
|
|
|
|
|
|
+ req.numHashes = SafeCast.toUint32(
|
|
|
|
|
+ assignedSequenceNumber -
|
|
|
|
|
+ providerInfo.currentCommitmentSequenceNumber
|
|
|
|
|
+ );
|
|
|
|
|
+ req.commitment = keccak256(
|
|
|
|
|
+ bytes.concat(userCommitment, providerInfo.currentCommitment)
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
if (useBlockHash) {
|
|
if (useBlockHash) {
|
|
|
- req.blockNumber = block.number;
|
|
|
|
|
|
|
+ req.blockNumber = SafeCast.toUint96(block.number);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ req.blockNumber = 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
emit Requested(req);
|
|
emit Requested(req);
|
|
@@ -202,24 +228,24 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
bytes32 userRandomness,
|
|
bytes32 userRandomness,
|
|
|
bytes32 providerRevelation
|
|
bytes32 providerRevelation
|
|
|
) public override returns (bytes32 randomNumber) {
|
|
) public override returns (bytes32 randomNumber) {
|
|
|
- // TODO: do we need to check that this request exists?
|
|
|
|
|
// TODO: this method may need to be authenticated to prevent griefing
|
|
// TODO: this method may need to be authenticated to prevent griefing
|
|
|
- bytes32 key = requestKey(provider, sequenceNumber);
|
|
|
|
|
- EntropyStructs.Request storage req = _state.requests[key];
|
|
|
|
|
- // This invariant should be guaranteed to hold by the key construction procedure above, but check it
|
|
|
|
|
- // explicitly to be extra cautious.
|
|
|
|
|
- if (req.sequenceNumber != sequenceNumber)
|
|
|
|
|
- revert EntropyErrors.AssertionFailure();
|
|
|
|
|
-
|
|
|
|
|
- bool valid = isProofValid(
|
|
|
|
|
- req.providerCommitmentSequenceNumber,
|
|
|
|
|
- req.providerCommitment,
|
|
|
|
|
- sequenceNumber,
|
|
|
|
|
|
|
+ EntropyStructs.Request storage req = findRequest(
|
|
|
|
|
+ provider,
|
|
|
|
|
+ sequenceNumber
|
|
|
|
|
+ );
|
|
|
|
|
+ // Check that there is a request for the given provider / sequence number.
|
|
|
|
|
+ if (req.provider != provider || req.sequenceNumber != sequenceNumber)
|
|
|
|
|
+ revert EntropyErrors.NoSuchRequest();
|
|
|
|
|
+
|
|
|
|
|
+ bytes32 providerCommitment = constructProviderCommitment(
|
|
|
|
|
+ req.numHashes,
|
|
|
providerRevelation
|
|
providerRevelation
|
|
|
);
|
|
);
|
|
|
- if (!valid) revert EntropyErrors.IncorrectProviderRevelation();
|
|
|
|
|
- if (constructUserCommitment(userRandomness) != req.userCommitment)
|
|
|
|
|
- revert EntropyErrors.IncorrectUserRevelation();
|
|
|
|
|
|
|
+ bytes32 userCommitment = constructUserCommitment(userRandomness);
|
|
|
|
|
+ if (
|
|
|
|
|
+ keccak256(bytes.concat(userCommitment, providerCommitment)) !=
|
|
|
|
|
+ req.commitment
|
|
|
|
|
+ ) revert EntropyErrors.IncorrectRevelation();
|
|
|
|
|
|
|
|
bytes32 blockHash = bytes32(uint256(0));
|
|
bytes32 blockHash = bytes32(uint256(0));
|
|
|
if (req.blockNumber != 0) {
|
|
if (req.blockNumber != 0) {
|
|
@@ -240,7 +266,7 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
randomNumber
|
|
randomNumber
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- delete _state.requests[key];
|
|
|
|
|
|
|
+ clearRequest(provider, sequenceNumber);
|
|
|
|
|
|
|
|
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
|
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
|
|
provider
|
|
provider
|
|
@@ -270,13 +296,12 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
address provider,
|
|
address provider,
|
|
|
uint64 sequenceNumber
|
|
uint64 sequenceNumber
|
|
|
) public view override returns (EntropyStructs.Request memory req) {
|
|
) public view override returns (EntropyStructs.Request memory req) {
|
|
|
- bytes32 key = requestKey(provider, sequenceNumber);
|
|
|
|
|
- req = _state.requests[key];
|
|
|
|
|
|
|
+ req = findRequest(provider, sequenceNumber);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getFee(
|
|
function getFee(
|
|
|
address provider
|
|
address provider
|
|
|
- ) public view override returns (uint feeAmount) {
|
|
|
|
|
|
|
+ ) public view override returns (uint128 feeAmount) {
|
|
|
return _state.providers[provider].feeInWei + _state.pythFeeInWei;
|
|
return _state.providers[provider].feeInWei + _state.pythFeeInWei;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -284,7 +309,7 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
public
|
|
public
|
|
|
view
|
|
view
|
|
|
override
|
|
override
|
|
|
- returns (uint accruedPythFeesInWei)
|
|
|
|
|
|
|
+ returns (uint128 accruedPythFeesInWei)
|
|
|
{
|
|
{
|
|
|
return _state.accruedPythFeesInWei;
|
|
return _state.accruedPythFeesInWei;
|
|
|
}
|
|
}
|
|
@@ -305,31 +330,90 @@ contract Entropy is IEntropy, EntropyState {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Create a unique key for an in-flight randomness request (to store it in the contract state)
|
|
|
|
|
|
|
+ // Create a unique key for an in-flight randomness request. Returns both a long key for use in the requestsOverflow
|
|
|
|
|
+ // mapping and a short key for use in the requests array.
|
|
|
function requestKey(
|
|
function requestKey(
|
|
|
address provider,
|
|
address provider,
|
|
|
uint64 sequenceNumber
|
|
uint64 sequenceNumber
|
|
|
- ) internal pure returns (bytes32 hash) {
|
|
|
|
|
|
|
+ ) internal pure returns (bytes32 hash, uint8 shortHash) {
|
|
|
hash = keccak256(abi.encodePacked(provider, sequenceNumber));
|
|
hash = keccak256(abi.encodePacked(provider, sequenceNumber));
|
|
|
|
|
+ shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Validate that revelation at sequenceNumber is the correct value in the hash chain for a provider whose
|
|
|
|
|
- // last known revealed random number was lastRevelation at lastSequenceNumber.
|
|
|
|
|
- function isProofValid(
|
|
|
|
|
- uint64 lastSequenceNumber,
|
|
|
|
|
- bytes32 lastRevelation,
|
|
|
|
|
- uint64 sequenceNumber,
|
|
|
|
|
|
|
+ // Construct a provider's commitment given their revealed random number and the distance in the hash chain
|
|
|
|
|
+ // between the commitment and the revealed random number.
|
|
|
|
|
+ function constructProviderCommitment(
|
|
|
|
|
+ uint64 numHashes,
|
|
|
bytes32 revelation
|
|
bytes32 revelation
|
|
|
- ) internal pure returns (bool valid) {
|
|
|
|
|
- if (sequenceNumber <= lastSequenceNumber)
|
|
|
|
|
- revert EntropyErrors.AssertionFailure();
|
|
|
|
|
-
|
|
|
|
|
- bytes32 currentHash = revelation;
|
|
|
|
|
- while (sequenceNumber > lastSequenceNumber) {
|
|
|
|
|
|
|
+ ) internal pure returns (bytes32 currentHash) {
|
|
|
|
|
+ currentHash = revelation;
|
|
|
|
|
+ while (numHashes > 0) {
|
|
|
currentHash = keccak256(bytes.concat(currentHash));
|
|
currentHash = keccak256(bytes.concat(currentHash));
|
|
|
- sequenceNumber -= 1;
|
|
|
|
|
|
|
+ numHashes -= 1;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 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).
|
|
|
|
|
+ function findRequest(
|
|
|
|
|
+ address provider,
|
|
|
|
|
+ uint64 sequenceNumber
|
|
|
|
|
+ ) internal view returns (EntropyStructs.Request storage req) {
|
|
|
|
|
+ (bytes32 key, uint8 shortKey) = requestKey(provider, sequenceNumber);
|
|
|
|
|
+
|
|
|
|
|
+ req = _state.requests[shortKey];
|
|
|
|
|
+ if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
|
|
|
|
|
+ return req;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ req = _state.requestsOverflow[key];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clear the storage for an in-flight request, deleting it from the hash table.
|
|
|
|
|
+ function clearRequest(address provider, uint64 sequenceNumber) internal {
|
|
|
|
|
+ (bytes32 key, uint8 shortKey) = requestKey(provider, sequenceNumber);
|
|
|
|
|
+
|
|
|
|
|
+ EntropyStructs.Request storage req = _state.requests[shortKey];
|
|
|
|
|
+ if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
|
|
|
|
|
+ req.sequenceNumber = 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ delete _state.requestsOverflow[key];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Allocate storage space for a new in-flight request. This method returns a pointer to a storage slot
|
|
|
|
|
+ // that the caller should overwrite with the new request. Note that the memory at this storage slot may
|
|
|
|
|
+ // -- and will -- be filled with arbitrary values, so the caller *must* overwrite every field of the returned
|
|
|
|
|
+ // struct.
|
|
|
|
|
+ function allocRequest(
|
|
|
|
|
+ address provider,
|
|
|
|
|
+ uint64 sequenceNumber
|
|
|
|
|
+ ) internal returns (EntropyStructs.Request storage req) {
|
|
|
|
|
+ (, uint8 shortKey) = requestKey(provider, sequenceNumber);
|
|
|
|
|
+
|
|
|
|
|
+ req = _state.requests[shortKey];
|
|
|
|
|
+ if (isActive(req)) {
|
|
|
|
|
+ // There's already a prior active request in the storage slot we want to use.
|
|
|
|
|
+ // Overflow the prior request to the requestsOverflow mapping.
|
|
|
|
|
+ // It is important that this code overflows the *prior* request to the mapping, and not the new request.
|
|
|
|
|
+ // There is a chance that some requests never get revealed and remain active forever. We do not want such
|
|
|
|
|
+ // requests to fill up all of the space in the array and cause all new requests to incur the higher gas cost
|
|
|
|
|
+ // of the mapping.
|
|
|
|
|
+ //
|
|
|
|
|
+ // This operation is expensive, but should be rare. If overflow happens frequently, increase
|
|
|
|
|
+ // the size of the requests array to support more concurrent active requests.
|
|
|
|
|
+ (bytes32 reqKey, ) = requestKey(req.provider, req.sequenceNumber);
|
|
|
|
|
+ _state.requestsOverflow[reqKey] = req;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- valid = currentHash == lastRevelation;
|
|
|
|
|
|
|
+ // Returns true if a request is active, i.e., its corresponding random value has not yet been revealed.
|
|
|
|
|
+ function isActive(
|
|
|
|
|
+ EntropyStructs.Request storage req
|
|
|
|
|
+ ) internal view returns (bool) {
|
|
|
|
|
+ // Note that a provider's initial registration occupies sequence number 0, so there is no way to construct
|
|
|
|
|
+ // a randomness request with sequence number 0.
|
|
|
|
|
+ return req.sequenceNumber != 0;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|