From 6a2a9a5bde7a8fa391eb446becced97e8d87f3c8 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 31 Oct 2023 07:29:17 -0700 Subject: [PATCH 1/3] rename stuff --- .../contracts/contracts/random/PythRandom.sol | 336 ------------ .../contracts/forge-test/PythRandom.t.sol | 496 ------------------ .../entropy_sdk/solidity/IEntropy.sol | 8 +- .../entropy_sdk/solidity/PythRandomErrors.sol | 22 - .../entropy_sdk/solidity/PythRandomEvents.sol | 18 - .../entropy_sdk/solidity/PythRandomState.sol | 55 -- 6 files changed, 4 insertions(+), 931 deletions(-) delete mode 100644 target_chains/ethereum/contracts/contracts/random/PythRandom.sol delete mode 100644 target_chains/ethereum/contracts/forge-test/PythRandom.t.sol delete mode 100644 target_chains/ethereum/entropy_sdk/solidity/PythRandomErrors.sol delete mode 100644 target_chains/ethereum/entropy_sdk/solidity/PythRandomEvents.sol delete mode 100644 target_chains/ethereum/entropy_sdk/solidity/PythRandomState.sol diff --git a/target_chains/ethereum/contracts/contracts/random/PythRandom.sol b/target_chains/ethereum/contracts/contracts/random/PythRandom.sol deleted file mode 100644 index 16d0b401f..000000000 --- a/target_chains/ethereum/contracts/contracts/random/PythRandom.sol +++ /dev/null @@ -1,336 +0,0 @@ -// SPDX-License-Identifier: Apache 2 - -pragma solidity ^0.8.0; - -import "@pythnetwork/entropy-sdk-solidity/PythRandomState.sol"; -import "@pythnetwork/entropy-sdk-solidity/PythRandomErrors.sol"; -import "@pythnetwork/entropy-sdk-solidity/PythRandomEvents.sol"; -import "@pythnetwork/entropy-sdk-solidity/IEntropy.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 -// - off-chain data ERC support? -contract PythRandom is IEntropy, PythRandomState { - // 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 override { - 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 override { - 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 override 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 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 - 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 - override - returns (PythRandomStructs.ProviderInfo memory info) - { - info = _state.providers[provider]; - } - - function getRequest( - address provider, - uint64 sequenceNumber - ) public view override returns (PythRandomStructs.Request memory req) { - bytes32 key = requestKey(provider, sequenceNumber); - req = _state.requests[key]; - } - - function getFee( - address provider - ) public view override returns (uint feeAmount) { - return _state.providers[provider].feeInWei + _state.pythFeeInWei; - } - - function getAccruedPythFees() - public - view - override - returns (uint accruedPythFeesInWei) - { - return _state.accruedPythFeesInWei; - } - - 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 (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; - } -} diff --git a/target_chains/ethereum/contracts/forge-test/PythRandom.t.sol b/target_chains/ethereum/contracts/forge-test/PythRandom.t.sol deleted file mode 100644 index c8e7c513c..000000000 --- a/target_chains/ethereum/contracts/forge-test/PythRandom.t.sol +++ /dev/null @@ -1,496 +0,0 @@ -// 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); - } -} diff --git a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol b/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol index e121de3f8..d1088006b 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.0; -import "./PythRandomEvents.sol"; +import "./EntropyEvents.sol"; -interface IEntropy is PythRandomEvents { +interface IEntropy is EntropyEvents { // 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). @@ -53,12 +53,12 @@ interface IEntropy is PythRandomEvents { function getProviderInfo( address provider - ) external view returns (PythRandomStructs.ProviderInfo memory info); + ) external view returns (EntropyStructs.ProviderInfo memory info); function getRequest( address provider, uint64 sequenceNumber - ) external view returns (PythRandomStructs.Request memory req); + ) external view returns (EntropyStructs.Request memory req); function getFee(address provider) external view returns (uint feeAmount); diff --git a/target_chains/ethereum/entropy_sdk/solidity/PythRandomErrors.sol b/target_chains/ethereum/entropy_sdk/solidity/PythRandomErrors.sol deleted file mode 100644 index f1f8dc213..000000000 --- a/target_chains/ethereum/entropy_sdk/solidity/PythRandomErrors.sol +++ /dev/null @@ -1,22 +0,0 @@ -// 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(); -} diff --git a/target_chains/ethereum/entropy_sdk/solidity/PythRandomEvents.sol b/target_chains/ethereum/entropy_sdk/solidity/PythRandomEvents.sol deleted file mode 100644 index fe2bb4c6a..000000000 --- a/target_chains/ethereum/entropy_sdk/solidity/PythRandomEvents.sol +++ /dev/null @@ -1,18 +0,0 @@ -// 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 - ); -} diff --git a/target_chains/ethereum/entropy_sdk/solidity/PythRandomState.sol b/target_chains/ethereum/entropy_sdk/solidity/PythRandomState.sol deleted file mode 100644 index 0ff407b4c..000000000 --- a/target_chains/ethereum/entropy_sdk/solidity/PythRandomState.sol +++ /dev/null @@ -1,55 +0,0 @@ -// 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; -} From 5e752774756133810df9516f1a94d8690bb1de5a Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 31 Oct 2023 07:29:41 -0700 Subject: [PATCH 2/3] fix --- .../contracts/contracts/entropy/Entropy.sol | 332 ++++++++++++ .../contracts/entropy/EntropyState.sol | 18 + .../contracts/forge-test/Entropy.t.sol | 489 ++++++++++++++++++ .../entropy_sdk/solidity/EntropyErrors.sol | 22 + .../entropy_sdk/solidity/EntropyEvents.sol | 18 + .../entropy_sdk/solidity/EntropyStructs.sol | 50 ++ .../ethereum/entropy_sdk/solidity/abi.json | 0 7 files changed, 929 insertions(+) create mode 100644 target_chains/ethereum/contracts/contracts/entropy/Entropy.sol create mode 100644 target_chains/ethereum/contracts/contracts/entropy/EntropyState.sol create mode 100644 target_chains/ethereum/contracts/forge-test/Entropy.t.sol create mode 100644 target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol create mode 100644 target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol create mode 100644 target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol create mode 100644 target_chains/ethereum/entropy_sdk/solidity/abi.json diff --git a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol new file mode 100644 index 000000000..0da17f84a --- /dev/null +++ b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol @@ -0,0 +1,332 @@ +// 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 "./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. +// 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 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 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 +// - off-chain data ERC support? +contract Entropy is IEntropy, EntropyState { + // 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 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.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 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"); + } + + // 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.ProviderInfo storage providerInfo = _state.providers[ + provider + ]; + if (_state.providers[provider].sequenceNumber == 0) + revert EntropyErrors.NoSuchProvider(); + + // Assign a sequence number to the request + assignedSequenceNumber = providerInfo.sequenceNumber; + if (assignedSequenceNumber >= providerInfo.endSequenceNumber) + revert EntropyErrors.OutOfRandomness(); + providerInfo.sequenceNumber += 1; + + // Check that fees were paid and increment the pyth / provider balances. + uint requiredFee = getFee(provider); + if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee(); + providerInfo.accruedFeesInWei += providerInfo.feeInWei; + _state.accruedPythFeesInWei += (msg.value - providerInfo.feeInWei); + + // Store the user's commitment so that we can fulfill the request later. + EntropyStructs.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 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 + 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, + providerRevelation + ); + if (!valid) revert EntropyErrors.IncorrectProviderRevelation(); + if (constructUserCommitment(userRandomness) != req.userCommitment) + revert EntropyErrors.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]; + + EntropyStructs.ProviderInfo storage providerInfo = _state.providers[ + provider + ]; + if (providerInfo.currentCommitmentSequenceNumber < sequenceNumber) { + providerInfo.currentCommitmentSequenceNumber = sequenceNumber; + providerInfo.currentCommitment = providerRevelation; + } + } + + function getProviderInfo( + address provider + ) public view override returns (EntropyStructs.ProviderInfo memory info) { + info = _state.providers[provider]; + } + + function getRequest( + address provider, + uint64 sequenceNumber + ) public view override returns (EntropyStructs.Request memory req) { + bytes32 key = requestKey(provider, sequenceNumber); + req = _state.requests[key]; + } + + function getFee( + address provider + ) public view override returns (uint feeAmount) { + return _state.providers[provider].feeInWei + _state.pythFeeInWei; + } + + function getAccruedPythFees() + public + view + override + returns (uint accruedPythFeesInWei) + { + return _state.accruedPythFeesInWei; + } + + 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 (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 EntropyErrors.AssertionFailure(); + + bytes32 currentHash = revelation; + while (sequenceNumber > lastSequenceNumber) { + currentHash = keccak256(bytes.concat(currentHash)); + sequenceNumber -= 1; + } + + valid = currentHash == lastRevelation; + } +} diff --git a/target_chains/ethereum/contracts/contracts/entropy/EntropyState.sol b/target_chains/ethereum/contracts/contracts/entropy/EntropyState.sol new file mode 100644 index 000000000..1044c9726 --- /dev/null +++ b/target_chains/ethereum/contracts/contracts/entropy/EntropyState.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol"; + +contract EntropyInternalStructs { + struct State { + uint pythFeeInWei; + uint accruedPythFeesInWei; + mapping(address => EntropyStructs.ProviderInfo) providers; + mapping(bytes32 => EntropyStructs.Request) requests; + } +} + +contract EntropyState { + EntropyInternalStructs.State _state; +} diff --git a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol new file mode 100644 index 000000000..e59c901cf --- /dev/null +++ b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol @@ -0,0 +1,489 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol"; +import "../contracts/entropy/Entropy.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 EntropyTest is Test { + Entropy 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 Entropy(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); + + EntropyStructs.ProviderInfo memory info1 = random.getProviderInfo( + provider1 + ); + assert( + info1.originalCommitmentSequenceNumber <= + info1.currentCommitmentSequenceNumber + ); + assert(info1.currentCommitmentSequenceNumber < info1.sequenceNumber); + assert(info1.sequenceNumber <= info1.endSequenceNumber); + EntropyStructs.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(); + EntropyStructs.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); + } +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol new file mode 100644 index 000000000..9fea3c683 --- /dev/null +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +library EntropyErrors { + // 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(); +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol new file mode 100644 index 000000000..39e6afb52 --- /dev/null +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./EntropyStructs.sol"; + +interface EntropyEvents { + event Registered(EntropyStructs.ProviderInfo provider); + + event Requested(EntropyStructs.Request request); + + event Revealed( + EntropyStructs.Request request, + bytes32 userRevelation, + bytes32 providerRevelation, + bytes32 blockHash, + bytes32 randomNumber + ); +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol new file mode 100644 index 000000000..8e4980bb8 --- /dev/null +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +contract EntropyStructs { + 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; + } +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/abi.json b/target_chains/ethereum/entropy_sdk/solidity/abi.json new file mode 100644 index 000000000..e69de29bb From bc7db008fe0fe4520a0280c6c8e70cb89561afe4 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Tue, 31 Oct 2023 07:31:42 -0700 Subject: [PATCH 3/3] fix --- target_chains/ethereum/entropy_sdk/solidity/abi.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 target_chains/ethereum/entropy_sdk/solidity/abi.json diff --git a/target_chains/ethereum/entropy_sdk/solidity/abi.json b/target_chains/ethereum/entropy_sdk/solidity/abi.json deleted file mode 100644 index e69de29bb..000000000