Selaa lähdekoodia

refactor(target_chains/ethereum): remove legacy batch updates support from parsePriceFeedUpdates and getUpdateFee

Pavel Strakhov 1 vuosi sitten
vanhempi
sitoutus
a2db288210

+ 2 - 75
target_chains/ethereum/contracts/contracts/pyth/Pyth.sol

@@ -111,7 +111,7 @@ abstract contract Pyth is
                     offset
                 );
             } else {
-                totalNumUpdates += 1;
+                revert PythErrors.InvalidUpdateData();
             }
         }
         return getTotalFee(totalNumUpdates);
@@ -472,80 +472,7 @@ abstract contract Pyth is
                     if (offset != encoded.length)
                         revert PythErrors.InvalidUpdateData();
                 } else {
-                    bytes memory encoded;
-                    {
-                        IWormhole.VM
-                            memory vm = parseAndVerifyBatchAttestationVM(
-                                updateData[i]
-                            );
-                        encoded = vm.payload;
-                    }
-
-                    /** Batch price logic */
-                    // TODO: gas optimization
-                    (
-                        uint index,
-                        uint nAttestations,
-                        uint attestationSize
-                    ) = parseBatchAttestationHeader(encoded);
-
-                    // Deserialize each attestation
-                    for (uint j = 0; j < nAttestations; j++) {
-                        // NOTE: We don't advance the global index immediately.
-                        // attestationIndex is an attestation-local offset used
-                        // for readability and easier debugging.
-                        uint attestationIndex = 0;
-
-                        // Unused bytes32 product id
-                        attestationIndex += 32;
-
-                        bytes32 priceId = UnsafeBytesLib.toBytes32(
-                            encoded,
-                            index + attestationIndex
-                        );
-
-                        // check whether caller requested for this data
-                        uint k = findIndexOfPriceId(priceIds, priceId);
-
-                        // If priceFeed[k].id != 0 then it means that there was a valid
-                        // update for priceIds[k] and we don't need to process this one.
-                        if (k == priceIds.length || priceFeeds[k].id != 0) {
-                            index += attestationSize;
-                            continue;
-                        }
-
-                        (
-                            PythInternalStructs.PriceInfo memory priceInfo,
-
-                        ) = parseSingleAttestationFromBatch(
-                                encoded,
-                                index,
-                                attestationSize
-                            );
-
-                        updateLatestPriceIfNecessary(priceId, priceInfo);
-
-                        uint publishTime = uint(priceInfo.publishTime);
-                        // Check the publish time of the price is within the given range
-                        // and only fill the priceFeedsInfo if it is.
-                        // If is not, default id value of 0 will still be set and
-                        // this will allow other updates for this price id to be processed.
-                        if (
-                            publishTime >= config.minPublishTime &&
-                            publishTime <= config.maxPublishTime &&
-                            !config.checkUniqueness // do not allow batch updates to be used by parsePriceFeedUpdatesUnique
-                        ) {
-                            fillPriceFeedFromPriceInfo(
-                                priceFeeds,
-                                k,
-                                priceId,
-                                priceInfo,
-                                publishTime
-                            );
-                        }
-
-                        index += attestationSize;
-                    }
+                    revert PythErrors.InvalidUpdateData();
                 }
             }
 

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

@@ -17,8 +17,7 @@ import "../contracts/libraries/MerkleTree.sol";
 contract PythWormholeMerkleAccumulatorTest is
     Test,
     WormholeTestUtils,
-    PythTestUtils,
-    RandTestUtils
+    PythTestUtils
 {
     IPyth public pyth;
 

+ 91 - 149
target_chains/ethereum/contracts/forge-test/Pyth.t.sol

@@ -12,14 +12,21 @@ import "./utils/WormholeTestUtils.t.sol";
 import "./utils/PythTestUtils.t.sol";
 import "./utils/RandTestUtils.t.sol";
 
-contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
+contract PythTest is Test, WormholeTestUtils, PythTestUtils {
     IPyth public pyth;
 
     // -1 is equal to 0xffffff which is the biggest uint if converted back
     uint64 constant MAX_UINT64 = uint64(int64(-1));
 
+    // 2/3 of the guardians should sign a message for a VAA which is 13 out of 19 guardians.
+    // It is possible to have more signers but the median seems to be 13.
+    uint8 constant NUM_GUARDIAN_SIGNERS = 13;
+
+    // We will have less than 512 price for a foreseeable future.
+    uint8 constant MERKLE_TREE_DEPTH = 9;
+
     function setUp() public {
-        pyth = IPyth(setUpPyth(setUpWormholeReceiver(1)));
+        pyth = IPyth(setUpPyth(setUpWormholeReceiver(NUM_GUARDIAN_SIGNERS)));
     }
 
     function generateRandomPriceAttestations(
@@ -28,28 +35,21 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         internal
         returns (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         )
     {
-        attestations = new PriceAttestation[](length);
+        attestations = new PriceFeedMessage[](length);
         priceIds = new bytes32[](length);
 
         for (uint i = 0; i < length; i++) {
-            attestations[i].productId = getRandBytes32();
             attestations[i].priceId = bytes32(i + 1); // price ids should be non-zero and unique
             attestations[i].price = getRandInt64();
             attestations[i].conf = getRandUint64();
             attestations[i].expo = getRandInt32();
             attestations[i].emaPrice = getRandInt64();
             attestations[i].emaConf = getRandUint64();
-            attestations[i].status = PriceAttestationStatus(getRandUint() % 2);
-            attestations[i].numPublishers = getRandUint32();
-            attestations[i].maxNumPublishers = getRandUint32();
-            attestations[i].attestationTime = getRandUint64();
             attestations[i].publishTime = getRandUint64();
             attestations[i].prevPublishTime = getRandUint64();
-            attestations[i].price = getRandInt64();
-            attestations[i].conf = getRandUint64();
 
             priceIds[i] = attestations[i].priceId;
         }
@@ -57,8 +57,9 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
 
     // This method divides attestations into a couple of batches and creates
     // updateData for them. It returns the updateData and the updateFee
-    function createBatchedUpdateDataFromAttestations(
-        PriceAttestation[] memory attestations
+    function createBatchedUpdateDataFromAttestationsWithConfig(
+        PriceFeedMessage[] memory attestations,
+        MerkleUpdateConfig memory config
     ) internal returns (bytes[] memory updateData, uint updateFee) {
         uint batchSize = 1 + (getRandUint() % attestations.length);
         uint numBatches = (attestations.length + batchSize - 1) / batchSize;
@@ -71,35 +72,48 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
                 len = attestations.length - i;
             }
 
-            PriceAttestation[]
-                memory batchAttestations = new PriceAttestation[](len);
+            PriceFeedMessage[]
+                memory batchAttestations = new PriceFeedMessage[](len);
             for (uint j = i; j < i + len; j++) {
                 batchAttestations[j - i] = attestations[j];
             }
 
-            updateData[i / batchSize] = generateWhBatchUpdate(
+            updateData[i / batchSize] = generateWhMerkleUpdateWithSource(
                 batchAttestations,
-                0,
-                1
+                config
             );
         }
 
         updateFee = pyth.getUpdateFee(updateData);
     }
 
+    function createBatchedUpdateDataFromAttestations(
+        PriceFeedMessage[] memory attestations
+    ) internal returns (bytes[] memory updateData, uint updateFee) {
+        (
+            updateData,
+            updateFee
+        ) = createBatchedUpdateDataFromAttestationsWithConfig(
+            attestations,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+    }
+
     /// Testing parsePriceFeedUpdates method.
-    function testParsePriceFeedUpdatesWorksWithTradingStatus(uint seed) public {
+    function testParsePriceFeedUpdatesWorks(uint seed) public {
         setRandSeed(seed);
         uint numAttestations = 1 + (getRandUint() % 10);
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
-        for (uint i = 0; i < numAttestations; i++) {
-            attestations[i].status = PriceAttestationStatus.Trading;
-        }
-
         (
             bytes[] memory updateData,
             uint updateFee
@@ -127,45 +141,6 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         }
     }
 
-    function testParsePriceFeedUpdatesWorksWithUnknownStatus(uint seed) public {
-        setRandSeed(seed);
-        uint numAttestations = 1 + (getRandUint() % 10);
-        (
-            bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
-        ) = generateRandomPriceAttestations(numAttestations);
-
-        for (uint i = 0; i < numAttestations; i++) {
-            attestations[i].status = PriceAttestationStatus.Unknown;
-        }
-
-        (
-            bytes[] memory updateData,
-            uint updateFee
-        ) = createBatchedUpdateDataFromAttestations(attestations);
-        PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
-            value: updateFee
-        }(updateData, priceIds, 0, MAX_UINT64);
-
-        for (uint i = 0; i < numAttestations; i++) {
-            assertEq(priceFeeds[i].id, priceIds[i]);
-            assertEq(priceFeeds[i].price.price, attestations[i].prevPrice);
-            assertEq(priceFeeds[i].price.conf, attestations[i].prevConf);
-            assertEq(priceFeeds[i].price.expo, attestations[i].expo);
-            assertEq(
-                priceFeeds[i].price.publishTime,
-                attestations[i].prevPublishTime
-            );
-            assertEq(priceFeeds[i].emaPrice.price, attestations[i].emaPrice);
-            assertEq(priceFeeds[i].emaPrice.conf, attestations[i].emaConf);
-            assertEq(priceFeeds[i].emaPrice.expo, attestations[i].expo);
-            assertEq(
-                priceFeeds[i].emaPrice.publishTime,
-                attestations[i].prevPublishTime
-            );
-        }
-    }
-
     function testParsePriceFeedUpdatesWorksWithRandomDistinctUpdatesInput(
         uint seed
     ) public {
@@ -173,7 +148,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         uint numAttestations = 1 + (getRandUint() % 30);
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
         (
@@ -197,7 +172,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         // Select only first numSelectedAttestations. numSelectedAttestations will be in [0, numAttestations]
         uint numSelectedAttestations = getRandUint() % (numAttestations + 1);
 
-        PriceAttestation[] memory selectedAttestations = new PriceAttestation[](
+        PriceFeedMessage[] memory selectedAttestations = new PriceFeedMessage[](
             numSelectedAttestations
         );
         bytes32[] memory selectedPriceIds = new bytes32[](
@@ -227,58 +202,29 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
             );
             assertEq(priceFeeds[i].emaPrice.expo, selectedAttestations[i].expo);
 
-            if (
-                selectedAttestations[i].status == PriceAttestationStatus.Trading
-            ) {
-                assertEq(
-                    priceFeeds[i].price.price,
-                    selectedAttestations[i].price
-                );
-                assertEq(
-                    priceFeeds[i].price.conf,
-                    selectedAttestations[i].conf
-                );
-                assertEq(
-                    priceFeeds[i].price.publishTime,
-                    selectedAttestations[i].publishTime
-                );
-                assertEq(
-                    priceFeeds[i].emaPrice.publishTime,
-                    selectedAttestations[i].publishTime
-                );
-            } else {
-                assertEq(
-                    priceFeeds[i].price.price,
-                    selectedAttestations[i].prevPrice
-                );
-                assertEq(
-                    priceFeeds[i].price.conf,
-                    selectedAttestations[i].prevConf
-                );
-                assertEq(
-                    priceFeeds[i].price.publishTime,
-                    selectedAttestations[i].prevPublishTime
-                );
-                assertEq(
-                    priceFeeds[i].emaPrice.publishTime,
-                    selectedAttestations[i].prevPublishTime
-                );
-            }
+            assertEq(priceFeeds[i].price.price, selectedAttestations[i].price);
+            assertEq(priceFeeds[i].price.conf, selectedAttestations[i].conf);
+            assertEq(
+                priceFeeds[i].price.publishTime,
+                selectedAttestations[i].publishTime
+            );
+            assertEq(
+                priceFeeds[i].emaPrice.publishTime,
+                selectedAttestations[i].publishTime
+            );
         }
     }
 
     function testParsePriceFeedUpdatesWorksWithOverlappingWithinTimeRangeUpdates()
         public
     {
-        PriceAttestation[] memory attestations = new PriceAttestation[](2);
+        PriceFeedMessage[] memory attestations = new PriceFeedMessage[](2);
 
         attestations[0].priceId = bytes32(uint(1));
-        attestations[0].status = PriceAttestationStatus.Trading;
         attestations[0].price = 1000;
         attestations[0].publishTime = 10;
 
         attestations[1].priceId = bytes32(uint(1));
-        attestations[1].status = PriceAttestationStatus.Trading;
         attestations[1].price = 2000;
         attestations[1].publishTime = 20;
 
@@ -308,15 +254,13 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
     function testParsePriceFeedUpdatesWorksWithOverlappingMixedTimeRangeUpdates()
         public
     {
-        PriceAttestation[] memory attestations = new PriceAttestation[](2);
+        PriceFeedMessage[] memory attestations = new PriceFeedMessage[](2);
 
         attestations[0].priceId = bytes32(uint(1));
-        attestations[0].status = PriceAttestationStatus.Trading;
         attestations[0].price = 1000;
         attestations[0].publishTime = 10;
 
         attestations[1].priceId = bytes32(uint(1));
-        attestations[1].status = PriceAttestationStatus.Trading;
         attestations[1].price = 2000;
         attestations[1].publishTime = 20;
 
@@ -354,7 +298,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         uint numAttestations = 10;
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
         (
@@ -382,18 +326,22 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         uint numAttestations = 1 + (getRandUint() % 10);
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
         (
             bytes[] memory updateData,
             uint updateFee
-        ) = createBatchedUpdateDataFromAttestations(attestations);
-
-        uint mutPos = getRandUint() % updateData[0].length;
-
-        // mutate the random position by 1 bit
-        updateData[0][mutPos] = bytes1(uint8(updateData[0][mutPos]) ^ 1);
+        ) = createBatchedUpdateDataFromAttestationsWithConfig(
+                attestations,
+                MerkleUpdateConfig(
+                    MERKLE_TREE_DEPTH,
+                    NUM_GUARDIAN_SIGNERS,
+                    SOURCE_EMITTER_CHAIN_ID,
+                    SOURCE_EMITTER_ADDRESS,
+                    true
+                )
+            );
 
         // It might revert due to different wormhole errors
         vm.expectRevert();
@@ -411,20 +359,22 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         uint numAttestations = 10;
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
-        bytes[] memory updateData = new bytes[](1);
-        updateData[0] = generateVaa(
-            uint32(block.timestamp),
-            SOURCE_EMITTER_CHAIN_ID + 1,
-            SOURCE_EMITTER_ADDRESS,
-            1, // Sequence
-            generatePriceFeedUpdatePayload(attestations),
-            1 // Num signers
-        );
-
-        uint updateFee = pyth.getUpdateFee(updateData);
+        (
+            bytes[] memory updateData,
+            uint updateFee
+        ) = createBatchedUpdateDataFromAttestationsWithConfig(
+                attestations,
+                MerkleUpdateConfig(
+                    MERKLE_TREE_DEPTH,
+                    NUM_GUARDIAN_SIGNERS,
+                    SOURCE_EMITTER_CHAIN_ID + 1,
+                    SOURCE_EMITTER_ADDRESS,
+                    false
+                )
+            );
 
         vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
         pyth.parsePriceFeedUpdates{value: updateFee}(
@@ -441,21 +391,20 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         uint numAttestations = 10;
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
-        bytes[] memory updateData = new bytes[](1);
-        updateData[0] = generateVaa(
-            uint32(block.timestamp),
-            SOURCE_EMITTER_CHAIN_ID,
-            0x00000000000000000000000000000000000000000000000000000000000000aa, // Random wrong source address
-            1, // Sequence
-            generatePriceFeedUpdatePayload(attestations),
-            1 // Num signers
+        (bytes[] memory updateData, uint updateFee) = createBatchedUpdateDataFromAttestationsWithConfig(
+            attestations,
+            MerkleUpdateConfig(
+                MERKLE_TREE_DEPTH,
+                NUM_GUARDIAN_SIGNERS,
+                SOURCE_EMITTER_CHAIN_ID,
+                0x00000000000000000000000000000000000000000000000000000000000000aa, // Random wrong source address
+                false
+            )
         );
 
-        uint updateFee = pyth.getUpdateFee(updateData);
-
         vm.expectRevert(PythErrors.InvalidUpdateDataSource.selector);
         pyth.parsePriceFeedUpdates{value: updateFee}(
             updateData,
@@ -466,10 +415,9 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
     }
 
     function testParsePriceFeedUpdatesRevertsIfPriceIdNotIncluded() public {
-        PriceAttestation[] memory attestations = new PriceAttestation[](1);
+        PriceFeedMessage[] memory attestations = new PriceFeedMessage[](1);
 
         attestations[0].priceId = bytes32(uint(1));
-        attestations[0].status = PriceAttestationStatus.Trading;
         attestations[0].price = 1000;
         attestations[0].publishTime = 10;
 
@@ -494,12 +442,10 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         uint numAttestations = 10;
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
         for (uint i = 0; i < numAttestations; i++) {
-            // Set status to Trading so publishTime is used
-            attestations[i].status = PriceAttestationStatus.Trading;
             attestations[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200]
         }
 
@@ -530,12 +476,10 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         uint numAttestations = 10;
         (
             bytes32[] memory priceIds,
-            PriceAttestation[] memory attestations
+            PriceFeedMessage[] memory attestations
         ) = generateRandomPriceAttestations(numAttestations);
 
         for (uint i = 0; i < numAttestations; i++) {
-            // Set status to Trading so publishTime is used
-            attestations[i].status = PriceAttestationStatus.Trading;
             attestations[i].publishTime = uint64((getRandUint() % 101)); // All between [0, 100]
         }
 
@@ -561,8 +505,6 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils, RandTestUtils {
         }
 
         for (uint i = 0; i < numAttestations; i++) {
-            // Set status to Trading so publishTime is used
-            attestations[i].status = PriceAttestationStatus.Trading;
             attestations[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200]
         }
 

+ 1 - 6
target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol

@@ -16,12 +16,7 @@ import "./utils/PythTestUtils.t.sol";
 import "./utils/RandTestUtils.t.sol";
 
 // Experiments to measure the gas usage of different ways of verifying prices in the EVM contract.
-contract VerificationExperiments is
-    Test,
-    WormholeTestUtils,
-    PythTestUtils,
-    RandTestUtils
-{
+contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils {
     // 19, current mainnet number of guardians, is used to have gas estimates
     // close to our mainnet transactions.
     uint8 constant NUM_GUARDIANS = 19;

+ 41 - 8
target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol

@@ -17,7 +17,7 @@ import "forge-std/Test.sol";
 import "./WormholeTestUtils.t.sol";
 import "./RandTestUtils.t.sol";
 
-abstract contract PythTestUtils is Test, WormholeTestUtils {
+abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils {
     uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1;
     bytes32 constant SOURCE_EMITTER_ADDRESS =
         0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b;
@@ -95,6 +95,14 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
         uint64 emaConf;
     }
 
+    struct MerkleUpdateConfig {
+        uint8 depth;
+        uint8 numSigners;
+        uint16 source_chain_id;
+        bytes32 source_emitter_address;
+        bool brokenVaa;
+    }
+
     function encodePriceFeedMessages(
         PriceFeedMessage[] memory priceFeedMessages
     ) internal pure returns (bytes[] memory encodedPriceFeedMessages) {
@@ -115,17 +123,16 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
         }
     }
 
-    function generateWhMerkleUpdate(
+    function generateWhMerkleUpdateWithSource(
         PriceFeedMessage[] memory priceFeedMessages,
-        uint8 depth,
-        uint8 numSigners
+        MerkleUpdateConfig memory config
     ) internal returns (bytes memory whMerkleUpdateData) {
         bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages(
             priceFeedMessages
         );
 
         (bytes20 rootDigest, bytes[] memory proofs) = MerkleTree
-            .constructProofs(encodedPriceFeedMessages, depth);
+            .constructProofs(encodedPriceFeedMessages, config.depth);
 
         bytes memory wormholePayload = abi.encodePacked(
             uint32(0x41555756), // PythAccumulator.ACCUMULATOR_WORMHOLE_MAGIC
@@ -137,13 +144,22 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
 
         bytes memory wormholeMerkleVaa = generateVaa(
             0,
-            SOURCE_EMITTER_CHAIN_ID,
-            SOURCE_EMITTER_ADDRESS,
+            config.source_chain_id,
+            config.source_emitter_address,
             0,
             wormholePayload,
-            numSigners
+            config.numSigners
         );
 
+        if (config.brokenVaa) {
+            uint mutPos = getRandUint() % wormholeMerkleVaa.length;
+
+            // mutate the random position by 1 bit
+            wormholeMerkleVaa[mutPos] = bytes1(
+                uint8(wormholeMerkleVaa[mutPos]) ^ 1
+            );
+        }
+
         whMerkleUpdateData = abi.encodePacked(
             uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC
             uint8(1), // major version
@@ -165,6 +181,23 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
         }
     }
 
+    function generateWhMerkleUpdate(
+        PriceFeedMessage[] memory priceFeedMessages,
+        uint8 depth,
+        uint8 numSigners
+    ) internal returns (bytes memory whMerkleUpdateData) {
+        whMerkleUpdateData = generateWhMerkleUpdateWithSource(
+            priceFeedMessages,
+            MerkleUpdateConfig(
+                depth,
+                numSigners,
+                SOURCE_EMITTER_CHAIN_ID,
+                SOURCE_EMITTER_ADDRESS,
+                false
+            )
+        );
+    }
+
     function generateForwardCompatibleWhMerkleUpdate(
         PriceFeedMessage[] memory priceFeedMessages,
         uint8 depth,