Forráskód Böngészése

feat(entropy-sdk): add MockEntropy contract for local testing (#3121)

* 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 <jayant@dourolabs.xyz>

* 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 <jayant@dourolabs.xyz>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Jayant <jayant@dourolabs.xyz>
devin-ai-integration[bot] 1 hónapja
szülő
commit
9e519a995a

+ 246 - 0
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, 1, "Fee should be 1");
+        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"
+            );
+        }
+    }
+}

+ 171 - 0
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 = 1;
+        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 = 1;
+            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;
+    }
+}

+ 1 - 1
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": {