From 2df390641402b31129ba6321e9850d01d00528de Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 03:21:26 +0000 Subject: [PATCH 1/2] feat(entropy-sdk): add MockEntropy contract for local testing - Implements IEntropyV2 interface with two-step request/reveal pattern - requestV2() stores request and returns sequence number - mockReveal() allows manual callback triggering with custom random numbers - Enables testing different interleavings of requests and reveals - Includes comprehensive test suite demonstrating usage patterns - Simplifies testing by removing fee requirements and commitment validation Co-Authored-By: Jayant --- .../ethereum/contracts/test/MockEntropy.t.sol | 246 ++++++++++++++++++ .../entropy_sdk/solidity/MockEntropy.sol | 171 ++++++++++++ 2 files changed, 417 insertions(+) create mode 100644 target_chains/ethereum/contracts/test/MockEntropy.t.sol create mode 100644 target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol diff --git a/target_chains/ethereum/contracts/test/MockEntropy.t.sol b/target_chains/ethereum/contracts/test/MockEntropy.t.sol new file mode 100644 index 0000000000..b1df3f00ca --- /dev/null +++ b/target_chains/ethereum/contracts/test/MockEntropy.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "@pythnetwork/entropy-sdk-solidity/MockEntropy.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; +import "@pythnetwork/entropy-sdk-solidity/EntropyStructsV2.sol"; + +contract MockEntropyConsumer is IEntropyConsumer { + address public entropy; + bytes32 public lastRandomNumber; + uint64 public lastSequenceNumber; + address public lastProvider; + uint256 public callbackCount; + + constructor(address _entropy) { + entropy = _entropy; + } + + function requestRandomNumber() external payable returns (uint64) { + return MockEntropy(entropy).requestV2{value: msg.value}(); + } + + function requestRandomNumberWithGasLimit( + uint32 gasLimit + ) external payable returns (uint64) { + return MockEntropy(entropy).requestV2{value: msg.value}(gasLimit); + } + + function requestRandomNumberFromProvider( + address provider, + uint32 gasLimit + ) external payable returns (uint64) { + return + MockEntropy(entropy).requestV2{value: msg.value}( + provider, + gasLimit + ); + } + + function getEntropy() internal view override returns (address) { + return entropy; + } + + function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber + ) internal override { + lastSequenceNumber = sequenceNumber; + lastProvider = provider; + lastRandomNumber = randomNumber; + callbackCount += 1; + } +} + +contract MockEntropyTest is Test { + MockEntropy public entropy; + MockEntropyConsumer public consumer; + address public provider; + + function setUp() public { + provider = address(0x1234); + entropy = new MockEntropy(provider); + consumer = new MockEntropyConsumer(address(entropy)); + } + + function testBasicRequestAndReveal() public { + uint64 seq = consumer.requestRandomNumber(); + assertEq(seq, 1, "Sequence number should be 1"); + + bytes32 randomNumber = bytes32(uint256(42)); + entropy.mockReveal(provider, seq, randomNumber); + + assertEq( + consumer.lastSequenceNumber(), + seq, + "Callback sequence number mismatch" + ); + assertEq( + consumer.lastProvider(), + provider, + "Callback provider mismatch" + ); + assertEq( + consumer.lastRandomNumber(), + randomNumber, + "Random number mismatch" + ); + assertEq(consumer.callbackCount(), 1, "Callback should be called once"); + } + + function testDifferentInterleavings() public { + uint64 seq1 = consumer.requestRandomNumber(); + uint64 seq2 = consumer.requestRandomNumber(); + uint64 seq3 = consumer.requestRandomNumber(); + + assertEq(seq1, 1, "First sequence should be 1"); + assertEq(seq2, 2, "Second sequence should be 2"); + assertEq(seq3, 3, "Third sequence should be 3"); + + bytes32 random2 = bytes32(uint256(200)); + bytes32 random3 = bytes32(uint256(300)); + bytes32 random1 = bytes32(uint256(100)); + + entropy.mockReveal(provider, seq2, random2); + assertEq( + consumer.lastRandomNumber(), + random2, + "Should reveal seq2 first" + ); + assertEq(consumer.lastSequenceNumber(), seq2, "Sequence should be 2"); + + entropy.mockReveal(provider, seq3, random3); + assertEq( + consumer.lastRandomNumber(), + random3, + "Should reveal seq3 second" + ); + assertEq(consumer.lastSequenceNumber(), seq3, "Sequence should be 3"); + + entropy.mockReveal(provider, seq1, random1); + assertEq( + consumer.lastRandomNumber(), + random1, + "Should reveal seq1 last" + ); + assertEq(consumer.lastSequenceNumber(), seq1, "Sequence should be 1"); + + assertEq( + consumer.callbackCount(), + 3, + "All three callbacks should be called" + ); + } + + function testDifferentProviders() public { + address provider2 = address(0x5678); + + uint64 seq1 = consumer.requestRandomNumberFromProvider( + provider, + 100000 + ); + uint64 seq2 = consumer.requestRandomNumberFromProvider( + provider2, + 100000 + ); + + assertEq(seq1, 1, "Provider 1 first sequence should be 1"); + assertEq(seq2, 1, "Provider 2 first sequence should be 1"); + + bytes32 random1 = bytes32(uint256(111)); + bytes32 random2 = bytes32(uint256(222)); + + entropy.mockReveal(provider, seq1, random1); + assertEq( + consumer.lastRandomNumber(), + random1, + "First provider random number" + ); + + entropy.mockReveal(provider2, seq2, random2); + assertEq( + consumer.lastRandomNumber(), + random2, + "Second provider random number" + ); + } + + function testRequestWithGasLimit() public { + uint64 seq = consumer.requestRandomNumberWithGasLimit(200000); + + assertEq(seq, 1, "Sequence should be 1"); + + EntropyStructsV2.Request memory req = entropy.getRequestV2( + provider, + seq + ); + assertEq(req.gasLimit10k, 20, "Gas limit should be 20 (200k / 10k)"); + + bytes32 randomNumber = bytes32(uint256(999)); + entropy.mockReveal(provider, seq, randomNumber); + + assertEq( + consumer.lastRandomNumber(), + randomNumber, + "Random number should match" + ); + } + + function testGetProviderInfo() public { + EntropyStructsV2.ProviderInfo memory info = entropy.getProviderInfoV2( + provider + ); + assertEq(info.feeInWei, 0, "Fee should be 0"); + assertEq( + info.defaultGasLimit, + 100000, + "Default gas limit should be 100000" + ); + assertEq(info.sequenceNumber, 1, "Sequence number should start at 1"); + } + + function testGetDefaultProvider() public { + assertEq( + entropy.getDefaultProvider(), + provider, + "Default provider should match" + ); + } + + function testFeesReturnZero() public { + assertEq(entropy.getFeeV2(), 0, "getFeeV2() should return 0"); + assertEq( + entropy.getFeeV2(100000), + 0, + "getFeeV2(gasLimit) should return 0" + ); + assertEq( + entropy.getFeeV2(provider, 100000), + 0, + "getFeeV2(provider, gasLimit) should return 0" + ); + } + + function testCustomRandomNumbers() public { + uint64 seq = consumer.requestRandomNumber(); + + bytes32[] memory randomNumbers = new bytes32[](3); + randomNumbers[0] = bytes32(uint256(0)); + randomNumbers[1] = bytes32(type(uint256).max); + randomNumbers[2] = keccak256("custom random value"); + + for (uint i = 0; i < randomNumbers.length; i++) { + if (i > 0) { + seq = consumer.requestRandomNumber(); + } + entropy.mockReveal(provider, seq, randomNumbers[i]); + assertEq( + consumer.lastRandomNumber(), + randomNumbers[i], + "Custom random number should match" + ); + } + } +} diff --git a/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol b/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol new file mode 100644 index 0000000000..4a3e94b0e7 --- /dev/null +++ b/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.0; + +import "./IEntropyV2.sol"; +import "./IEntropyConsumer.sol"; +import "./EntropyStructsV2.sol"; +import "./EntropyEventsV2.sol"; + +contract MockEntropy is IEntropyV2 { + address public defaultProvider; + + mapping(address => EntropyStructsV2.ProviderInfo) public providers; + mapping(address => mapping(uint64 => EntropyStructsV2.Request)) + public requests; + + constructor(address _defaultProvider) { + require(_defaultProvider != address(0), "Invalid default provider"); + defaultProvider = _defaultProvider; + + providers[_defaultProvider].sequenceNumber = 1; + providers[_defaultProvider].feeInWei = 0; + providers[_defaultProvider].defaultGasLimit = 100000; + } + + function requestV2() + external + payable + override + returns (uint64 assignedSequenceNumber) + { + return _requestV2(defaultProvider, bytes32(0), 0); + } + + function requestV2( + uint32 gasLimit + ) external payable override returns (uint64 assignedSequenceNumber) { + return _requestV2(defaultProvider, bytes32(0), gasLimit); + } + + function requestV2( + address provider, + uint32 gasLimit + ) external payable override returns (uint64 assignedSequenceNumber) { + return _requestV2(provider, bytes32(0), gasLimit); + } + + function requestV2( + address provider, + bytes32 userRandomNumber, + uint32 gasLimit + ) external payable override returns (uint64 assignedSequenceNumber) { + return _requestV2(provider, userRandomNumber, gasLimit); + } + + function _requestV2( + address provider, + bytes32 userRandomNumber, + uint32 gasLimit + ) internal returns (uint64 assignedSequenceNumber) { + EntropyStructsV2.ProviderInfo storage providerInfo = providers[ + provider + ]; + + if (providerInfo.sequenceNumber == 0) { + providerInfo.sequenceNumber = 1; + providerInfo.feeInWei = 0; + providerInfo.defaultGasLimit = 100000; + } + + assignedSequenceNumber = providerInfo.sequenceNumber; + providerInfo.sequenceNumber += 1; + + uint32 effectiveGasLimit = gasLimit == 0 + ? providerInfo.defaultGasLimit + : gasLimit; + + EntropyStructsV2.Request storage req = requests[provider][ + assignedSequenceNumber + ]; + req.provider = provider; + req.sequenceNumber = assignedSequenceNumber; + req.requester = msg.sender; + req.blockNumber = uint64(block.number); + req.useBlockhash = false; + req.gasLimit10k = uint16(effectiveGasLimit / 10000); + + emit Requested( + provider, + msg.sender, + assignedSequenceNumber, + userRandomNumber, + effectiveGasLimit, + bytes("") + ); + + return assignedSequenceNumber; + } + + function mockReveal( + address provider, + uint64 sequenceNumber, + bytes32 randomNumber + ) external { + EntropyStructsV2.Request storage req = requests[provider][ + sequenceNumber + ]; + require(req.requester != address(0), "Request not found"); + + address requester = req.requester; + + emit Revealed( + provider, + requester, + sequenceNumber, + randomNumber, + bytes32(0), + bytes32(0), + false, + bytes(""), + 0, + bytes("") + ); + + delete requests[provider][sequenceNumber]; + + uint256 codeSize; + assembly { + codeSize := extcodesize(requester) + } + + if (codeSize > 0) { + IEntropyConsumer(requester)._entropyCallback( + sequenceNumber, + provider, + randomNumber + ); + } + } + + function getProviderInfoV2( + address provider + ) external view override returns (EntropyStructsV2.ProviderInfo memory) { + return providers[provider]; + } + + function getDefaultProvider() external view override returns (address) { + return defaultProvider; + } + + function getRequestV2( + address provider, + uint64 sequenceNumber + ) external view override returns (EntropyStructsV2.Request memory) { + return requests[provider][sequenceNumber]; + } + + function getFeeV2() external pure override returns (uint128) { + return 0; + } + + function getFeeV2(uint32) external pure override returns (uint128) { + return 0; + } + + function getFeeV2( + address, + uint32 + ) external pure override returns (uint128) { + return 0; + } +} From 4e93abd4d1167d64be75ac1419f55e8f1e5144f2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:29:04 +0000 Subject: [PATCH 2/2] fix(entropy-sdk): initialize feeInWei to 1 and bump version to 2.1.0 - Set feeInWei to 1 in MockEntropy constructor and _requestV2 - Update package.json version from 2.0.0 to 2.1.0 - Update test assertion to expect feeInWei = 1 Co-Authored-By: Jayant --- target_chains/ethereum/contracts/test/MockEntropy.t.sol | 2 +- target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol | 4 ++-- target_chains/ethereum/entropy_sdk/solidity/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/target_chains/ethereum/contracts/test/MockEntropy.t.sol b/target_chains/ethereum/contracts/test/MockEntropy.t.sol index b1df3f00ca..f9e74d4a89 100644 --- a/target_chains/ethereum/contracts/test/MockEntropy.t.sol +++ b/target_chains/ethereum/contracts/test/MockEntropy.t.sol @@ -192,7 +192,7 @@ contract MockEntropyTest is Test { EntropyStructsV2.ProviderInfo memory info = entropy.getProviderInfoV2( provider ); - assertEq(info.feeInWei, 0, "Fee should be 0"); + assertEq(info.feeInWei, 1, "Fee should be 1"); assertEq( info.defaultGasLimit, 100000, diff --git a/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol b/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol index 4a3e94b0e7..e5d4e791d4 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol +++ b/target_chains/ethereum/entropy_sdk/solidity/MockEntropy.sol @@ -18,7 +18,7 @@ contract MockEntropy is IEntropyV2 { defaultProvider = _defaultProvider; providers[_defaultProvider].sequenceNumber = 1; - providers[_defaultProvider].feeInWei = 0; + providers[_defaultProvider].feeInWei = 1; providers[_defaultProvider].defaultGasLimit = 100000; } @@ -63,7 +63,7 @@ contract MockEntropy is IEntropyV2 { if (providerInfo.sequenceNumber == 0) { providerInfo.sequenceNumber = 1; - providerInfo.feeInWei = 0; + providerInfo.feeInWei = 1; providerInfo.defaultGasLimit = 100000; } diff --git a/target_chains/ethereum/entropy_sdk/solidity/package.json b/target_chains/ethereum/entropy_sdk/solidity/package.json index 216cecadf1..ba2d35c454 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": "2.0.0", + "version": "2.1.0", "description": "Generate secure random numbers with Pyth Entropy", "type": "module", "repository": {