[entropy] Entropy gas benchmarks and optimizations (#1153)
* add gas benchmark * fix benchmark * fix benchmark * fix benchmark * optimization 1: remove provider * move to u128 for fees * update benchmark * comment * reduce commitment storage * optimize storage more * fix fee fields in state * hmm * ok * fix * cleanup * this got out of hand * test overflow conditions * fix bad merge * doc
This commit is contained in:
parent
f1bec26581
commit
b2e4d56d36
|
@ -89,7 +89,7 @@ A gas report should have a couple of tables like this:
|
|||
|
||||
For most of the methods, the minimum gas usage is an indication of our desired gas usage. Because the calls that store something in the storage
|
||||
for the first time in `setUp` use significantly more gas. For example, in the above table, there are two calls to `updatePriceFeeds`. The first
|
||||
call has happend in the `setUp` method and costed over a million gas and is not intended for our Benchmark. So our desired value is the
|
||||
call has happened in the `setUp` method and costed over a million gas and is not intended for our Benchmark. So our desired value is the
|
||||
minimum value which is around 380k gas.
|
||||
|
||||
If you like to optimize the contract and measure the gas optimization you can get gas snapshots using `forge snapshot` and evaluate your
|
||||
|
|
|
@ -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 "@openzeppelin/contracts/utils/math/SafeCast.sol";
|
||||
import "./EntropyState.sol";
|
||||
|
||||
// Entropy implements a secure 2-party random number generation procedure. The protocol
|
||||
|
@ -74,10 +75,26 @@ import "./EntropyState.sol";
|
|||
// cases where the user chooses not to reveal.
|
||||
contract Entropy is IEntropy, EntropyState {
|
||||
// TODO: Use an upgradeable proxy
|
||||
constructor(uint pythFeeInWei, address defaultProvider) {
|
||||
constructor(
|
||||
uint128 pythFeeInWei,
|
||||
address defaultProvider,
|
||||
bool prefillRequestStorage
|
||||
) {
|
||||
_state.accruedPythFeesInWei = 0;
|
||||
_state.pythFeeInWei = pythFeeInWei;
|
||||
_state.defaultProvider = defaultProvider;
|
||||
|
||||
if (prefillRequestStorage) {
|
||||
// Write some data to every storage slot in the requests array such that new requests
|
||||
// use a more consistent amount of gas.
|
||||
// Note that these requests are not live because their sequenceNumber is 0.
|
||||
for (uint8 i = 0; i < NUM_REQUESTS; i++) {
|
||||
EntropyStructs.Request storage req = _state.requests[i];
|
||||
req.provider = address(1);
|
||||
req.blockNumber = 1234;
|
||||
req.commitment = hex"0123";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register msg.sender as a randomness provider. The arguments are the provider's configuration parameters
|
||||
|
@ -86,7 +103,7 @@ contract Entropy is IEntropy, EntropyState {
|
|||
//
|
||||
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
|
||||
function register(
|
||||
uint feeInWei,
|
||||
uint128 feeInWei,
|
||||
bytes32 commitment,
|
||||
bytes calldata commitmentMetadata,
|
||||
uint64 chainLength,
|
||||
|
@ -121,7 +138,7 @@ contract Entropy is IEntropy, EntropyState {
|
|||
// 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 {
|
||||
function withdraw(uint128 amount) public override {
|
||||
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
||||
msg.sender
|
||||
];
|
||||
|
@ -166,24 +183,33 @@ contract Entropy is IEntropy, EntropyState {
|
|||
providerInfo.sequenceNumber += 1;
|
||||
|
||||
// Check that fees were paid and increment the pyth / provider balances.
|
||||
uint requiredFee = getFee(provider);
|
||||
uint128 requiredFee = getFee(provider);
|
||||
if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee();
|
||||
providerInfo.accruedFeesInWei += providerInfo.feeInWei;
|
||||
_state.accruedPythFeesInWei += (msg.value - providerInfo.feeInWei);
|
||||
_state.accruedPythFeesInWei += (SafeCast.toUint128(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)
|
||||
];
|
||||
// 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(
|
||||
provider,
|
||||
assignedSequenceNumber
|
||||
);
|
||||
req.provider = provider;
|
||||
req.sequenceNumber = assignedSequenceNumber;
|
||||
req.userCommitment = userCommitment;
|
||||
req.providerCommitment = providerInfo.currentCommitment;
|
||||
req.providerCommitmentSequenceNumber = providerInfo
|
||||
.currentCommitmentSequenceNumber;
|
||||
req.numHashes = SafeCast.toUint32(
|
||||
assignedSequenceNumber -
|
||||
providerInfo.currentCommitmentSequenceNumber
|
||||
);
|
||||
req.commitment = keccak256(
|
||||
bytes.concat(userCommitment, providerInfo.currentCommitment)
|
||||
);
|
||||
|
||||
if (useBlockHash) {
|
||||
req.blockNumber = block.number;
|
||||
req.blockNumber = SafeCast.toUint96(block.number);
|
||||
} else {
|
||||
req.blockNumber = 0;
|
||||
}
|
||||
|
||||
emit Requested(req);
|
||||
|
@ -202,24 +228,24 @@ contract Entropy is IEntropy, EntropyState {
|
|||
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();
|
||||
EntropyStructs.Request storage req = findRequest(
|
||||
provider,
|
||||
sequenceNumber
|
||||
);
|
||||
// Check that there is a request for the given provider / sequence number.
|
||||
if (req.provider != provider || req.sequenceNumber != sequenceNumber)
|
||||
revert EntropyErrors.NoSuchRequest();
|
||||
|
||||
bool valid = isProofValid(
|
||||
req.providerCommitmentSequenceNumber,
|
||||
req.providerCommitment,
|
||||
sequenceNumber,
|
||||
bytes32 providerCommitment = constructProviderCommitment(
|
||||
req.numHashes,
|
||||
providerRevelation
|
||||
);
|
||||
if (!valid) revert EntropyErrors.IncorrectProviderRevelation();
|
||||
if (constructUserCommitment(userRandomness) != req.userCommitment)
|
||||
revert EntropyErrors.IncorrectUserRevelation();
|
||||
bytes32 userCommitment = constructUserCommitment(userRandomness);
|
||||
if (
|
||||
keccak256(bytes.concat(userCommitment, providerCommitment)) !=
|
||||
req.commitment
|
||||
) revert EntropyErrors.IncorrectRevelation();
|
||||
|
||||
bytes32 blockHash = bytes32(uint256(0));
|
||||
if (req.blockNumber != 0) {
|
||||
|
@ -240,7 +266,7 @@ contract Entropy is IEntropy, EntropyState {
|
|||
randomNumber
|
||||
);
|
||||
|
||||
delete _state.requests[key];
|
||||
clearRequest(provider, sequenceNumber);
|
||||
|
||||
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
|
||||
provider
|
||||
|
@ -270,13 +296,12 @@ contract Entropy is IEntropy, EntropyState {
|
|||
address provider,
|
||||
uint64 sequenceNumber
|
||||
) public view override returns (EntropyStructs.Request memory req) {
|
||||
bytes32 key = requestKey(provider, sequenceNumber);
|
||||
req = _state.requests[key];
|
||||
req = findRequest(provider, sequenceNumber);
|
||||
}
|
||||
|
||||
function getFee(
|
||||
address provider
|
||||
) public view override returns (uint feeAmount) {
|
||||
) public view override returns (uint128 feeAmount) {
|
||||
return _state.providers[provider].feeInWei + _state.pythFeeInWei;
|
||||
}
|
||||
|
||||
|
@ -284,7 +309,7 @@ contract Entropy is IEntropy, EntropyState {
|
|||
public
|
||||
view
|
||||
override
|
||||
returns (uint accruedPythFeesInWei)
|
||||
returns (uint128 accruedPythFeesInWei)
|
||||
{
|
||||
return _state.accruedPythFeesInWei;
|
||||
}
|
||||
|
@ -305,31 +330,90 @@ contract Entropy is IEntropy, EntropyState {
|
|||
);
|
||||
}
|
||||
|
||||
// Create a unique key for an in-flight randomness request (to store it in the contract state)
|
||||
// Create a unique key for an in-flight randomness request. Returns both a long key for use in the requestsOverflow
|
||||
// mapping and a short key for use in the requests array.
|
||||
function requestKey(
|
||||
address provider,
|
||||
uint64 sequenceNumber
|
||||
) internal pure returns (bytes32 hash) {
|
||||
) internal pure returns (bytes32 hash, uint8 shortHash) {
|
||||
hash = keccak256(abi.encodePacked(provider, sequenceNumber));
|
||||
shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
|
||||
}
|
||||
|
||||
// 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,
|
||||
// Construct a provider's commitment given their revealed random number and the distance in the hash chain
|
||||
// between the commitment and the revealed random number.
|
||||
function constructProviderCommitment(
|
||||
uint64 numHashes,
|
||||
bytes32 revelation
|
||||
) internal pure returns (bool valid) {
|
||||
if (sequenceNumber <= lastSequenceNumber)
|
||||
revert EntropyErrors.AssertionFailure();
|
||||
|
||||
bytes32 currentHash = revelation;
|
||||
while (sequenceNumber > lastSequenceNumber) {
|
||||
) internal pure returns (bytes32 currentHash) {
|
||||
currentHash = revelation;
|
||||
while (numHashes > 0) {
|
||||
currentHash = keccak256(bytes.concat(currentHash));
|
||||
sequenceNumber -= 1;
|
||||
numHashes -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
valid = currentHash == lastRevelation;
|
||||
// Find an in-flight request.
|
||||
// Note that this method can return requests that are not currently active. The caller is responsible for checking
|
||||
// that the returned request is active (if they care).
|
||||
function findRequest(
|
||||
address provider,
|
||||
uint64 sequenceNumber
|
||||
) internal view returns (EntropyStructs.Request storage req) {
|
||||
(bytes32 key, uint8 shortKey) = requestKey(provider, sequenceNumber);
|
||||
|
||||
req = _state.requests[shortKey];
|
||||
if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
|
||||
return req;
|
||||
} else {
|
||||
req = _state.requestsOverflow[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the storage for an in-flight request, deleting it from the hash table.
|
||||
function clearRequest(address provider, uint64 sequenceNumber) internal {
|
||||
(bytes32 key, uint8 shortKey) = requestKey(provider, sequenceNumber);
|
||||
|
||||
EntropyStructs.Request storage req = _state.requests[shortKey];
|
||||
if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
|
||||
req.sequenceNumber = 0;
|
||||
} else {
|
||||
delete _state.requestsOverflow[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate storage space for a new in-flight request. This method returns a pointer to a storage slot
|
||||
// that the caller should overwrite with the new request. Note that the memory at this storage slot may
|
||||
// -- and will -- be filled with arbitrary values, so the caller *must* overwrite every field of the returned
|
||||
// struct.
|
||||
function allocRequest(
|
||||
address provider,
|
||||
uint64 sequenceNumber
|
||||
) internal returns (EntropyStructs.Request storage req) {
|
||||
(, uint8 shortKey) = requestKey(provider, sequenceNumber);
|
||||
|
||||
req = _state.requests[shortKey];
|
||||
if (isActive(req)) {
|
||||
// There's already a prior active request in the storage slot we want to use.
|
||||
// Overflow the prior request to the requestsOverflow mapping.
|
||||
// It is important that this code overflows the *prior* request to the mapping, and not the new request.
|
||||
// There is a chance that some requests never get revealed and remain active forever. We do not want such
|
||||
// requests to fill up all of the space in the array and cause all new requests to incur the higher gas cost
|
||||
// of the mapping.
|
||||
//
|
||||
// This operation is expensive, but should be rare. If overflow happens frequently, increase
|
||||
// the size of the requests array to support more concurrent active requests.
|
||||
(bytes32 reqKey, ) = requestKey(req.provider, req.sequenceNumber);
|
||||
_state.requestsOverflow[reqKey] = req;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if a request is active, i.e., its corresponding random value has not yet been revealed.
|
||||
function isActive(
|
||||
EntropyStructs.Request storage req
|
||||
) internal view returns (bool) {
|
||||
// Note that a provider's initial registration occupies sequence number 0, so there is no way to construct
|
||||
// a randomness request with sequence number 0.
|
||||
return req.sequenceNumber != 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,39 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
|
|||
|
||||
contract EntropyInternalStructs {
|
||||
struct State {
|
||||
uint pythFeeInWei;
|
||||
uint accruedPythFeesInWei;
|
||||
// Fee charged by the pyth protocol in wei.
|
||||
uint128 pythFeeInWei;
|
||||
// Total quantity of fees (in wei) earned by the pyth protocol that are currently stored in the contract.
|
||||
// This quantity is incremented when fees are paid and decremented when fees are withdrawn.
|
||||
// Note that u128 can store up to ~10^36 wei, which is ~10^18 in native base tokens, which should be plenty.
|
||||
uint128 accruedPythFeesInWei;
|
||||
// The protocol sets a provider as default to simplify integration for developers.
|
||||
address defaultProvider;
|
||||
// Hash table for storing in-flight requests. Table keys are hash(provider, sequenceNumber), and the value is
|
||||
// the current request (if one is currently in-flight).
|
||||
//
|
||||
// Due to the vagaries of EVM opcode costs, it is inefficient to simply use a mapping here. Overwriting zero-valued
|
||||
// storage slots with non-zero values is expensive in EVM (21k gas). Using a mapping, each new request starts
|
||||
// from all-zero values, and thus incurs a substantial write cost. Deleting non-zero values does refund gas, but
|
||||
// unfortunately the refund is not substantial enough to matter.
|
||||
//
|
||||
// This data structure is a two-level hash table. It first tries to store new requests in the requests array at
|
||||
// an index determined by a few bits of the request's key. If that slot in the array is already occupied by a
|
||||
// prior request, the prior request is evicted into the requestsOverflow mapping. Requests in the array are
|
||||
// considered active if their sequenceNumber is > 0.
|
||||
//
|
||||
// WARNING: the number of requests must be kept in sync with the constants below
|
||||
EntropyStructs.Request[32] requests;
|
||||
mapping(bytes32 => EntropyStructs.Request) requestsOverflow;
|
||||
// Mapping from randomness providers to information about each them.
|
||||
mapping(address => EntropyStructs.ProviderInfo) providers;
|
||||
mapping(bytes32 => EntropyStructs.Request) requests;
|
||||
}
|
||||
}
|
||||
|
||||
contract EntropyState {
|
||||
// The size of the requests hash table. Must be a power of 2.
|
||||
uint8 public constant NUM_REQUESTS = 32;
|
||||
bytes1 public constant NUM_REQUESTS_MASK = 0x1f;
|
||||
|
||||
EntropyInternalStructs.State _state;
|
||||
}
|
||||
|
|
|
@ -5,37 +5,36 @@ pragma solidity ^0.8.0;
|
|||
import "forge-std/Test.sol";
|
||||
import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
|
||||
import "../contracts/entropy/Entropy.sol";
|
||||
import "./utils/EntropyTestUtils.t.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 {
|
||||
contract EntropyTest is Test, EntropyTestUtils {
|
||||
Entropy public random;
|
||||
|
||||
uint pythFeeInWei = 7;
|
||||
uint128 pythFeeInWei = 7;
|
||||
|
||||
address public provider1 = address(1);
|
||||
bytes32[] provider1Proofs;
|
||||
uint provider1FeeInWei = 8;
|
||||
uint128 provider1FeeInWei = 8;
|
||||
uint64 provider1ChainLength = 100;
|
||||
bytes provider1Uri = bytes("https://foo.com");
|
||||
bytes provider1CommitmentMetadata = hex"0100";
|
||||
|
||||
address public provider2 = address(2);
|
||||
bytes32[] provider2Proofs;
|
||||
uint provider2FeeInWei = 20;
|
||||
uint128 provider2FeeInWei = 20;
|
||||
bytes provider2Uri = bytes("https://bar.com");
|
||||
|
||||
address public user1 = address(3);
|
||||
address public user2 = address(4);
|
||||
|
||||
address public unregisteredProvider = address(7);
|
||||
uint256 MAX_UINT256 = 2 ** 256 - 1;
|
||||
uint128 MAX_UINT128 = 2 ** 128 - 1;
|
||||
bytes32 ALL_ZEROS = bytes32(uint256(0));
|
||||
|
||||
function setUp() public {
|
||||
random = new Entropy(pythFeeInWei, provider1);
|
||||
random = new Entropy(pythFeeInWei, provider1, false);
|
||||
|
||||
bytes32[] memory hashChain1 = generateHashChain(
|
||||
provider1,
|
||||
|
@ -64,21 +63,6 @@ contract EntropyTest is Test {
|
|||
);
|
||||
}
|
||||
|
||||
function generateHashChain(
|
||||
address provider,
|
||||
uint64 startSequenceNumber,
|
||||
uint64 size
|
||||
) public pure returns (bytes32[] memory hashChain) {
|
||||
bytes32 initialValue = keccak256(
|
||||
abi.encodePacked(provider, 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,
|
||||
|
@ -431,7 +415,7 @@ contract EntropyTest is Test {
|
|||
// Check that overflowing the fee arithmetic causes the transaction to revert.
|
||||
vm.prank(provider1);
|
||||
random.register(
|
||||
MAX_UINT256,
|
||||
MAX_UINT128,
|
||||
provider1Proofs[0],
|
||||
hex"0100",
|
||||
100,
|
||||
|
@ -441,6 +425,20 @@ contract EntropyTest is Test {
|
|||
random.getFee(provider1);
|
||||
}
|
||||
|
||||
function testOverflow() public {
|
||||
// msg.value overflows the uint128 fee variable
|
||||
assertRequestReverts(2 ** 128, provider1, 42, false);
|
||||
|
||||
// block number is too large
|
||||
vm.roll(2 ** 96);
|
||||
assertRequestReverts(
|
||||
pythFeeInWei + provider1FeeInWei,
|
||||
provider1,
|
||||
42,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function testFees() public {
|
||||
// Insufficient fees causes a revert
|
||||
assertRequestReverts(0, provider1, 42, false);
|
||||
|
@ -497,7 +495,7 @@ contract EntropyTest is Test {
|
|||
assertRequestReverts(pythFeeInWei + 12345 - 1, provider1, 42, false);
|
||||
requestWithFee(user2, pythFeeInWei + 12345, provider1, 42, false);
|
||||
|
||||
uint providerOneBalance = provider1FeeInWei * 3 + 12345;
|
||||
uint128 providerOneBalance = provider1FeeInWei * 3 + 12345;
|
||||
assertEq(
|
||||
random.getProviderInfo(provider1).accruedFeesInWei,
|
||||
providerOneBalance
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
// 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";
|
||||
import "./utils/EntropyTestUtils.t.sol";
|
||||
|
||||
// TODO
|
||||
// - what's the impact of # of in-flight requests on gas usage? More requests => more hashes to
|
||||
// verify the provider's value.
|
||||
contract EntropyGasBenchmark is Test, EntropyTestUtils {
|
||||
Entropy public random;
|
||||
|
||||
uint128 pythFeeInWei = 7;
|
||||
|
||||
address public provider1 = address(1);
|
||||
bytes32[] provider1Proofs;
|
||||
uint128 provider1FeeInWei = 8;
|
||||
uint64 provider1ChainLength = 100;
|
||||
|
||||
address public user1 = address(3);
|
||||
|
||||
function setUp() public {
|
||||
random = new Entropy(pythFeeInWei, provider1, true);
|
||||
|
||||
bytes32[] memory hashChain1 = generateHashChain(
|
||||
provider1,
|
||||
0,
|
||||
provider1ChainLength
|
||||
);
|
||||
provider1Proofs = hashChain1;
|
||||
vm.prank(provider1);
|
||||
random.register(
|
||||
provider1FeeInWei,
|
||||
provider1Proofs[0],
|
||||
hex"0100",
|
||||
provider1ChainLength,
|
||||
""
|
||||
);
|
||||
|
||||
// Register twice so the commitment sequence number is nonzero. Zero values can be misleading
|
||||
// when gas benchmarking.
|
||||
vm.prank(provider1);
|
||||
random.register(
|
||||
provider1FeeInWei,
|
||||
provider1Proofs[0],
|
||||
hex"0100",
|
||||
provider1ChainLength,
|
||||
""
|
||||
);
|
||||
|
||||
assert(
|
||||
random.getProviderInfo(provider1).currentCommitmentSequenceNumber !=
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Test helper method for requesting a random value as user from provider.
|
||||
function requestHelper(
|
||||
address user,
|
||||
uint randomNumber,
|
||||
bool useBlockhash
|
||||
) public returns (uint64 sequenceNumber) {
|
||||
uint fee = random.getFee(provider1);
|
||||
vm.deal(user, fee);
|
||||
vm.prank(user);
|
||||
sequenceNumber = random.request{value: fee}(
|
||||
provider1,
|
||||
random.constructUserCommitment(bytes32(randomNumber)),
|
||||
useBlockhash
|
||||
);
|
||||
}
|
||||
|
||||
function revealHelper(
|
||||
uint64 sequenceNumber,
|
||||
uint userRandom
|
||||
) public returns (bytes32 randomNumber) {
|
||||
randomNumber = random.reveal(
|
||||
provider1,
|
||||
sequenceNumber,
|
||||
bytes32(userRandom),
|
||||
provider1Proofs[
|
||||
sequenceNumber -
|
||||
random
|
||||
.getProviderInfo(provider1)
|
||||
.originalCommitmentSequenceNumber
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function testBasicFlow() public {
|
||||
uint userRandom = 42;
|
||||
uint64 sequenceNumber = requestHelper(user1, userRandom, true);
|
||||
|
||||
revealHelper(sequenceNumber, userRandom);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
|
||||
|
||||
abstract contract EntropyTestUtils is Test {
|
||||
// Generate a hash chain for a provider that can be used for test purposes.
|
||||
function generateHashChain(
|
||||
address provider,
|
||||
uint64 startSequenceNumber,
|
||||
uint64 size
|
||||
) public pure returns (bytes32[] memory hashChain) {
|
||||
bytes32 initialValue = keccak256(
|
||||
abi.encodePacked(provider, startSequenceNumber)
|
||||
);
|
||||
hashChain = new bytes32[](size);
|
||||
for (uint64 i = 0; i < size; i++) {
|
||||
hashChain[size - (i + 1)] = initialValue;
|
||||
initialValue = keccak256(bytes.concat(initialValue));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,17 +6,16 @@ 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 specified request does not exist.
|
||||
error NoSuchRequest();
|
||||
// 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();
|
||||
// Either the user's or the provider's revealed random values did not match their commitment.
|
||||
error IncorrectRevelation();
|
||||
}
|
||||
|
|
|
@ -3,16 +3,9 @@
|
|||
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;
|
||||
uint128 feeInWei;
|
||||
uint128 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.
|
||||
|
@ -42,12 +35,21 @@ contract EntropyStructs {
|
|||
}
|
||||
|
||||
struct Request {
|
||||
// Storage slot 1 //
|
||||
address provider;
|
||||
uint64 sequenceNumber;
|
||||
bytes32 userCommitment;
|
||||
bytes32 providerCommitment;
|
||||
uint64 providerCommitmentSequenceNumber;
|
||||
// The number of hashes required to verify the provider revelation.
|
||||
uint32 numHashes;
|
||||
// Storage slot 2 //
|
||||
// The commitment is keccak256(userCommitment, providerCommitment). Storing the hash instead of both saves 20k gas by
|
||||
// eliminating 1 store.
|
||||
bytes32 commitment;
|
||||
// Storage slot 3 //
|
||||
// If nonzero, the randomness requester wants the blockhash of this block to be incorporated into the random number.
|
||||
uint256 blockNumber;
|
||||
// Note that we're using a uint96 such that we have an additional 20 bytes of storage afterward for an address.
|
||||
// Although block.number returns a uint256, 96 bits should be plenty to index all of the blocks ever generated.
|
||||
uint96 blockNumber;
|
||||
|
||||
// TODO: store the calling contract address here and authenticate the reveal method
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ interface IEntropy is EntropyEvents {
|
|||
//
|
||||
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
|
||||
function register(
|
||||
uint feeInWei,
|
||||
uint128 feeInWei,
|
||||
bytes32 commitment,
|
||||
bytes calldata commitmentMetadata,
|
||||
uint64 chainLength,
|
||||
|
@ -20,7 +20,7 @@ interface IEntropy is EntropyEvents {
|
|||
// 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) external;
|
||||
function withdraw(uint128 amount) external;
|
||||
|
||||
// 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
|
||||
|
@ -63,12 +63,12 @@ interface IEntropy is EntropyEvents {
|
|||
uint64 sequenceNumber
|
||||
) external view returns (EntropyStructs.Request memory req);
|
||||
|
||||
function getFee(address provider) external view returns (uint feeAmount);
|
||||
function getFee(address provider) external view returns (uint128 feeAmount);
|
||||
|
||||
function getAccruedPythFees()
|
||||
external
|
||||
view
|
||||
returns (uint accruedPythFeesInWei);
|
||||
returns (uint128 accruedPythFeesInWei);
|
||||
|
||||
function constructUserCommitment(
|
||||
bytes32 userRandomness
|
||||
|
|
Loading…
Reference in New Issue