From e7bf47a18e2d9a9d983214342540691c1bada52e Mon Sep 17 00:00:00 2001 From: Dev Kalra Date: Tue, 12 Mar 2024 01:58:57 +0530 Subject: [PATCH] feat(entropy-v2): request with callback (#1342) * request with callback * address comments * pre-commit * compilation successful * pre-commit * add tests * generate-abis * pre-commit * correct version * address comments * pre-commit * remove unused * add comments * pre-commit * gen abi * naming consistency * remove gas limit comment * requestWithCallback comment * remove unnecessary asserts * pre commit * update request with callback coment * abis regen * refactor as per feedback * abi gen * rename * implement ientropyconsumer * gen abi * comment entropy consumer * test fix * add comment * reintroduce blockhash * add error for invalid reveal call * use getEntropy in entropy consumer * add test for requestAndRevealWithCallback * pass through for entropy consumer * pre commit fix * abi gen * address comments * address feedback * gen abis * pre commit run --- .../contracts/contracts/entropy/Entropy.sol | 269 +++++++++++++----- .../contracts/entropy/EntropyUpgradable.sol | 2 +- .../contracts/forge-test/Entropy.t.sol | 212 +++++++++++++- .../entropy_sdk/solidity/EntropyErrors.sol | 4 + .../entropy_sdk/solidity/EntropyEvents.sol | 13 + .../entropy_sdk/solidity/EntropyStructs.sol | 4 +- .../entropy_sdk/solidity/IEntropy.sol | 33 ++- .../entropy_sdk/solidity/IEntropyConsumer.sol | 28 ++ .../solidity/abis/EntropyErrors.json | 5 + .../solidity/abis/EntropyEvents.json | 162 +++++++++++ .../entropy_sdk/solidity/abis/IEntropy.json | 221 +++++++++++++- .../solidity/abis/IEntropyConsumer.json | 20 ++ .../entropy_sdk/solidity/package.json | 4 +- 13 files changed, 898 insertions(+), 79 deletions(-) create mode 100644 target_chains/ethereum/entropy_sdk/solidity/IEntropyConsumer.sol create mode 100644 target_chains/ethereum/entropy_sdk/solidity/abis/IEntropyConsumer.json diff --git a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol index b04cea29..e3df9acb 100644 --- a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol +++ b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol @@ -6,6 +6,7 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol"; import "@pythnetwork/entropy-sdk-solidity/EntropyErrors.sol"; import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol"; import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import "./EntropyState.sol"; @@ -163,6 +164,54 @@ abstract contract Entropy is IEntropy, EntropyState { require(sent, "withdrawal to msg.sender failed"); } + // requestHelper allocates and returns a new request for the given provider. + // Note: This method will revert unless the caller provides a sufficient fee + // (at least getFee(provider)) as msg.value. + function requestHelper( + address provider, + bytes32 userCommitment, + bool useBlockhash, + bool isRequestWithCallback + ) internal returns (EntropyStructs.Request storage req) { + EntropyStructs.ProviderInfo storage providerInfo = _state.providers[ + provider + ]; + if (_state.providers[provider].sequenceNumber == 0) + revert EntropyErrors.NoSuchProvider(); + + // Assign a sequence number to the request + uint64 assignedSequenceNumber = providerInfo.sequenceNumber; + if (assignedSequenceNumber >= providerInfo.endSequenceNumber) + revert EntropyErrors.OutOfRandomness(); + providerInfo.sequenceNumber += 1; + + // Check that fees were paid and increment the pyth / provider balances. + uint128 requiredFee = getFee(provider); + if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee(); + providerInfo.accruedFeesInWei += providerInfo.feeInWei; + _state.accruedPythFeesInWei += (SafeCast.toUint128(msg.value) - + providerInfo.feeInWei); + + // Store the user's commitment so that we can fulfill the request later. + // Warning: this code needs to overwrite *every* field in the request, because the returned request can be + // filled with arbitrary data. + req = allocRequest(provider, assignedSequenceNumber); + req.provider = provider; + req.sequenceNumber = assignedSequenceNumber; + req.numHashes = SafeCast.toUint32( + assignedSequenceNumber - + providerInfo.currentCommitmentSequenceNumber + ); + req.commitment = keccak256( + bytes.concat(userCommitment, providerInfo.currentCommitment) + ); + req.requester = msg.sender; + + req.blockNumber = SafeCast.toUint64(block.number); + req.useBlockhash = useBlockhash; + req.isRequestWithCallback = isRequestWithCallback; + } + // As a user, request a random number from `provider`. Prior to calling this method, the user should // generate a random number x and keep it secret. The user should then compute hash(x) and pass that // as the userCommitment argument. (You may call the constructUserCommitment method to compute the hash.) @@ -178,89 +227,68 @@ abstract contract Entropy is IEntropy, EntropyState { 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. - uint128 requiredFee = getFee(provider); - if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee(); - providerInfo.accruedFeesInWei += providerInfo.feeInWei; - _state.accruedPythFeesInWei += (SafeCast.toUint128(msg.value) - - providerInfo.feeInWei); - - // Store the user's commitment so that we can fulfill the request later. - // Warning: this code needs to overwrite *every* field in the request, because the returned request can be - // filled with arbitrary data. - EntropyStructs.Request storage req = allocRequest( + EntropyStructs.Request storage req = requestHelper( provider, - assignedSequenceNumber + userCommitment, + useBlockHash, + false ); - req.provider = provider; - req.sequenceNumber = assignedSequenceNumber; - req.numHashes = SafeCast.toUint32( - assignedSequenceNumber - - providerInfo.currentCommitmentSequenceNumber - ); - req.commitment = keccak256( - bytes.concat(userCommitment, providerInfo.currentCommitment) - ); - req.requester = msg.sender; - - req.blockNumber = SafeCast.toUint64(block.number); - req.useBlockhash = useBlockHash; - + assignedSequenceNumber = req.sequenceNumber; 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. + // Request a random number. The method expects the provider address and a secret random number + // in the arguments. It returns a sequence 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. + // The address calling this function should be a contract that inherits from the IEntropyConsumer interface. + // The `entropyCallback` method on that interface will receive a callback with the generated random number. // - // This function must be called by the same `msg.sender` that originally requested the random number. This check - // prevents denial-of-service attacks where another actor front-runs the requester's reveal transaction. - function reveal( + // This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value. + // Note that excess value is *not* refunded to the caller. + function requestWithCallback( address provider, - uint64 sequenceNumber, - bytes32 userRandomness, - bytes32 providerRevelation - ) public override returns (bytes32 randomNumber) { - EntropyStructs.Request storage req = findRequest( + bytes32 userRandomNumber + ) public payable override returns (uint64) { + EntropyStructs.Request storage req = requestHelper( provider, - sequenceNumber + constructUserCommitment(userRandomNumber), + // If useBlockHash is set to true, it allows a scenario in which the provider and miner can collude. + // If we remove the blockHash from this, the provider would have no choice but to provide its committed + // random number. Hence, useBlockHash is set to false. + false, + true ); - // Check that there is an active request for the given provider / sequence number. - if ( - req.sequenceNumber == 0 || - req.provider != provider || - req.sequenceNumber != sequenceNumber - ) revert EntropyErrors.NoSuchRequest(); - if (req.requester != msg.sender) revert EntropyErrors.Unauthorized(); + emit RequestedWithCallback( + provider, + req.requester, + req.sequenceNumber, + userRandomNumber, + req + ); + return req.sequenceNumber; + } + + // This method validates the provided user's revelation and provider's revelation against the corresponding + // commitment in the in-flight request. If both values are validated, this method will update the provider + // current commitment and returns the generated random number. + function revealHelper( + EntropyStructs.Request storage req, + bytes32 userRevelation, + bytes32 providerRevelation + ) internal returns (bytes32 randomNumber, bytes32 blockHash) { bytes32 providerCommitment = constructProviderCommitment( req.numHashes, providerRevelation ); - bytes32 userCommitment = constructUserCommitment(userRandomness); + bytes32 userCommitment = constructUserCommitment(userRevelation); if ( keccak256(bytes.concat(userCommitment, providerCommitment)) != req.commitment ) revert EntropyErrors.IncorrectRevelation(); - bytes32 blockHash = bytes32(uint256(0)); + blockHash = bytes32(uint256(0)); if (req.useBlockhash) { bytes32 _blockHash = blockhash(req.blockNumber); @@ -277,28 +305,110 @@ abstract contract Entropy is IEntropy, EntropyState { } randomNumber = combineRandomValues( - userRandomness, + userRevelation, providerRevelation, blockHash ); + EntropyStructs.ProviderInfo storage providerInfo = _state.providers[ + req.provider + ]; + if (providerInfo.currentCommitmentSequenceNumber < req.sequenceNumber) { + providerInfo.currentCommitmentSequenceNumber = req.sequenceNumber; + providerInfo.currentCommitment = providerRevelation; + } + } + + // Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof + // against the corresponding commitments in the in-flight request. If both values are validated, this function returns + // the corresponding random number. + // + // Note that this function can only be called once per in-flight request. Calling this function deletes the stored + // request information (so that the contract doesn't use a linear amount of storage in the number of requests). + // If you need to use the returned random number more than once, you are responsible for storing it. + // + // This function must be called by the same `msg.sender` that originally requested the random number. This check + // prevents denial-of-service attacks where another actor front-runs the requester's reveal transaction. + function reveal( + address provider, + uint64 sequenceNumber, + bytes32 userRevelation, + bytes32 providerRevelation + ) public override returns (bytes32 randomNumber) { + EntropyStructs.Request storage req = findActiveRequest( + provider, + sequenceNumber + ); + + if (req.isRequestWithCallback) { + revert EntropyErrors.InvalidRevealCall(); + } + + if (req.requester != msg.sender) { + revert EntropyErrors.Unauthorized(); + } + bytes32 blockHash; + (randomNumber, blockHash) = revealHelper( + req, + userRevelation, + providerRevelation + ); emit Revealed( req, - userRandomness, + userRevelation, providerRevelation, blockHash, randomNumber ); + clearRequest(provider, sequenceNumber); + } + + // Fulfill a request for a random number and call back the requester. This method validates the provided userRandomness + // and provider's revelation against the corresponding commitment in the in-flight request. If both values are validated, + // this function calls the requester's entropyCallback method with the sequence number and the random number as arguments. + // + // Note that this function can only be called once per in-flight request. Calling this function deletes the stored + // request information (so that the contract doesn't use a linear amount of storage in the number of requests). + // If you need to use the returned random number more than once, you are responsible for storing it. + // + // Anyone can call this method to fulfill a request, but the callback will only be made to the original requester. + function revealWithCallback( + address provider, + uint64 sequenceNumber, + bytes32 userRandomNumber, + bytes32 providerRevelation + ) public override { + EntropyStructs.Request storage req = findActiveRequest( + provider, + sequenceNumber + ); + + if (!req.isRequestWithCallback) { + revert EntropyErrors.InvalidRevealCall(); + } + bytes32 blockHash; + bytes32 randomNumber; + (randomNumber, blockHash) = revealHelper( + req, + userRandomNumber, + providerRevelation + ); + + address callAddress = req.requester; + + emit RevealedWithCallback( + req, + userRandomNumber, + providerRevelation, + randomNumber + ); clearRequest(provider, sequenceNumber); - EntropyStructs.ProviderInfo storage providerInfo = _state.providers[ - provider - ]; - if (providerInfo.currentCommitmentSequenceNumber < sequenceNumber) { - providerInfo.currentCommitmentSequenceNumber = sequenceNumber; - providerInfo.currentCommitment = providerRevelation; - } + IEntropyConsumer(callAddress)._entropyCallback( + sequenceNumber, + randomNumber + ); } function getProviderInfo( @@ -408,6 +518,23 @@ abstract contract Entropy is IEntropy, EntropyState { } } + // Find an in-flight active request for given the provider and the sequence number. + // This method returns a reference to the request, and will revert if the request is + // not active. + function findActiveRequest( + address provider, + uint64 sequenceNumber + ) internal view returns (EntropyStructs.Request storage req) { + req = findRequest(provider, sequenceNumber); + + // Check there is an active request for the given provider and sequence number. + if ( + !isActive(req) || + req.provider != provider || + req.sequenceNumber != sequenceNumber + ) revert EntropyErrors.NoSuchRequest(); + } + // Find an in-flight request. // Note that this method can return requests that are not currently active. The caller is responsible for checking // that the returned request is active (if they care). diff --git a/target_chains/ethereum/contracts/contracts/entropy/EntropyUpgradable.sol b/target_chains/ethereum/contracts/contracts/entropy/EntropyUpgradable.sol index 68c48305..8910885e 100644 --- a/target_chains/ethereum/contracts/contracts/entropy/EntropyUpgradable.sol +++ b/target_chains/ethereum/contracts/contracts/entropy/EntropyUpgradable.sol @@ -105,6 +105,6 @@ contract EntropyUpgradable is } function version() public pure returns (string memory) { - return "0.1.0"; + return "0.2.0"; } } diff --git a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol index f5eb0054..f888b3fc 100644 --- a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol @@ -4,13 +4,16 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol"; +import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "./utils/EntropyTestUtils.t.sol"; import "../contracts/entropy/EntropyUpgradable.sol"; // TODO // - fuzz test? -contract EntropyTest is Test, EntropyTestUtils { +contract EntropyTest is Test, EntropyTestUtils, EntropyEvents { ERC1967Proxy public proxy; EntropyUpgradable public random; @@ -222,6 +225,12 @@ contract EntropyTest is Test, EntropyTestUtils { ALL_ZEROS ); + EntropyStructs.Request memory reqAfterReveal = random.getRequest( + provider1, + sequenceNumber + ); + assertEq(reqAfterReveal.sequenceNumber, 0); + // 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( @@ -729,4 +738,205 @@ contract EntropyTest is Test, EntropyTestUtils { vm.expectRevert(); random.setProviderUri(newUri); } + + function testRequestWithCallbackAndReveal() public { + bytes32 userRandomNumber = bytes32(uint(42)); + uint fee = random.getFee(provider1); + EntropyStructs.ProviderInfo memory providerInfo = random + .getProviderInfo(provider1); + + vm.roll(1234); + vm.deal(user1, fee); + vm.startPrank(user1); + vm.expectEmit(false, false, false, true, address(random)); + emit RequestedWithCallback( + provider1, + user1, + providerInfo.sequenceNumber, + userRandomNumber, + EntropyStructs.Request({ + provider: provider1, + sequenceNumber: providerInfo.sequenceNumber, + numHashes: SafeCast.toUint32( + providerInfo.sequenceNumber - + providerInfo.currentCommitmentSequenceNumber + ), + commitment: keccak256( + bytes.concat( + random.constructUserCommitment(userRandomNumber), + providerInfo.currentCommitment + ) + ), + blockNumber: 1234, + requester: user1, + useBlockhash: false, + isRequestWithCallback: true + }) + ); + vm.roll(1234); + uint64 assignedSequenceNumber = random.requestWithCallback{value: fee}( + provider1, + userRandomNumber + ); + + assertEq( + random.getRequest(provider1, assignedSequenceNumber).requester, + user1 + ); + + assertEq( + random.getRequest(provider1, assignedSequenceNumber).provider, + provider1 + ); + + vm.expectRevert(EntropyErrors.InvalidRevealCall.selector); + random.reveal( + provider1, + assignedSequenceNumber, + userRandomNumber, + provider1Proofs[assignedSequenceNumber] + ); + vm.stopPrank(); + } + + function testRequestWithCallbackAndRevealWithCallback() public { + bytes32 userRandomNumber = bytes32(uint(42)); + uint fee = random.getFee(provider1); + EntropyConsumer consumer = new EntropyConsumer(address(random)); + vm.deal(user1, fee); + vm.prank(user1); + uint64 assignedSequenceNumber = consumer.requestEntropy{value: fee}( + userRandomNumber + ); + EntropyStructs.Request memory req = random.getRequest( + provider1, + assignedSequenceNumber + ); + bytes32 blockHash = bytes32(uint256(0)); + + vm.expectEmit(false, false, false, true, address(random)); + emit RevealedWithCallback( + req, + userRandomNumber, + provider1Proofs[assignedSequenceNumber], + random.combineRandomValues( + userRandomNumber, + provider1Proofs[assignedSequenceNumber], + 0 + ) + ); + vm.prank(user1); + random.revealWithCallback( + provider1, + assignedSequenceNumber, + userRandomNumber, + provider1Proofs[assignedSequenceNumber] + ); + + assertEq(consumer.sequence(), assignedSequenceNumber); + assertEq( + consumer.randomness(), + random.combineRandomValues( + userRandomNumber, + provider1Proofs[assignedSequenceNumber], + // No blockhash is being used in callback method. As it + // is being depreceated. Passing 0 for it. + 0 + ) + ); + + EntropyStructs.Request memory reqAfterReveal = random.getRequest( + provider1, + assignedSequenceNumber + ); + assertEq(reqAfterReveal.sequenceNumber, 0); + } + + function testRequestAndRevealWithCallback() public { + uint64 sequenceNumber = request(user2, provider1, 42, false); + assertEq(random.getRequest(provider1, sequenceNumber).requester, user2); + + vm.expectRevert(EntropyErrors.InvalidRevealCall.selector); + vm.prank(user2); + random.revealWithCallback( + provider1, + sequenceNumber, + bytes32(uint256(42)), + provider1Proofs[sequenceNumber] + ); + } + + function testRequestWithCallbackAndRevealWithCallbackFailing() public { + bytes32 userRandomNumber = bytes32(uint(42)); + uint fee = random.getFee(provider1); + EntropyConsumerFails consumer = new EntropyConsumerFails( + address(random) + ); + vm.deal(address(consumer), fee); + vm.startPrank(address(consumer)); + uint64 assignedSequenceNumber = random.requestWithCallback{value: fee}( + provider1, + userRandomNumber + ); + + vm.expectRevert(); + random.revealWithCallback( + provider1, + assignedSequenceNumber, + userRandomNumber, + provider1Proofs[assignedSequenceNumber] + ); + } +} + +contract EntropyConsumer is IEntropyConsumer { + uint64 public sequence; + bytes32 public randomness; + address public entropy; + + constructor(address _entropy) { + entropy = _entropy; + } + + function requestEntropy( + bytes32 randomNumber + ) public payable returns (uint64 sequenceNumber) { + address provider = IEntropy(entropy).getDefaultProvider(); + sequenceNumber = IEntropy(entropy).requestWithCallback{ + value: msg.value + }(provider, randomNumber); + } + + function getEntropy() internal view override returns (address) { + return entropy; + } + + function entropyCallback( + uint64 _sequence, + bytes32 _randomness + ) internal override { + sequence = _sequence; + randomness = _randomness; + } +} + +contract EntropyConsumerFails is IEntropyConsumer { + uint64 public sequence; + bytes32 public randomness; + address public entropy; + + constructor(address _entropy) { + entropy = _entropy; + } + + function getEntropy() internal view override returns (address) { + return entropy; + } + + function entropyCallback( + uint64 _sequence, + bytes32 _randomness + ) internal override { + revert("Callback failed"); + } } diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol index 818b4f4b..017409ee 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol @@ -34,4 +34,8 @@ library EntropyErrors { // The blockhash is 0. // Signature: 0x92555c0e error BlockhashUnavailable(); + // if a request was made using `requestWithCallback`, request should be fulfilled using `revealWithCallback` + // else if a request was made using `request`, request should be fulfilled using `reveal` + // Signature: 0x50f0dc92 + error InvalidRevealCall(); } diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol index 8ee93265..b1ed42b8 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol @@ -7,6 +7,13 @@ interface EntropyEvents { event Registered(EntropyStructs.ProviderInfo provider); event Requested(EntropyStructs.Request request); + event RequestedWithCallback( + address indexed provider, + address indexed requestor, + uint64 indexed sequenceNumber, + bytes32 userRandomNumber, + EntropyStructs.Request request + ); event Revealed( EntropyStructs.Request request, @@ -15,6 +22,12 @@ interface EntropyEvents { bytes32 blockHash, bytes32 randomNumber ); + event RevealedWithCallback( + EntropyStructs.Request request, + bytes32 userRandomNumber, + bytes32 providerRevelation, + bytes32 randomNumber + ); event ProviderFeeUpdated(address provider, uint128 oldFee, uint128 newFee); diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol index 8117c3d7..d5a08069 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol @@ -54,6 +54,8 @@ contract EntropyStructs { address requester; // If true, incorporate the blockhash of blockNumber into the generated random value. bool useBlockhash; - // There are 3 remaining bytes of free space in this slot. + // If true, the requester will be called back with the generated random value. + bool isRequestWithCallback; + // There are 2 remaining bytes of free space in this slot. } } diff --git a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol b/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol index 2ba41bf3..b37343cb 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol @@ -38,7 +38,20 @@ interface IEntropy is EntropyEvents { bool useBlockHash ) external payable returns (uint64 assignedSequenceNumber); - // Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof + // Request a random number. The method expects the provider address and a secret random number + // in the arguments. It returns a sequence number. + // + // The address calling this function should be a contract that inherits from the IEntropyConsumer interface. + // The `entropyCallback` method on that interface will receive a callback with the generated random number. + // + // This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value. + // Note that excess value is *not* refunded to the caller. + function requestWithCallback( + address provider, + bytes32 userRandomNumber + ) external payable returns (uint64 assignedSequenceNumber); + + // Fulfill a request for a random number. This method validates the provided userRevelation 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. // @@ -48,10 +61,26 @@ interface IEntropy is EntropyEvents { function reveal( address provider, uint64 sequenceNumber, - bytes32 userRandomness, + bytes32 userRevelation, bytes32 providerRevelation ) external returns (bytes32 randomNumber); + // Fulfill a request for a random number and call back the requester. This method validates the provided userRandomness + // and provider's revelation against the corresponding commitment in the in-flight request. If both values are validated, + // this function calls the requester's entropyCallback method with the sequence number and the random number as arguments. + // + // Note that this function can only be called once per in-flight request. Calling this function deletes the stored + // request information (so that the contract doesn't use a linear amount of storage in the number of requests). + // If you need to use the returned random number more than once, you are responsible for storing it. + // + // Anyone can call this method to fulfill a request, but the callback will only be made to the original requester. + function revealWithCallback( + address provider, + uint64 sequenceNumber, + bytes32 userRandomNumber, + bytes32 providerRevelation + ) external; + function getProviderInfo( address provider ) external view returns (EntropyStructs.ProviderInfo memory info); diff --git a/target_chains/ethereum/entropy_sdk/solidity/IEntropyConsumer.sol b/target_chains/ethereum/entropy_sdk/solidity/IEntropyConsumer.sol new file mode 100644 index 00000000..a726b780 --- /dev/null +++ b/target_chains/ethereum/entropy_sdk/solidity/IEntropyConsumer.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.0; + +abstract contract IEntropyConsumer { + // This method is called by Entropy to provide the random number to the consumer. + // It asserts that the msg.sender is the Entropy contract. It is not meant to be + // override by the consumer. + function _entropyCallback(uint64 sequence, bytes32 randomNumber) external { + address entropy = getEntropy(); + require(entropy != address(0), "Entropy address not set"); + require(msg.sender == entropy, "Only Entropy can call this function"); + + entropyCallback(sequence, randomNumber); + } + + // getEntropy returns Entropy contract address. The method is being used to check that the + // callback is indeed from Entropy contract. The consumer is expected to implement this method. + // Entropy address can be found here - https://docs.pyth.network/entropy/contract-addresses + function getEntropy() internal view virtual returns (address); + + // This method is expected to be implemented by the consumer to handle the random number. + // It will be called by _entropyCallback after _entropyCallback ensures that the call is + // indeed from Entropy contract. + function entropyCallback( + uint64 sequence, + bytes32 randomNumber + ) internal virtual; +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyErrors.json b/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyErrors.json index 1274b4a6..d37ec781 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyErrors.json +++ b/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyErrors.json @@ -19,6 +19,11 @@ "name": "InsufficientFee", "type": "error" }, + { + "inputs": [], + "name": "InvalidRevealCall", + "type": "error" + }, { "inputs": [], "name": "InvalidUpgradeMagic", diff --git a/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json b/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json index f5c51b60..1f6df6f8 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json +++ b/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json @@ -153,6 +153,11 @@ "internalType": "bool", "name": "useBlockhash", "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" } ], "indexed": false, @@ -164,6 +169,85 @@ "name": "Requested", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "requestor", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "sequenceNumber", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "userRandomNumber", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "uint64", + "name": "sequenceNumber", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "numHashes", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "blockNumber", + "type": "uint64" + }, + { + "internalType": "address", + "name": "requester", + "type": "address" + }, + { + "internalType": "bool", + "name": "useBlockhash", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct EntropyStructs.Request", + "name": "request", + "type": "tuple" + } + ], + "name": "RequestedWithCallback", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -203,6 +287,11 @@ "internalType": "bool", "name": "useBlockhash", "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" } ], "indexed": false, @@ -237,5 +326,78 @@ ], "name": "Revealed", "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "uint64", + "name": "sequenceNumber", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "numHashes", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "blockNumber", + "type": "uint64" + }, + { + "internalType": "address", + "name": "requester", + "type": "address" + }, + { + "internalType": "bool", + "name": "useBlockhash", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct EntropyStructs.Request", + "name": "request", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "userRandomNumber", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "providerRevelation", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "randomNumber", + "type": "bytes32" + } + ], + "name": "RevealedWithCallback", + "type": "event" } ] diff --git a/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json b/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json index 311ddef7..3c9d99fa 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json +++ b/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json @@ -153,6 +153,11 @@ "internalType": "bool", "name": "useBlockhash", "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" } ], "indexed": false, @@ -164,6 +169,85 @@ "name": "Requested", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "requestor", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "sequenceNumber", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "userRandomNumber", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "uint64", + "name": "sequenceNumber", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "numHashes", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "blockNumber", + "type": "uint64" + }, + { + "internalType": "address", + "name": "requester", + "type": "address" + }, + { + "internalType": "bool", + "name": "useBlockhash", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct EntropyStructs.Request", + "name": "request", + "type": "tuple" + } + ], + "name": "RequestedWithCallback", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -203,6 +287,11 @@ "internalType": "bool", "name": "useBlockhash", "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" } ], "indexed": false, @@ -238,6 +327,79 @@ "name": "Revealed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "uint64", + "name": "sequenceNumber", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "numHashes", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "blockNumber", + "type": "uint64" + }, + { + "internalType": "address", + "name": "requester", + "type": "address" + }, + { + "internalType": "bool", + "name": "useBlockhash", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" + } + ], + "indexed": false, + "internalType": "struct EntropyStructs.Request", + "name": "request", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "userRandomNumber", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "providerRevelation", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "randomNumber", + "type": "bytes32" + } + ], + "name": "RevealedWithCallback", + "type": "event" + }, { "inputs": [ { @@ -453,6 +615,11 @@ "internalType": "bool", "name": "useBlockhash", "type": "bool" + }, + { + "internalType": "bool", + "name": "isRequestWithCallback", + "type": "bool" } ], "internalType": "struct EntropyStructs.Request", @@ -525,6 +692,30 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "userRandomNumber", + "type": "bytes32" + } + ], + "name": "requestWithCallback", + "outputs": [ + { + "internalType": "uint64", + "name": "assignedSequenceNumber", + "type": "uint64" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -539,7 +730,7 @@ }, { "internalType": "bytes32", - "name": "userRandomness", + "name": "userRevelation", "type": "bytes32" }, { @@ -559,6 +750,34 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "uint64", + "name": "sequenceNumber", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "userRandomNumber", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "providerRevelation", + "type": "bytes32" + } + ], + "name": "revealWithCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropyConsumer.json b/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropyConsumer.json new file mode 100644 index 00000000..0bc18b82 --- /dev/null +++ b/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropyConsumer.json @@ -0,0 +1,20 @@ +[ + { + "inputs": [ + { + "internalType": "uint64", + "name": "sequence", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "randomNumber", + "type": "bytes32" + } + ], + "name": "_entropyCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/target_chains/ethereum/entropy_sdk/solidity/package.json b/target_chains/ethereum/entropy_sdk/solidity/package.json index c660be45..1e2eb35d 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/package.json +++ b/target_chains/ethereum/entropy_sdk/solidity/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/entropy-sdk-solidity", - "version": "1.1.3", + "version": "1.2.0", "description": "Generate secure random numbers with Pyth Entropy", "repository": { "type": "git", @@ -12,7 +12,7 @@ }, "scripts": { "format": "npx prettier --write .", - "generate-abi": "npx generate-abis IEntropy EntropyErrors EntropyEvents EntropyStructs", + "generate-abi": "npx generate-abis IEntropy IEntropyConsumer EntropyErrors EntropyEvents EntropyStructs", "check-abi": "git diff --exit-code abis" }, "keywords": [