pyth-crosschain/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol

611 lines
27 KiB
Solidity

// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
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";
// Entropy implements a secure 2-party random number generation procedure. The protocol
// is an extension of a simple commit/reveal protocol. The original version has the following steps:
//
// 1. Two parties A and B each draw a random number x_{A,B}
// 2. A and B then share h_{A,B} = hash(x_{A,B})
// 3. A and B reveal x_{A,B}
// 4. Both parties verify that hash(x_{A, B}) == h_{A,B}
// 5. The random number r = hash(x_A, x_B)
//
// This protocol has the property that the result is random as long as either A or B are honest.
// Thus, neither party needs to trust the other -- as long as they are themselves honest, they can
// ensure that the result r is random.
//
// Entropy implements a version of this protocol that is optimized for on-chain usage. The
// key difference is that one of the participants (the provider) commits to a sequence of random numbers
// up-front using a hash chain. Users of the protocol then simply grab the next random number in the sequence.
//
// Setup: The provider P computes a sequence of N random numbers, x_i (i = 0...N-1):
// x_{N-1} = random()
// x_i = hash(x_{i + 1})
// The provider commits to x_0 by posting it to the contract. Each random number in the sequence can then be
// verified against the previous one in the sequence by hashing it, i.e., hash(x_i) == x_{i - 1}
//
// Request: To produce a random number, the following steps occur.
// 1. The user draws a random number x_U, and submits h_U = hash(x_U) to this contract
// 2. The contract remembers h_U and assigns it an incrementing sequence number i, representing which
// of the provider's random numbers the user will receive.
// 3. The user submits an off-chain request (e.g. via HTTP) to the provider to reveal the i'th random number.
// 4. The provider checks the on-chain sequence number and ensures it is > i. If it is not, the provider
// refuses to reveal the ith random number. The provider should wait for a sufficient number of block confirmations
// to ensure that the request does not get re-orged out of the blockchain.
// 5. The provider reveals x_i to the user.
// 6. The user submits both the provider's revealed number x_i and their own x_U to the contract.
// 7. The contract verifies hash(x_i) == x_{i-1} to prove that x_i is the i'th random number. The contract also checks that hash(x_U) == h_U.
// The contract stores x_i as the i'th random number to reuse for future verifications.
// 8. If both of the above conditions are satisfied, the random number r = hash(x_i, x_U).
// (Optional) as an added security mechanism, this step can further incorporate the blockhash of the block that the
// request transaction landed in: r = hash(x_i, x_U, blockhash).
//
// This protocol has the same security properties as the 2-party randomness protocol above: as long as either
// the provider or user is honest, the number r is random. Honesty here means that the participant keeps their
// random number x a secret until the revelation phase (step 5) of the protocol. Note that providers need to
// be careful to ensure their off-chain service isn't compromised to reveal the random numbers -- if this occurs,
// then users will be able to influence the random number r.
//
// The Entropy implementation of the above protocol allows anyone to permissionlessly register to be a
// randomness provider. Users then choose which provider to request randomness from. Each provider can set
// their own fee for the service. In addition, the Entropy contract charges a flat fee that goes to the
// Pyth protocol for each requested random number. Fees are paid in the native token of the network.
//
// This implementation has two intricacies that merit further explanation. First, the implementation supports
// multiple concurrent requests for randomness by checking the provider's random number against their last known
// random number. Verification therefore may require computing multiple hashes (~ the number of concurrent requests).
// Second, the implementation allows providers to rotate their commitment at any time. This operation allows
// providers to commit to additional random numbers once they reach the end of their initial sequence, or rotate out
// a compromised sequence. On rotation, any in-flight requests continue to use the pre-rotation commitment.
// Providers can use the sequence number of the request along with the event log of their registrations to determine
// which hash chain contains the requested random number.
//
// Warning to integrators:
// An important caveat of this protocol is that the user can compute the random number r before
// revealing their own number to the contract. This property means that the user can choose to halt the
// protocol prior to the random number being revealed (i.e., prior to step (6) above). Integrators should ensure that
// the user is always incentivized to reveal their random number, and that the protocol has an escape hatch for
// cases where the user chooses not to reveal.
abstract contract Entropy is IEntropy, EntropyState {
function _initialize(
address admin,
uint128 pythFeeInWei,
address defaultProvider,
bool prefillRequestStorage
) internal {
require(admin != address(0), "admin is zero address");
require(
defaultProvider != address(0),
"defaultProvider is zero address"
);
_state.admin = admin;
_state.accruedPythFeesInWei = 0;
_state.pythFeeInWei = pythFeeInWei;
_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
// and initial commitment. Re-registering the same provider rotates the provider's commitment (and updates
// the feeInWei).
//
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
function register(
uint128 feeInWei,
bytes32 commitment,
bytes calldata commitmentMetadata,
uint64 chainLength,
bytes calldata uri
) public override {
if (chainLength == 0) revert EntropyErrors.AssertionFailure();
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
// NOTE: this method implementation depends on the fact that ProviderInfo will be initialized to all-zero.
// Specifically, accruedFeesInWei is intentionally not set. On initial registration, it will be zero,
// then on future registrations, it will be unchanged. Similarly, provider.sequenceNumber defaults to 0
// on initial registration.
provider.feeInWei = feeInWei;
provider.originalCommitment = commitment;
provider.originalCommitmentSequenceNumber = provider.sequenceNumber;
provider.currentCommitment = commitment;
provider.currentCommitmentSequenceNumber = provider.sequenceNumber;
provider.commitmentMetadata = commitmentMetadata;
provider.endSequenceNumber = provider.sequenceNumber + chainLength;
provider.uri = uri;
provider.sequenceNumber += 1;
emit Registered(provider);
}
// 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
// balance of fees in the contract).
function withdraw(uint128 amount) public override {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
msg.sender
];
// Use checks-effects-interactions pattern to prevent reentrancy attacks.
require(
providerInfo.accruedFeesInWei >= amount,
"Insufficient balance"
);
providerInfo.accruedFeesInWei -= amount;
// Interaction with an external contract or token transfer
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "withdrawal to msg.sender failed");
}
// 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,
bool isRequestWithCallback
) internal returns (EntropyStructs.Request storage req) {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
provider
];
if (_state.providers[provider].sequenceNumber == 0)
revert EntropyErrors.NoSuchProvider();
// Assign a sequence number to the request
uint64 assignedSequenceNumber = providerInfo.sequenceNumber;
if (assignedSequenceNumber >= providerInfo.endSequenceNumber)
revert EntropyErrors.OutOfRandomness();
providerInfo.sequenceNumber += 1;
// Check that fees were paid and increment the pyth / provider balances.
uint128 requiredFee = getFee(provider);
if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee();
providerInfo.accruedFeesInWei += providerInfo.feeInWei;
_state.accruedPythFeesInWei += (SafeCast.toUint128(msg.value) -
providerInfo.feeInWei);
// 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.
req = allocRequest(provider, assignedSequenceNumber);
req.provider = provider;
req.sequenceNumber = assignedSequenceNumber;
req.numHashes = SafeCast.toUint32(
assignedSequenceNumber -
providerInfo.currentCommitmentSequenceNumber
);
req.commitment = keccak256(
bytes.concat(userCommitment, providerInfo.currentCommitment)
);
req.requester = msg.sender;
req.blockNumber = SafeCast.toUint64(block.number);
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);
}
// Request a random number. The method expects the provider address and a secret random number
// in the arguments. It returns a sequence number.
//
// 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 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,
bytes32 userRandomNumber
) public payable override returns (uint64) {
EntropyStructs.Request storage req = requestHelper(
provider,
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
);
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(userRevelation);
if (
keccak256(bytes.concat(userCommitment, providerCommitment)) !=
req.commitment
) revert EntropyErrors.IncorrectRevelation();
blockHash = bytes32(uint256(0));
if (req.useBlockhash) {
bytes32 _blockHash = blockhash(req.blockNumber);
// The `blockhash` function will return zero if the req.blockNumber is equal to the current
// block number, or if it is not within the 256 most recent blocks. This allows the user to
// select between two random numbers by executing the reveal function in the same block as the
// request, or after 256 blocks. This gives each user two chances to get a favorable result on
// each request.
// Revert this transaction for when the blockHash is 0;
if (_blockHash == bytes32(uint256(0)))
revert EntropyErrors.BlockhashUnavailable();
blockHash = _blockHash;
}
randomNumber = combineRandomValues(
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,
userRevelation,
providerRevelation,
blockHash,
randomNumber
);
clearRequest(provider, sequenceNumber);
}
// Fulfill a request for a random number. This method validates the provided userRandomness
// and provider's revelation against the corresponding commitment in the in-flight request. If both values are validated
// and the requestor address is a contract address, this function calls the requester's entropyCallback method with the
// sequence number, provider address and the random number as arguments. Else if the requestor is an EOA, it won't call it.
//
// 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);
// Check if the callAddress is a contract account.
uint len;
assembly {
len := extcodesize(callAddress)
}
if (len != 0) {
IEntropyConsumer(callAddress)._entropyCallback(
sequenceNumber,
provider,
randomNumber
);
}
}
function getProviderInfo(
address provider
) public view override returns (EntropyStructs.ProviderInfo memory info) {
info = _state.providers[provider];
}
function getDefaultProvider()
public
view
override
returns (address provider)
{
provider = _state.defaultProvider;
}
function getRequest(
address provider,
uint64 sequenceNumber
) public view override returns (EntropyStructs.Request memory req) {
req = findRequest(provider, sequenceNumber);
}
function getFee(
address provider
) public view override returns (uint128 feeAmount) {
return _state.providers[provider].feeInWei + _state.pythFeeInWei;
}
function getPythFee() public view returns (uint128 feeAmount) {
return _state.pythFeeInWei;
}
function getAccruedPythFees()
public
view
override
returns (uint128 accruedPythFeesInWei)
{
return _state.accruedPythFeesInWei;
}
// Set provider fee. It will revert if provider is not registered.
function setProviderFee(uint128 newFeeInWei) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
uint128 oldFeeInWei = provider.feeInWei;
provider.feeInWei = newFeeInWei;
emit ProviderFeeUpdated(msg.sender, oldFeeInWei, newFeeInWei);
}
// Set provider uri. It will revert if provider is not registered.
function setProviderUri(bytes calldata newUri) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
bytes memory oldUri = provider.uri;
provider.uri = newUri;
emit ProviderUriUpdated(msg.sender, oldUri, newUri);
}
function constructUserCommitment(
bytes32 userRandomness
) public pure override returns (bytes32 userCommitment) {
userCommitment = keccak256(bytes.concat(userRandomness));
}
function combineRandomValues(
bytes32 userRandomness,
bytes32 providerRandomness,
bytes32 blockHash
) public pure override returns (bytes32 combinedRandomness) {
combinedRandomness = keccak256(
abi.encodePacked(userRandomness, providerRandomness, blockHash)
);
}
// 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(
address provider,
uint64 sequenceNumber
) internal pure returns (bytes32 hash, uint8 shortHash) {
hash = keccak256(abi.encodePacked(provider, sequenceNumber));
shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
}
// 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
) internal pure returns (bytes32 currentHash) {
currentHash = revelation;
while (numHashes > 0) {
currentHash = keccak256(bytes.concat(currentHash));
numHashes -= 1;
}
}
// 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).
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;
}
}
// 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;
}
}