diff --git a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol index 87556d252d..0c58b88b17 100644 --- a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol +++ b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol @@ -355,6 +355,49 @@ abstract contract Entropy is IEntropy, EntropyState { } } + // Update the provider commitment and increase the sequence number. + // This is used to reduce the `numHashes` required for future requests which leads to reduced gas usage. + function updateProviderCommitment( + address provider, + uint32 updatedSequenceNumber, + bytes32 providerRevelation + ) public override { + EntropyStructs.ProviderInfo storage providerInfo = _state.providers[ + provider + ]; + if ( + updatedSequenceNumber <= + providerInfo.currentCommitmentSequenceNumber + ) revert EntropyErrors.UpdateTooOld(); + if (updatedSequenceNumber >= providerInfo.endSequenceNumber) + revert EntropyErrors.AssertionFailure(); + + uint32 numHashes = SafeCast.toUint32( + updatedSequenceNumber - providerInfo.currentCommitmentSequenceNumber + ); + bytes32 providerCommitment = constructProviderCommitment( + numHashes, + providerRevelation + ); + + if (providerCommitment != providerInfo.currentCommitment) + revert EntropyErrors.IncorrectRevelation(); + + providerInfo.currentCommitmentSequenceNumber = updatedSequenceNumber; + providerInfo.currentCommitment = providerRevelation; + if ( + providerInfo.currentCommitmentSequenceNumber >= + providerInfo.sequenceNumber + ) { + // This means the provider called the function with a sequence number that was not yet requested. + // Assuming this is landed on-chain it's better to bump the sequence number and never use that range + // for future requests. Otherwise, someone can use the leaked revelation to derive favorable random numbers. + providerInfo.sequenceNumber = + providerInfo.currentCommitmentSequenceNumber + + 1; + } + } + // 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. diff --git a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol index 8d51c2d779..8c0fb0aa77 100644 --- a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol @@ -958,7 +958,6 @@ contract EntropyTest is Test, EntropyTestUtils, EntropyEvents { } function testLastRevealedTooOld() public { - vm.roll(17); uint64 sequenceNumber = request(user2, provider1, 42, false); assertRevealSucceeds( user2, @@ -980,6 +979,108 @@ contract EntropyTest is Test, EntropyTestUtils, EntropyEvents { ); } + function testUpdateProviderCommitment( + uint32 requestCount, + uint32 updateSeqNumber + ) public { + vm.assume(requestCount < 500); + vm.assume(requestCount < provider1ChainLength); + vm.assume(updateSeqNumber < requestCount); + vm.assume(0 < updateSeqNumber); + + for (uint256 i = 0; i < requestCount; i++) { + request(user1, provider1, 42, false); + } + assertInvariants(); + EntropyStructs.ProviderInfo memory info1 = random.getProviderInfo( + provider1 + ); + assertEq(info1.currentCommitmentSequenceNumber, 0); + assertEq(info1.sequenceNumber, requestCount + 1); + random.updateProviderCommitment( + provider1, + updateSeqNumber, + provider1Proofs[updateSeqNumber] + ); + info1 = random.getProviderInfo(provider1); + assertEq(info1.currentCommitmentSequenceNumber, updateSeqNumber); + assertEq(info1.currentCommitment, provider1Proofs[updateSeqNumber]); + assertEq(info1.sequenceNumber, requestCount + 1); + assertInvariants(); + } + + function testUpdateProviderCommitmentTooOld( + uint32 requestCount, + uint32 updateSeqNumber + ) public { + vm.assume(requestCount < 500); + vm.assume(requestCount < provider1ChainLength); + vm.assume(updateSeqNumber < requestCount); + vm.assume(0 < updateSeqNumber); + + for (uint256 i = 0; i < requestCount; i++) { + request(user1, provider1, 42, false); + } + assertRevealSucceeds( + user1, + provider1, + requestCount, + 42, + provider1Proofs[requestCount], + ALL_ZEROS + ); + vm.expectRevert(EntropyErrors.UpdateTooOld.selector); + random.updateProviderCommitment( + provider1, + updateSeqNumber, + provider1Proofs[updateSeqNumber] + ); + } + + function testUpdateProviderCommitmentIncorrectRevelation( + uint32 seqNumber, + uint32 mismatchedProofNumber + ) public { + vm.assume(seqNumber < provider1ChainLength); + vm.assume(mismatchedProofNumber < provider1ChainLength); + vm.assume(seqNumber != mismatchedProofNumber); + vm.assume(seqNumber > 0); + vm.expectRevert(EntropyErrors.IncorrectRevelation.selector); + random.updateProviderCommitment( + provider1, + seqNumber, + provider1Proofs[mismatchedProofNumber] + ); + } + + function testUpdateProviderCommitmentUpdatesSequenceNumber( + uint32 seqNumber + ) public { + vm.assume(seqNumber < provider1ChainLength); + vm.assume(seqNumber > 0); + random.updateProviderCommitment( + provider1, + seqNumber, + provider1Proofs[seqNumber] + ); + EntropyStructs.ProviderInfo memory info1 = random.getProviderInfo( + provider1 + ); + assertEq(info1.sequenceNumber, seqNumber + 1); + } + + function testUpdateProviderCommitmentHigherThanChainLength( + uint32 seqNumber + ) public { + vm.assume(seqNumber >= provider1ChainLength); + vm.expectRevert(EntropyErrors.AssertionFailure.selector); + random.updateProviderCommitment( + provider1, + seqNumber, + provider1Proofs[0] + ); + } + function testFeeManager() public { address manager = address(12); diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol index 4b56c95404..0511daae9a 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/EntropyErrors.sol @@ -41,4 +41,6 @@ library EntropyErrors { // The last random number revealed from the provider is too old. Therefore, too many hashes // are required for any new reveal. Please update the currentCommitment before making more requests. error LastRevealedTooOld(); + // A more recent commitment is already revealed on-chain + error UpdateTooOld(); } diff --git a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol b/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol index 2d9b6ac72f..d53bd196f0 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol @@ -120,6 +120,14 @@ interface IEntropy is EntropyEvents { // will override the previous value. Call this function with the all-zero address to disable the fee manager role. function setFeeManager(address manager) external; + // Update the provider commitment and increase the sequence number. + // This is used to reduce the `numHashes` required for future requests which leads to reduced gas usage. + function updateProviderCommitment( + address provider, + uint32 updatedSequenceNumber, + bytes32 providerRevelation + ) external; + function constructUserCommitment( bytes32 userRandomness ) external pure returns (bytes32 userCommitment);