Initial version of random numbers contract (#1093)
* initial merkle tree cut * initial working test * grrr * add this deploy script * contract stuff * doc * cleanup * cleanup * delete janky deploy script * first commit of random2 * randomness * format * wtf * wtf * stuff * cleanup * minor * add extra field * pr comments
This commit is contained in:
parent
4bc11b863c
commit
727f9ec33d
|
@ -0,0 +1,326 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./PythRandomState.sol";
|
||||
import "./PythRandomErrors.sol";
|
||||
import "./PythRandomEvents.sol";
|
||||
|
||||
// PythRandom 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.
|
||||
//
|
||||
// PythRandom 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.
|
||||
// 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 request transaction,
|
||||
// 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 PythRandom 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 PythRandom 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 are continue to use the pre-rotation commitment.
|
||||
// Each commitment has a metadata field that providers can use to determine which commitment a request is for.
|
||||
// Providers *must* retrieve the metadata for a request from the blockchain itself to prevent user manipulation of this field.
|
||||
//
|
||||
// Warning to integrators:
|
||||
// An important caveat for users 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.
|
||||
//
|
||||
// TODOs:
|
||||
// - governance??
|
||||
// - correct method access modifiers (public vs external)
|
||||
// - gas optimizations
|
||||
// - function to check invariants??
|
||||
// - need to increment pyth fees if someone transfers funds to the contract via another method
|
||||
contract PythRandom is PythRandomState, PythRandomEvents {
|
||||
// TODO: Use an upgradeable proxy
|
||||
constructor(uint pythFeeInWei) {
|
||||
_state.accruedPythFeesInWei = 0;
|
||||
_state.pythFeeInWei = pythFeeInWei;
|
||||
}
|
||||
|
||||
// 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(
|
||||
uint feeInWei,
|
||||
bytes32 commitment,
|
||||
bytes32 commitmentMetadata,
|
||||
uint64 chainLength
|
||||
) public {
|
||||
if (chainLength == 0) revert PythRandomErrors.AssertionFailure();
|
||||
|
||||
PythRandomStructs.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.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(uint256 amount) public {
|
||||
PythRandomStructs.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");
|
||||
}
|
||||
|
||||
// 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 returns (uint64 assignedSequenceNumber) {
|
||||
PythRandomStructs.ProviderInfo storage providerInfo = _state.providers[
|
||||
provider
|
||||
];
|
||||
if (_state.providers[provider].sequenceNumber == 0)
|
||||
revert PythRandomErrors.NoSuchProvider();
|
||||
|
||||
// Assign a sequence number to the request
|
||||
assignedSequenceNumber = providerInfo.sequenceNumber;
|
||||
if (assignedSequenceNumber >= providerInfo.endSequenceNumber)
|
||||
revert PythRandomErrors.OutOfRandomness();
|
||||
providerInfo.sequenceNumber += 1;
|
||||
|
||||
// Check that fees were paid and increment the pyth / provider balances.
|
||||
uint requiredFee = getFee(provider);
|
||||
if (msg.value < requiredFee) revert PythRandomErrors.InsufficientFee();
|
||||
providerInfo.accruedFeesInWei += providerInfo.feeInWei;
|
||||
_state.accruedPythFeesInWei += (msg.value - providerInfo.feeInWei);
|
||||
|
||||
// Store the user's commitment so that we can fulfill the request later.
|
||||
PythRandomStructs.Request storage req = _state.requests[
|
||||
requestKey(provider, assignedSequenceNumber)
|
||||
];
|
||||
req.provider = provider;
|
||||
req.sequenceNumber = assignedSequenceNumber;
|
||||
req.userCommitment = userCommitment;
|
||||
req.providerCommitment = providerInfo.currentCommitment;
|
||||
req.providerCommitmentSequenceNumber = providerInfo
|
||||
.currentCommitmentSequenceNumber;
|
||||
req.providerCommitmentMetadata = providerInfo.commitmentMetadata;
|
||||
|
||||
if (useBlockHash) {
|
||||
req.blockNumber = block.number;
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
function reveal(
|
||||
address provider,
|
||||
uint64 sequenceNumber,
|
||||
bytes32 userRandomness,
|
||||
bytes32 providerRevelation
|
||||
) public returns (bytes32 randomNumber) {
|
||||
// TODO: do we need to check that this request exists?
|
||||
// TODO: this method may need to be authenticated to prevent griefing
|
||||
bytes32 key = requestKey(provider, sequenceNumber);
|
||||
PythRandomStructs.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 PythRandomErrors.AssertionFailure();
|
||||
|
||||
bool valid = isProofValid(
|
||||
req.providerCommitmentSequenceNumber,
|
||||
req.providerCommitment,
|
||||
sequenceNumber,
|
||||
providerRevelation
|
||||
);
|
||||
if (!valid) revert PythRandomErrors.IncorrectProviderRevelation();
|
||||
if (constructUserCommitment(userRandomness) != req.userCommitment)
|
||||
revert PythRandomErrors.IncorrectUserRevelation();
|
||||
|
||||
bytes32 blockHash = bytes32(uint256(0));
|
||||
if (req.blockNumber != 0) {
|
||||
blockHash = blockhash(req.blockNumber);
|
||||
}
|
||||
|
||||
randomNumber = combineRandomValues(
|
||||
userRandomness,
|
||||
providerRevelation,
|
||||
blockHash
|
||||
);
|
||||
|
||||
emit Revealed(
|
||||
req,
|
||||
userRandomness,
|
||||
providerRevelation,
|
||||
blockHash,
|
||||
randomNumber
|
||||
);
|
||||
|
||||
delete _state.requests[key];
|
||||
|
||||
PythRandomStructs.ProviderInfo storage providerInfo = _state.providers[
|
||||
provider
|
||||
];
|
||||
if (providerInfo.currentCommitmentSequenceNumber < sequenceNumber) {
|
||||
providerInfo.currentCommitmentSequenceNumber = sequenceNumber;
|
||||
providerInfo.currentCommitment = providerRevelation;
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderInfo(
|
||||
address provider
|
||||
) public view returns (PythRandomStructs.ProviderInfo memory info) {
|
||||
info = _state.providers[provider];
|
||||
}
|
||||
|
||||
function getRequest(
|
||||
address provider,
|
||||
uint64 sequenceNumber
|
||||
) public view returns (PythRandomStructs.Request memory req) {
|
||||
bytes32 key = requestKey(provider, sequenceNumber);
|
||||
req = _state.requests[key];
|
||||
}
|
||||
|
||||
function getFee(address provider) public view returns (uint feeAmount) {
|
||||
return _state.providers[provider].feeInWei + _state.pythFeeInWei;
|
||||
}
|
||||
|
||||
function getAccruedPythFees()
|
||||
public
|
||||
view
|
||||
returns (uint accruedPythFeesInWei)
|
||||
{
|
||||
return _state.accruedPythFeesInWei;
|
||||
}
|
||||
|
||||
function constructUserCommitment(
|
||||
bytes32 userRandomness
|
||||
) public pure returns (bytes32 userCommitment) {
|
||||
userCommitment = keccak256(bytes.concat(userRandomness));
|
||||
}
|
||||
|
||||
function combineRandomValues(
|
||||
bytes32 userRandomness,
|
||||
bytes32 providerRandomness,
|
||||
bytes32 blockHash
|
||||
) public pure returns (bytes32 combinedRandomness) {
|
||||
combinedRandomness = keccak256(
|
||||
abi.encodePacked(userRandomness, providerRandomness, blockHash)
|
||||
);
|
||||
}
|
||||
|
||||
// Create a unique key for an in-flight randomness request (to store it in the contract state)
|
||||
function requestKey(
|
||||
address provider,
|
||||
uint64 sequenceNumber
|
||||
) internal pure returns (bytes32 hash) {
|
||||
hash = keccak256(abi.encodePacked(provider, sequenceNumber));
|
||||
}
|
||||
|
||||
// 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,
|
||||
bytes32 revelation
|
||||
) internal pure returns (bool valid) {
|
||||
if (sequenceNumber <= lastSequenceNumber)
|
||||
revert PythRandomErrors.AssertionFailure();
|
||||
|
||||
bytes32 currentHash = revelation;
|
||||
while (sequenceNumber > lastSequenceNumber) {
|
||||
currentHash = keccak256(bytes.concat(currentHash));
|
||||
sequenceNumber -= 1;
|
||||
}
|
||||
|
||||
valid = currentHash == lastRevelation;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
library PythRandomErrors {
|
||||
// An invariant of the contract failed to hold. This error indicates a software logic bug.
|
||||
error AssertionFailure();
|
||||
// The provider being registered has already registered
|
||||
// Signature: TODO
|
||||
error ProviderAlreadyRegistered();
|
||||
// The requested provider does not exist.
|
||||
error NoSuchProvider();
|
||||
// The randomness provider is out of commited random numbers. The provider needs to
|
||||
// rotate their on-chain commitment to resolve this error.
|
||||
error OutOfRandomness();
|
||||
// The transaction fee was not sufficient
|
||||
error InsufficientFee();
|
||||
// The user's revealed random value did not match their commitment.
|
||||
error IncorrectUserRevelation();
|
||||
// The provider's revealed random value did not match their commitment.
|
||||
error IncorrectProviderRevelation();
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./PythRandomState.sol";
|
||||
|
||||
interface PythRandomEvents {
|
||||
event Registered(PythRandomStructs.ProviderInfo provider);
|
||||
|
||||
event Requested(PythRandomStructs.Request request);
|
||||
|
||||
event Revealed(
|
||||
PythRandomStructs.Request request,
|
||||
bytes32 userRevelation,
|
||||
bytes32 providerRevelation,
|
||||
bytes32 blockHash,
|
||||
bytes32 randomNumber
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// contracts/State.sol
|
||||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
contract PythRandomStructs {
|
||||
struct State {
|
||||
uint pythFeeInWei;
|
||||
uint accruedPythFeesInWei;
|
||||
mapping(address => ProviderInfo) providers;
|
||||
mapping(bytes32 => Request) requests;
|
||||
}
|
||||
|
||||
struct ProviderInfo {
|
||||
uint feeInWei;
|
||||
uint accruedFeesInWei;
|
||||
// The commitment that the provider posted to the blockchain, and the sequence number
|
||||
// where they committed to this. This value is not advanced after the provider commits,
|
||||
// and instead is stored to help providers track where they are in the hash chain.
|
||||
bytes32 originalCommitment;
|
||||
uint64 originalCommitmentSequenceNumber;
|
||||
// Metadata for the current commitment. Providers may optionally use this field to to help
|
||||
// manage rotations (i.e., to pick the sequence number from the correct hash chain).
|
||||
bytes32 commitmentMetadata;
|
||||
// The first sequence number that is *not* included in the current commitment (i.e., an exclusive end index).
|
||||
// The contract maintains the invariant that sequenceNumber <= endSequenceNumber.
|
||||
// If sequenceNumber == endSequenceNumber, the provider must rotate their commitment to add additional random values.
|
||||
uint64 endSequenceNumber;
|
||||
// The sequence number that will be assigned to the next inbound user request.
|
||||
uint64 sequenceNumber;
|
||||
// The current commitment represents an index/value in the provider's hash chain.
|
||||
// These values are used to verify requests for future sequence numbers. Note that
|
||||
// currentCommitmentSequenceNumber < sequenceNumber.
|
||||
//
|
||||
// The currentCommitment advances forward through the provider's hash chain as values
|
||||
// are revealed on-chain.
|
||||
bytes32 currentCommitment;
|
||||
uint64 currentCommitmentSequenceNumber;
|
||||
}
|
||||
|
||||
struct Request {
|
||||
address provider;
|
||||
uint64 sequenceNumber;
|
||||
bytes32 userCommitment;
|
||||
bytes32 providerCommitment;
|
||||
uint64 providerCommitmentSequenceNumber;
|
||||
bytes32 providerCommitmentMetadata;
|
||||
// If nonzero, the randomness requester wants the blockhash of this block to be incorporated into the random number.
|
||||
uint256 blockNumber;
|
||||
}
|
||||
}
|
||||
|
||||
contract PythRandomState {
|
||||
PythRandomStructs.State _state;
|
||||
}
|
|
@ -0,0 +1,496 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
|
||||
import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
|
||||
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
|
||||
import "./utils/WormholeTestUtils.t.sol";
|
||||
import "./utils/PythTestUtils.t.sol";
|
||||
import "./utils/RandTestUtils.t.sol";
|
||||
import "../contracts/random/PythRandom.sol";
|
||||
|
||||
// TODO
|
||||
// - what's the impact of # of in-flight requests on gas usage? More requests => more hashes to
|
||||
// verify the provider's value.
|
||||
// - fuzz test?
|
||||
contract PythRandomTest is Test, RandTestUtils {
|
||||
PythRandom public random;
|
||||
|
||||
uint pythFeeInWei = 7;
|
||||
|
||||
address public provider1 = address(1);
|
||||
bytes32[] provider1Proofs;
|
||||
uint provider1FeeInWei = 8;
|
||||
uint64 provider1ChainLength = 100;
|
||||
|
||||
address public provider2 = address(2);
|
||||
bytes32[] provider2Proofs;
|
||||
uint provider2FeeInWei = 20;
|
||||
|
||||
address public user1 = address(3);
|
||||
address public user2 = address(4);
|
||||
|
||||
address public unregisteredProvider = address(7);
|
||||
uint256 MAX_UINT256 = 2 ** 256 - 1;
|
||||
bytes32 ALL_ZEROS = bytes32(uint256(0));
|
||||
|
||||
function setUp() public {
|
||||
random = new PythRandom(pythFeeInWei);
|
||||
|
||||
bytes32[] memory hashChain1 = generateHashChain(
|
||||
provider1,
|
||||
0,
|
||||
provider1ChainLength
|
||||
);
|
||||
provider1Proofs = hashChain1;
|
||||
vm.prank(provider1);
|
||||
random.register(
|
||||
provider1FeeInWei,
|
||||
provider1Proofs[0],
|
||||
bytes32(keccak256(abi.encodePacked(uint256(0x0100)))),
|
||||
provider1ChainLength
|
||||
);
|
||||
|
||||
bytes32[] memory hashChain2 = generateHashChain(provider2, 0, 100);
|
||||
provider2Proofs = hashChain2;
|
||||
vm.prank(provider2);
|
||||
random.register(
|
||||
provider2FeeInWei,
|
||||
provider2Proofs[0],
|
||||
bytes32(keccak256(abi.encodePacked(uint256(0x0200)))),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
function generateHashChain(
|
||||
address provider,
|
||||
uint64 startSequenceNumber,
|
||||
uint64 size
|
||||
) public pure returns (bytes32[] memory hashChain) {
|
||||
bytes32 initialValue = keccak256(abi.encodePacked(startSequenceNumber));
|
||||
hashChain = new bytes32[](size);
|
||||
for (uint64 i = 0; i < size; i++) {
|
||||
hashChain[size - (i + 1)] = initialValue;
|
||||
initialValue = keccak256(bytes.concat(initialValue));
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper method for requesting a random value as user from provider.
|
||||
function request(
|
||||
address user,
|
||||
address provider,
|
||||
uint randomNumber,
|
||||
bool useBlockhash
|
||||
) public returns (uint64 sequenceNumber) {
|
||||
sequenceNumber = requestWithFee(
|
||||
user,
|
||||
random.getFee(provider),
|
||||
provider,
|
||||
randomNumber,
|
||||
useBlockhash
|
||||
);
|
||||
}
|
||||
|
||||
function requestWithFee(
|
||||
address user,
|
||||
uint fee,
|
||||
address provider,
|
||||
uint randomNumber,
|
||||
bool useBlockhash
|
||||
) public returns (uint64 sequenceNumber) {
|
||||
vm.deal(user, fee);
|
||||
vm.prank(user);
|
||||
sequenceNumber = random.request{value: fee}(
|
||||
provider,
|
||||
random.constructUserCommitment(bytes32(randomNumber)),
|
||||
useBlockhash
|
||||
);
|
||||
}
|
||||
|
||||
function assertRequestReverts(
|
||||
uint fee,
|
||||
address provider,
|
||||
uint randomNumber,
|
||||
bool useBlockhash
|
||||
) public {
|
||||
// Note: for some reason vm.expectRevert() won't catch errors from the request function (?!),
|
||||
// even though they definitely revert. Use a try/catch instead for the moment, though the try/catch
|
||||
// doesn't let you simulate the msg.sender. However, it's fine if the msg.sender is the test contract.
|
||||
bool requestSucceeds = false;
|
||||
try
|
||||
random.request{value: fee}(
|
||||
provider,
|
||||
random.constructUserCommitment(bytes32(uint256(randomNumber))),
|
||||
useBlockhash
|
||||
)
|
||||
{
|
||||
requestSucceeds = true;
|
||||
} catch {
|
||||
requestSucceeds = false;
|
||||
}
|
||||
|
||||
assert(!requestSucceeds);
|
||||
}
|
||||
|
||||
function assertRevealSucceeds(
|
||||
address provider,
|
||||
uint64 sequenceNumber,
|
||||
uint userRandom,
|
||||
bytes32 providerRevelation,
|
||||
bytes32 hash
|
||||
) public {
|
||||
bytes32 randomNumber = random.reveal(
|
||||
provider,
|
||||
sequenceNumber,
|
||||
bytes32(userRandom),
|
||||
providerRevelation
|
||||
);
|
||||
assertEq(
|
||||
randomNumber,
|
||||
random.combineRandomValues(
|
||||
bytes32(userRandom),
|
||||
providerRevelation,
|
||||
hash
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function assertRevealReverts(
|
||||
address provider,
|
||||
uint64 sequenceNumber,
|
||||
uint userRandom,
|
||||
bytes32 providerRevelation
|
||||
) public {
|
||||
vm.expectRevert();
|
||||
random.reveal(
|
||||
provider,
|
||||
sequenceNumber,
|
||||
bytes32(uint256(userRandom)),
|
||||
providerRevelation
|
||||
);
|
||||
}
|
||||
|
||||
function assertInvariants() public {
|
||||
uint expectedBalance = random
|
||||
.getProviderInfo(provider1)
|
||||
.accruedFeesInWei +
|
||||
random.getProviderInfo(provider2).accruedFeesInWei +
|
||||
random.getAccruedPythFees();
|
||||
assertEq(address(random).balance, expectedBalance);
|
||||
|
||||
PythRandomStructs.ProviderInfo memory info1 = random.getProviderInfo(
|
||||
provider1
|
||||
);
|
||||
assert(
|
||||
info1.originalCommitmentSequenceNumber <=
|
||||
info1.currentCommitmentSequenceNumber
|
||||
);
|
||||
assert(info1.currentCommitmentSequenceNumber < info1.sequenceNumber);
|
||||
assert(info1.sequenceNumber <= info1.endSequenceNumber);
|
||||
PythRandomStructs.ProviderInfo memory info2 = random.getProviderInfo(
|
||||
provider2
|
||||
);
|
||||
assert(
|
||||
info2.originalCommitmentSequenceNumber <=
|
||||
info2.currentCommitmentSequenceNumber
|
||||
);
|
||||
assert(info2.sequenceNumber > info2.currentCommitmentSequenceNumber);
|
||||
assert(info2.sequenceNumber <= info2.endSequenceNumber);
|
||||
}
|
||||
|
||||
function testBasicFlow() public {
|
||||
uint64 sequenceNumber = request(user2, provider1, 42, false);
|
||||
assertEq(random.getRequest(provider1, sequenceNumber).blockNumber, 0);
|
||||
|
||||
assertRevealSucceeds(
|
||||
provider1,
|
||||
sequenceNumber,
|
||||
42,
|
||||
provider1Proofs[sequenceNumber],
|
||||
ALL_ZEROS
|
||||
);
|
||||
|
||||
// You can only reveal the random number once. This isn't a feature of the contract per se, but it is
|
||||
// the expected behavior.
|
||||
assertRevealReverts(
|
||||
provider1,
|
||||
sequenceNumber,
|
||||
42,
|
||||
provider1Proofs[sequenceNumber]
|
||||
);
|
||||
}
|
||||
|
||||
function testNoSuchProvider() public {
|
||||
assertRequestReverts(10000000, unregisteredProvider, 42, false);
|
||||
}
|
||||
|
||||
function testAdversarialReveal() public {
|
||||
uint64 sequenceNumber = request(user2, provider1, 42, false);
|
||||
|
||||
// test revealing with the wrong hashes in the same chain
|
||||
for (uint256 i = 0; i < 10; i++) {
|
||||
if (i != sequenceNumber) {
|
||||
assertRevealReverts(
|
||||
provider1,
|
||||
sequenceNumber,
|
||||
42,
|
||||
provider1Proofs[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// test revealing with the wrong user revealed value.
|
||||
for (uint256 i = 0; i < 42; i++) {
|
||||
assertRevealReverts(
|
||||
provider1,
|
||||
sequenceNumber,
|
||||
i,
|
||||
provider1Proofs[sequenceNumber]
|
||||
);
|
||||
}
|
||||
|
||||
// test revealing sequence numbers that haven't been requested yet.
|
||||
for (uint64 i = sequenceNumber + 1; i < sequenceNumber + 3; i++) {
|
||||
assertRevealReverts(
|
||||
provider1,
|
||||
i,
|
||||
42,
|
||||
provider1Proofs[sequenceNumber]
|
||||
);
|
||||
|
||||
assertRevealReverts(provider1, i, 42, provider1Proofs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function testConcurrentRequests() public {
|
||||
uint64 s1 = request(user1, provider1, 1, false);
|
||||
uint64 s2 = request(user2, provider1, 2, false);
|
||||
uint64 s3 = request(user1, provider1, 3, false);
|
||||
uint64 s4 = request(user1, provider1, 4, false);
|
||||
|
||||
assertRevealSucceeds(provider1, s3, 3, provider1Proofs[s3], ALL_ZEROS);
|
||||
assertInvariants();
|
||||
|
||||
uint64 s5 = request(user1, provider1, 5, false);
|
||||
|
||||
assertRevealSucceeds(provider1, s4, 4, provider1Proofs[s4], ALL_ZEROS);
|
||||
assertInvariants();
|
||||
|
||||
assertRevealSucceeds(provider1, s1, 1, provider1Proofs[s1], ALL_ZEROS);
|
||||
assertInvariants();
|
||||
|
||||
assertRevealSucceeds(provider1, s2, 2, provider1Proofs[s2], ALL_ZEROS);
|
||||
assertInvariants();
|
||||
|
||||
assertRevealSucceeds(provider1, s5, 5, provider1Proofs[s5], ALL_ZEROS);
|
||||
assertInvariants();
|
||||
}
|
||||
|
||||
function testBlockhash() public {
|
||||
vm.roll(1234);
|
||||
uint64 sequenceNumber = request(user2, provider1, 42, true);
|
||||
|
||||
assertEq(
|
||||
random.getRequest(provider1, sequenceNumber).blockNumber,
|
||||
1234
|
||||
);
|
||||
|
||||
assertRevealSucceeds(
|
||||
provider1,
|
||||
sequenceNumber,
|
||||
42,
|
||||
provider1Proofs[sequenceNumber],
|
||||
blockhash(1234)
|
||||
);
|
||||
|
||||
// You can only reveal the random number once. This isn't a feature of the contract per se, but it is
|
||||
// the expected behavior.
|
||||
assertRevealReverts(
|
||||
provider1,
|
||||
sequenceNumber,
|
||||
42,
|
||||
provider1Proofs[sequenceNumber]
|
||||
);
|
||||
}
|
||||
|
||||
function testProviderCommitmentRotation() public {
|
||||
uint userRandom = 42;
|
||||
uint64 sequenceNumber1 = request(user2, provider1, userRandom, false);
|
||||
uint64 sequenceNumber2 = request(user2, provider1, userRandom, false);
|
||||
assertInvariants();
|
||||
|
||||
uint64 newHashChainOffset = sequenceNumber2 + 1;
|
||||
bytes32[] memory newHashChain = generateHashChain(
|
||||
provider1,
|
||||
newHashChainOffset,
|
||||
10
|
||||
);
|
||||
vm.prank(provider1);
|
||||
random.register(
|
||||
provider1FeeInWei,
|
||||
newHashChain[0],
|
||||
bytes32(keccak256(abi.encodePacked(uint256(0x0100)))),
|
||||
10
|
||||
);
|
||||
assertInvariants();
|
||||
PythRandomStructs.ProviderInfo memory info1 = random.getProviderInfo(
|
||||
provider1
|
||||
);
|
||||
assertEq(info1.endSequenceNumber, newHashChainOffset + 10);
|
||||
|
||||
uint64 sequenceNumber3 = request(user2, provider1, 42, false);
|
||||
// Rotating the provider key uses a sequence number
|
||||
assertEq(sequenceNumber3, sequenceNumber2 + 2);
|
||||
|
||||
// Requests that were in-flight at the time of rotation use the commitment from the time of request
|
||||
for (uint256 i = 0; i < 10; i++) {
|
||||
assertRevealReverts(
|
||||
provider1,
|
||||
sequenceNumber1,
|
||||
userRandom,
|
||||
newHashChain[i]
|
||||
);
|
||||
}
|
||||
assertRevealSucceeds(
|
||||
provider1,
|
||||
sequenceNumber1,
|
||||
userRandom,
|
||||
provider1Proofs[sequenceNumber1],
|
||||
ALL_ZEROS
|
||||
);
|
||||
assertInvariants();
|
||||
|
||||
// Requests after the rotation use the new commitment
|
||||
assertRevealReverts(
|
||||
provider1,
|
||||
sequenceNumber3,
|
||||
userRandom,
|
||||
provider1Proofs[sequenceNumber3]
|
||||
);
|
||||
assertRevealSucceeds(
|
||||
provider1,
|
||||
sequenceNumber3,
|
||||
userRandom,
|
||||
newHashChain[sequenceNumber3 - newHashChainOffset],
|
||||
ALL_ZEROS
|
||||
);
|
||||
assertInvariants();
|
||||
}
|
||||
|
||||
function testOutOfRandomness() public {
|
||||
// Should be able to request chainLength - 1 random numbers successfully.
|
||||
for (uint64 i = 0; i < provider1ChainLength - 1; i++) {
|
||||
request(user1, provider1, i, false);
|
||||
}
|
||||
|
||||
assertRequestReverts(
|
||||
random.getFee(provider1),
|
||||
provider1,
|
||||
provider1ChainLength - 1,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function testGetFee() public {
|
||||
assertEq(random.getFee(provider1), pythFeeInWei + provider1FeeInWei);
|
||||
assertEq(random.getFee(provider2), pythFeeInWei + provider2FeeInWei);
|
||||
// Requesting the fee for a nonexistent provider returns pythFeeInWei. This isn't necessarily desirable behavior,
|
||||
// but it's unlikely to cause a problem.
|
||||
assertEq(random.getFee(unregisteredProvider), pythFeeInWei);
|
||||
|
||||
// Check that overflowing the fee arithmetic causes the transaction to revert.
|
||||
vm.prank(provider1);
|
||||
random.register(
|
||||
MAX_UINT256,
|
||||
provider1Proofs[0],
|
||||
bytes32(keccak256(abi.encodePacked(uint256(0x0100)))),
|
||||
100
|
||||
);
|
||||
vm.expectRevert();
|
||||
random.getFee(provider1);
|
||||
}
|
||||
|
||||
function testFees() public {
|
||||
// Insufficient fees causes a revert
|
||||
assertRequestReverts(0, provider1, 42, false);
|
||||
assertRequestReverts(
|
||||
pythFeeInWei + provider1FeeInWei - 1,
|
||||
provider1,
|
||||
42,
|
||||
false
|
||||
);
|
||||
assertRequestReverts(0, provider2, 42, false);
|
||||
assertRequestReverts(
|
||||
pythFeeInWei + provider2FeeInWei - 1,
|
||||
provider2,
|
||||
42,
|
||||
false
|
||||
);
|
||||
|
||||
// Accrue some fees for both providers
|
||||
for (uint i = 0; i < 3; i++) {
|
||||
request(user2, provider1, 42, false);
|
||||
}
|
||||
|
||||
request(user2, provider2, 42, false);
|
||||
// this call overpays for the random number
|
||||
requestWithFee(
|
||||
user2,
|
||||
pythFeeInWei + provider2FeeInWei + 10000,
|
||||
provider2,
|
||||
42,
|
||||
false
|
||||
);
|
||||
|
||||
assertEq(
|
||||
random.getProviderInfo(provider1).accruedFeesInWei,
|
||||
provider1FeeInWei * 3
|
||||
);
|
||||
assertEq(
|
||||
random.getProviderInfo(provider2).accruedFeesInWei,
|
||||
provider2FeeInWei * 2
|
||||
);
|
||||
assertEq(random.getAccruedPythFees(), pythFeeInWei * 5 + 10000);
|
||||
assertInvariants();
|
||||
|
||||
// Reregistering updates the required fees
|
||||
vm.prank(provider1);
|
||||
random.register(
|
||||
12345,
|
||||
provider1Proofs[0],
|
||||
bytes32(keccak256(abi.encodePacked(uint256(0x0100)))),
|
||||
100
|
||||
);
|
||||
|
||||
assertRequestReverts(pythFeeInWei + 12345 - 1, provider1, 42, false);
|
||||
requestWithFee(user2, pythFeeInWei + 12345, provider1, 42, false);
|
||||
|
||||
uint providerOneBalance = provider1FeeInWei * 3 + 12345;
|
||||
assertEq(
|
||||
random.getProviderInfo(provider1).accruedFeesInWei,
|
||||
providerOneBalance
|
||||
);
|
||||
assertInvariants();
|
||||
|
||||
vm.prank(unregisteredProvider);
|
||||
vm.expectRevert();
|
||||
random.withdraw(1000);
|
||||
|
||||
vm.prank(provider1);
|
||||
random.withdraw(1000);
|
||||
|
||||
assertEq(
|
||||
random.getProviderInfo(provider1).accruedFeesInWei,
|
||||
providerOneBalance - 1000
|
||||
);
|
||||
assertInvariants();
|
||||
|
||||
vm.prank(provider1);
|
||||
vm.expectRevert();
|
||||
random.withdraw(providerOneBalance);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue