Bläddra i källkod

[eth]: add forward compatibility test for accumulator updateData (#884)

* test(ethereum): add forward compatibility test for accumulator data

* test(ethereum): fix typo and rename
swimricky 2 år sedan
förälder
incheckning
bc338ccfb5

+ 121 - 2
target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol

@@ -85,6 +85,24 @@ contract PythWormholeMerkleAccumulatorTest is
         assertEq(emaPrice.publishTime, priceFeed.emaPrice.publishTime);
     }
 
+    function assertPriceFeedEqual(
+        PythStructs.PriceFeed memory priceFeed1,
+        PythStructs.PriceFeed memory priceFeed2
+    ) internal {
+        assertEq(priceFeed1.id, priceFeed2.id);
+        assertEq(priceFeed1.price.price, priceFeed2.price.price);
+        assertEq(priceFeed1.price.conf, priceFeed2.price.conf);
+        assertEq(priceFeed1.price.expo, priceFeed2.price.expo);
+        assertEq(priceFeed1.price.publishTime, priceFeed2.price.publishTime);
+        assertEq(priceFeed1.emaPrice.price, priceFeed2.emaPrice.price);
+        assertEq(priceFeed1.emaPrice.conf, priceFeed2.emaPrice.conf);
+        assertEq(priceFeed1.emaPrice.expo, priceFeed2.emaPrice.expo);
+        assertEq(
+            priceFeed1.emaPrice.publishTime,
+            priceFeed2.emaPrice.publishTime
+        );
+    }
+
     function generateRandomPriceFeedMessage(
         uint numPriceFeeds
     ) internal returns (PriceFeedMessage[] memory priceFeedMessages) {
@@ -120,6 +138,36 @@ contract PythWormholeMerkleAccumulatorTest is
         updateFee = pyth.getUpdateFee(updateData);
     }
 
+    /// @notice This method creates a forward compatible wormhole update data by using a newer minor version,
+    /// setting a trailing header size and generating additional trailing header data of size `trailingHeaderSize`
+    function createFowardCompatibleWormholeMerkleUpdateData(
+        PriceFeedMessage[] memory priceFeedMessages,
+        uint8 minorVersion,
+        uint8 trailingHeaderSize
+    ) internal returns (bytes[] memory updateData, uint updateFee) {
+        updateData = new bytes[](1);
+
+        uint8 depth = 0;
+        while ((1 << depth) < priceFeedMessages.length) {
+            depth++;
+        }
+
+        depth += getRandUint8() % 3;
+        bytes memory trailingHeaderData = new bytes(uint8(0));
+        for (uint i = 0; i < trailingHeaderSize; i++) {
+            trailingHeaderData = abi.encodePacked(trailingHeaderData, uint8(i));
+        }
+        updateData[0] = generateForwardCompatibleWhMerkleUpdate(
+            priceFeedMessages,
+            depth,
+            1,
+            minorVersion,
+            trailingHeaderData
+        );
+
+        updateFee = pyth.getUpdateFee(updateData);
+    }
+
     /// Testing update price feeds method using wormhole merkle update type.
     function testUpdatePriceFeedWithWormholeMerkleWorks(uint seed) public {
         setRandSeed(seed);
@@ -947,6 +995,77 @@ contract PythWormholeMerkleAccumulatorTest is
         assertEq(updateFee, SINGLE_UPDATE_FEE_IN_WEI * numPriceFeeds);
     }
 
-    //TODO: add some tests of forward compatibility.
-    // I.e., create a message where each part that can be expanded in size is expanded and make sure that parsing still works
+    function testParsePriceFeedUpdatesWithWhMerkleUpdateWorksForForwardCompatibility()
+        public
+    {
+        uint numPriceFeeds = (getRandUint() % 10) + 1;
+        PriceFeedMessage[]
+            memory priceFeedMessages = generateRandomPriceFeedMessage(
+                numPriceFeeds
+            );
+        (
+            bytes[] memory updateData,
+            uint updateFee
+        ) = createWormholeMerkleUpdateData(priceFeedMessages);
+
+        bytes32[] memory priceIds = new bytes32[](numPriceFeeds);
+        for (uint i = 0; i < numPriceFeeds; i++) {
+            priceIds[i] = priceFeedMessages[i].priceId;
+        }
+        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
+            value: updateFee
+        }(updateData, priceIds, 0, MAX_UINT64);
+        uint8 futureMinorVersion = uint8(2);
+        uint8 futureTrailingHeaderSize = uint8(20);
+        (
+            bytes[] memory updateDataFromFuture,
+            uint updateFeeFromFuture
+        ) = createFowardCompatibleWormholeMerkleUpdateData(
+                priceFeedMessages,
+                futureMinorVersion,
+                futureTrailingHeaderSize
+            );
+
+        PythStructs.PriceFeed[] memory priceFeedsFromFutureUpdateData = pyth
+            .parsePriceFeedUpdates{value: updateFeeFromFuture}(
+            updateDataFromFuture,
+            priceIds,
+            0,
+            MAX_UINT64
+        );
+        assertEq(updateFee, updateFeeFromFuture);
+
+        for (uint i = 0; i < priceFeeds.length; i++) {
+            assertPriceFeedEqual(
+                priceFeeds[i],
+                priceFeedsFromFutureUpdateData[i]
+            );
+        }
+    }
+
+    function testUpdatePriceFeedUpdatesWithWhMerkleUpdateWorksForForwardCompatibility()
+        public
+    {
+        uint numPriceFeeds = (getRandUint() % 10) + 1;
+        PriceFeedMessage[]
+            memory priceFeedMessages = generateRandomPriceFeedMessage(
+                numPriceFeeds
+            );
+        uint8 futureMinorVersion = uint8(2);
+        uint8 futureTrailingHeaderSize = uint8(20);
+        (
+            bytes[] memory forwardCompatibleUpdateData,
+            uint updateFee
+        ) = createFowardCompatibleWormholeMerkleUpdateData(
+                priceFeedMessages,
+                futureMinorVersion,
+                futureTrailingHeaderSize
+            );
+
+        pyth.updatePriceFeeds{value: updateFee}(forwardCompatibleUpdateData);
+
+        for (uint i = 0; i < priceFeedMessages.length; i++) {
+            assertPriceFeedMessageStored(priceFeedMessages[i]);
+        }
+    }
 }

+ 79 - 0
target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol

@@ -164,6 +164,85 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
         }
     }
 
+    function generateForwardCompatibleWhMerkleUpdate(
+        PriceFeedMessage[] memory priceFeedMessages,
+        uint8 depth,
+        uint8 numSigners,
+        uint8 minorVersion,
+        bytes memory trailingHeaderData
+    ) internal returns (bytes memory whMerkleUpdateData) {
+        bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages(
+            priceFeedMessages
+        );
+
+        (bytes20 rootDigest, bytes[] memory proofs) = MerkleTree
+            .constructProofs(encodedPriceFeedMessages, depth);
+        // refactoring some of these generateWormhole functions was necessary
+        // to workaround the stack too deep limit.
+        bytes
+            memory wormholeMerkleVaa = generateForwardCompatibleWormholeMerkleVaa(
+                rootDigest,
+                trailingHeaderData,
+                numSigners
+            );
+        {
+            whMerkleUpdateData = abi.encodePacked(
+                generateForwardCompatibleWormholeMerkleUpdateHeader(
+                    minorVersion,
+                    trailingHeaderData
+                ),
+                uint16(wormholeMerkleVaa.length),
+                wormholeMerkleVaa,
+                uint8(priceFeedMessages.length)
+            );
+        }
+
+        for (uint i = 0; i < priceFeedMessages.length; i++) {
+            whMerkleUpdateData = abi.encodePacked(
+                whMerkleUpdateData,
+                uint16(encodedPriceFeedMessages[i].length),
+                encodedPriceFeedMessages[i],
+                proofs[i]
+            );
+        }
+    }
+
+    function generateForwardCompatibleWormholeMerkleUpdateHeader(
+        uint8 minorVersion,
+        bytes memory trailingHeaderData
+    ) private returns (bytes memory whMerkleUpdateHeader) {
+        whMerkleUpdateHeader = abi.encodePacked(
+            uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC
+            uint8(1), // major version
+            minorVersion,
+            uint8(trailingHeaderData.length), // trailing header size
+            trailingHeaderData,
+            uint8(PythAccumulator.UpdateType.WormholeMerkle)
+        );
+    }
+
+    function generateForwardCompatibleWormholeMerkleVaa(
+        bytes20 rootDigest,
+        bytes memory futureData,
+        uint8 numSigners
+    ) internal returns (bytes memory wormholeMerkleVaa) {
+        wormholeMerkleVaa = generateVaa(
+            0,
+            SOURCE_EMITTER_CHAIN_ID,
+            SOURCE_EMITTER_ADDRESS,
+            0,
+            abi.encodePacked(
+                uint32(0x41555756), // PythAccumulator.ACCUMULATOR_WORMHOLE_MAGIC
+                uint8(PythAccumulator.UpdateType.WormholeMerkle),
+                uint64(0), // Slot, not used in target networks
+                uint32(0), // Ring size, not used in target networks
+                rootDigest, // this can have bytes past this for future versions
+                futureData
+            ),
+            numSigners
+        );
+    }
+
     // Generates byte-encoded payload for the given price attestations. You can use this to mock wormhole
     // call using `vm.mockCall` and return a VM struct with this payload.
     // You can use generatePriceFeedUpdate to generate a VAA for a price update.