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:
Jayant Krishnamurthy 2023-10-17 13:07:47 -07:00 committed by GitHub
parent 4bc11b863c
commit 727f9ec33d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 917 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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
);
}

View File

@ -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;
}

View File

@ -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);
}
}