From 5ee30bfa223f89d2e0f23b64b4c05b59741b444e Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Wed, 27 Mar 2024 14:13:59 +0000 Subject: [PATCH 01/10] refactor(target_chains/ethereum): remove truffle tests for batch updates --- target_chains/ethereum/contracts/test/pyth.js | 446 +----------------- 1 file changed, 2 insertions(+), 444 deletions(-) diff --git a/target_chains/ethereum/contracts/test/pyth.js b/target_chains/ethereum/contracts/test/pyth.js index 81d3ab7992..16eb1319a8 100644 --- a/target_chains/ethereum/contracts/test/pyth.js +++ b/target_chains/ethereum/contracts/test/pyth.js @@ -77,127 +77,6 @@ contract("Pyth", function () { ); }); - // NOTE(2022-05-02): Raw hex payload obtained from format serialization unit tests in `wormhole_attester/sdk/rust` - // Latest known addition: wire format v3 - // - // Tests rely on a wormhole_attester/sdk/rust mock price/prod ID generation rule: - // nthProdByte(n) = n % 256, starting with n=1 - // nthPriceByte(n) = 255 - (n % 256), starting with n=1 - // - // Examples: - // 1st prod = "0x010101[...]" - // 1st price = "0xFEFEFE[...]" - // 2nd prod = "0x020202[...]" - // 2nd price = "0xFDFDFD[...]" - // 3rd prod = "0x030303[...]" - // 3rd price = "0xFCFCFC[...]" - const RAW_BATCH_ATTESTATION_TIME_REGEX = /DEADBEEFFADEDEED/g; - const RAW_BATCH_PUBLISH_TIME_REGEX = /00000000DADEBEEF/g; - const RAW_BATCH_PRICE_REGEX = /0000002BAD2FEED7/g; - const RAW_BATCH_PREV_PRICE_REGEX = /0000DEADFACEBEEF/g; - const RAW_BATCH_PREV_PUBLISH_TIME_REGEX = /00000000DEADBABE/g; - const RAW_BATCH_EMA_PRICE_REGEX = /FFFFFFFFFFFFFFD6/g; - const RAW_PRICE_ATTESTATION_SIZE = 149; - const RAW_BATCH_ATTESTATION_COUNT = 10; - const RAW_BATCH = - "0x" + - "5032574800030000000102000A00950101010101010101010101010101010101010101010101010101010101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0202020202020202020202020202020202020202020202020202020202020202FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0303030303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0404040404040404040404040404040404040404040404040404040404040404FBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFBFB0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0505050505050505050505050505050505050505050505050505050505050505FAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFAFA0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0606060606060606060606060606060606060606060606060606060606060606F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F9F90000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0707070707070707070707070707070707070707070707070707070707070707F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F8F80000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0808080808080808080808080808080808080808080808080808080808080808F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F70000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0909090909090909090909090909090909090909090909090909090909090909F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F6F60000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0AF5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F50000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF"; - const RAW_UNKNOWN_BATCH_ATTESTATION_COUNT = 3; - const RAW_UNKNOWN_BATCH = - "0x" + - "5032574800030000000102000300950101010101010101010101010101010101010101010101010101010101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A000001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0202020202020202020202020202020202020202020202020202020202020202FDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A000001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0303030303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC0000002BAD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A000001E14C0004E6D0DEADBEEFFADEDEED00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF"; - - // Takes an unsigned 64-bit integer, converts it to hex with 0-padding - function u64ToHex(timestamp) { - // u64 -> 8 bytes -> 16 hex bytes - return timestamp.toString(16).padStart(16, "0"); - } - - function generateRawBatchAttestation( - publishTime, - attestationTime, - priceVal, - emaPriceVal - ) { - const pubTs = u64ToHex(publishTime); - const attTs = u64ToHex(attestationTime); - const price = u64ToHex(priceVal); - const emaPrice = u64ToHex(emaPriceVal || priceVal); - const replaced = RAW_BATCH.replace(RAW_BATCH_PUBLISH_TIME_REGEX, pubTs) - .replace(RAW_BATCH_ATTESTATION_TIME_REGEX, attTs) - .replace(RAW_BATCH_PRICE_REGEX, price) - .replace(RAW_BATCH_EMA_PRICE_REGEX, emaPrice); - return replaced; - } - - function generateRawUnknownBatchAttestation( - publishTime, - attestationTime, - priceVal, - emaPriceVal, - prevPublishTime, - prevPriceVal - ) { - const pubTs = u64ToHex(publishTime); - const attTs = u64ToHex(attestationTime); - const price = u64ToHex(priceVal); - const emaPrice = u64ToHex(emaPriceVal); - const prevPubTs = u64ToHex(prevPublishTime); - const prevPrice = u64ToHex(prevPriceVal); - - const replaced = RAW_UNKNOWN_BATCH.replace( - RAW_BATCH_PUBLISH_TIME_REGEX, - pubTs - ) - .replace(RAW_BATCH_ATTESTATION_TIME_REGEX, attTs) - .replace(RAW_BATCH_PRICE_REGEX, price) - .replace(RAW_BATCH_EMA_PRICE_REGEX, emaPrice) - .replace(RAW_BATCH_PREV_PUBLISH_TIME_REGEX, prevPubTs) - .replace(RAW_BATCH_PREV_PRICE_REGEX, prevPrice); - return replaced; - } - - it("should parse batch price attestation correctly", async function () { - let attestationTime = 1647273460; // re-used for publishTime - let publishTime = 1647273465; // re-used for publishTime - let priceVal = 1337; - let emaPriceVal = 2022; - let rawBatch = generateRawBatchAttestation( - publishTime, - attestationTime, - priceVal, - emaPriceVal - ); - - const receipt = await updatePriceFeeds(this.pythProxy, [rawBatch]); - - expectEventMultipleTimes( - receipt, - "PriceFeedUpdate", - { - price: "1337", - }, - 10 - ); - - for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) { - const price_id = - "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - - const price = await this.pythProxy.getPriceUnsafe(price_id); - assert.equal(price.price, priceVal.toString()); - assert.equal(price.conf, "101"); // The value is hardcoded in the RAW_BATCH. - assert.equal(price.publishTime, publishTime.toString()); - assert.equal(price.expo, "-3"); // The value is hardcoded in the RAW_BATCH. - - const emaPrice = await this.pythProxy.getEmaPriceUnsafe(price_id); - assert.equal(emaPrice.price, emaPriceVal.toString()); - assert.equal(emaPrice.conf, "42"); // The value is hardcoded in the RAW_BATCH. - assert.equal(emaPrice.publishTime, publishTime.toString()); - assert.equal(emaPrice.expo, "-3"); // The value is hardcoded in the RAW_BATCH. - } - }); - async function updatePriceFeeds( contract, batches, @@ -245,50 +124,9 @@ contract("Pyth", function () { ); } - it("should attest price updates over wormhole", async function () { - let ts = 1647273460; - let rawBatch = generateRawBatchAttestation(ts - 5, ts, 1337); - await updatePriceFeeds(this.pythProxy, [rawBatch]); - }); - it("should attest price updates empty", async function () { const receipt = await updatePriceFeeds(this.pythProxy, []); expectEvent.notEmitted(receipt, "PriceFeedUpdate"); - expectEvent.notEmitted(receipt, "BatchPriceFeedUpdate"); - }); - - it("should attest price updates with multiple batches of correct order", async function () { - let ts = 1647273460; - let rawBatch1 = generateRawBatchAttestation(ts - 5, ts, 1337); - let rawBatch2 = generateRawBatchAttestation(ts + 5, ts + 10, 1338); - const receipt = await updatePriceFeeds(this.pythProxy, [ - rawBatch1, - rawBatch2, - ]); - expectEvent(receipt, "PriceFeedUpdate", { - publishTime: (ts - 5).toString(), - }); - expectEvent(receipt, "PriceFeedUpdate", { - publishTime: (ts + 5).toString(), - }); - expectEventMultipleTimes(receipt, "BatchPriceFeedUpdate", {}, 2); - }); - - it("should attest price updates with multiple batches of wrong order", async function () { - let ts = 1647273460; - let rawBatch1 = generateRawBatchAttestation(ts - 5, ts, 1337); - let rawBatch2 = generateRawBatchAttestation(ts + 5, ts + 10, 1338); - const receipt = await updatePriceFeeds(this.pythProxy, [ - rawBatch2, - rawBatch1, - ]); - expectEvent(receipt, "PriceFeedUpdate", { - publishTime: (ts + 5).toString(), - }); - expectEventMultipleTimes(receipt, "BatchPriceFeedUpdate", {}, 2); - expectEventNotEmittedWithArgs(receipt, "PriceFeedUpdate", { - publishTime: (ts - 5).toString(), - }); }); /** @@ -305,151 +143,6 @@ contract("Pyth", function () { ); } - it("should not attest price updates with when required fee is not given", async function () { - // Check initial fee is zero - assert.equal(await this.pythProxy.singleUpdateFeeInWei(), 0); - - // Set fee to 10 - await setFeeTo(this.pythProxy, 10); - assert.equal(await this.pythProxy.singleUpdateFeeInWei(), 10); - - let ts = 1647273460; - let rawBatch1 = generateRawBatchAttestation(ts - 5, ts, 1337); - let rawBatch2 = generateRawBatchAttestation(ts + 5, ts + 10, 1338); - - // Getting the fee from the contract - let feeInWei = await this.pythProxy.methods["getUpdateFee(bytes[])"]([ - rawBatch1, - rawBatch2, - ]); - assert.equal(feeInWei, 20); - - // When a smaller fee is payed it reverts - await expectRevertCustomError( - updatePriceFeeds(this.pythProxy, [rawBatch1, rawBatch2], feeInWei - 1), - "InsufficientFee" - ); - }); - - it("should attest price updates with when required fee is given", async function () { - // Check initial fee is zero - assert.equal(await this.pythProxy.singleUpdateFeeInWei(), 0); - - // Set fee to 10 - await setFeeTo(this.pythProxy, 10); - assert.equal(await this.pythProxy.singleUpdateFeeInWei(), 10); - - let ts = 1647273460; - let rawBatch1 = generateRawBatchAttestation(ts - 5, ts, 1337); - let rawBatch2 = generateRawBatchAttestation(ts + 5, ts + 10, 1338); - - // Getting the fee from the contract - let feeInWei = await this.pythProxy.methods["getUpdateFee(bytes[])"]([ - rawBatch1, - rawBatch2, - ]); - assert.equal(feeInWei, 20); - - await updatePriceFeeds(this.pythProxy, [rawBatch1, rawBatch2], feeInWei); - const pythBalance = await web3.eth.getBalance(this.pythProxy.address); - assert.equal(pythBalance, feeInWei); - }); - - it("should attest price updates with required fee even if more fee is given", async function () { - // Check initial fee is zero - assert.equal(await this.pythProxy.singleUpdateFeeInWei(), 0); - - // Set fee to 10 - await setFeeTo(this.pythProxy, 10); - assert.equal(await this.pythProxy.singleUpdateFeeInWei(), 10); - - let ts = 1647273460; - let rawBatch1 = generateRawBatchAttestation(ts - 5, ts, 1337); - let rawBatch2 = generateRawBatchAttestation(ts + 5, ts + 10, 1338); - - // Paying the fee works and extra fee is not paid back. - let feeInWei = await this.pythProxy.methods["getUpdateFee(bytes[])"]([ - rawBatch1, - rawBatch2, - ]); - assert.equal(feeInWei, 20); - - await updatePriceFeeds( - this.pythProxy, - [rawBatch1, rawBatch2], - feeInWei + 10 - ); - const pythBalance = await web3.eth.getBalance(this.pythProxy.address); - assert.equal(pythBalance, feeInWei + 10); - }); - - it("should cache price updates", async function () { - let currentTimestamp = (await web3.eth.getBlock("latest")).timestamp; - let priceVal = 521; - let rawBatch = generateRawBatchAttestation( - currentTimestamp - 5, - currentTimestamp, - priceVal - ); - let receipt = await updatePriceFeeds(this.pythProxy, [rawBatch]); - expectEvent(receipt, "PriceFeedUpdate", { - price: priceVal.toString(), - publishTime: (currentTimestamp - 5).toString(), - }); - expectEvent(receipt, "BatchPriceFeedUpdate"); - - let first_prod_id = "0x" + "01".repeat(32); - let first_price_id = "0x" + "fe".repeat(32); - let second_prod_id = "0x" + "02".repeat(32); - let second_price_id = "0x" + "fd".repeat(32); - - // Confirm that previously non-existent feeds are created - let first = await this.pythProxy.queryPriceFeed(first_price_id); - console.debug(`first is ${JSON.stringify(first)}`); - assert.equal(first.price.price, priceVal); - - let second = await this.pythProxy.queryPriceFeed(second_price_id); - assert.equal(second.price.price, priceVal); - - // Confirm the price is bumped after a new attestation updates each record - let nextTimestamp = currentTimestamp + 1; - let rawBatch2 = generateRawBatchAttestation( - nextTimestamp - 5, - nextTimestamp, - priceVal + 5 - ); - receipt = await updatePriceFeeds(this.pythProxy, [rawBatch2]); - expectEvent(receipt, "PriceFeedUpdate", { - price: (priceVal + 5).toString(), - publishTime: (nextTimestamp - 5).toString(), - }); - expectEvent(receipt, "BatchPriceFeedUpdate"); - - first = await this.pythProxy.queryPriceFeed(first_price_id); - assert.equal(first.price.price, priceVal + 5); - - second = await this.pythProxy.queryPriceFeed(second_price_id); - assert.equal(second.price.price, priceVal + 5); - - // Confirm that only strictly larger timestamps trigger updates - let rawBatch3 = generateRawBatchAttestation( - nextTimestamp - 5, - nextTimestamp, - priceVal + 10 - ); - receipt = await updatePriceFeeds(this.pythProxy, [rawBatch3]); - expectEvent.notEmitted(receipt, "PriceFeedUpdate"); - expectEvent(receipt, "BatchPriceFeedUpdate"); - - first = await this.pythProxy.queryPriceFeed(first_price_id); - assert.equal(first.price.price, priceVal + 5); - assert.notEqual(first.price.price, priceVal + 10); - - second = await this.pythProxy.queryPriceFeed(second_price_id); - assert.equal(second.price.price, priceVal + 5); - assert.notEqual(second.price.price, priceVal + 10); - }); - it("should fail transaction if a price is not found", async function () { await expectRevertCustomError( this.pythProxy.queryPriceFeed( @@ -459,44 +152,6 @@ contract("Pyth", function () { ); }); - it("should revert on getting stale current prices", async function () { - let smallestTimestamp = 1; - let rawBatch = generateRawBatchAttestation( - smallestTimestamp, - smallestTimestamp + 5, - 1337 - ); - await updatePriceFeeds(this.pythProxy, [rawBatch]); - - for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) { - const price_id = - "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - await expectRevertCustomError( - this.pythProxy.getPrice(price_id), - "StalePrice" - ); - } - }); - - it("should revert on getting current prices too far into the future as they are considered unknown", async function () { - let largestTimestamp = 4294967295; - let rawBatch = generateRawBatchAttestation( - largestTimestamp - 5, - largestTimestamp, - 1337 - ); - await updatePriceFeeds(this.pythProxy, [rawBatch]); - - for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) { - const price_id = - "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - await expectRevertCustomError( - this.pythProxy.getPrice(price_id), - "StalePrice" - ); - } - }); - /** * Set valid time period to `newValidPeriod` by creating and submitting a * governance instruction for it. @@ -516,85 +171,6 @@ contract("Pyth", function () { ); } - it("changing validity time works", async function () { - const latestTime = await time.latest(); - let rawBatch = generateRawBatchAttestation(latestTime, latestTime, 1337); - - await updatePriceFeeds(this.pythProxy, [rawBatch]); - - // Setting the validity time to 30 seconds - await setValidPeriodTo(this.pythProxy, 30, 1); - assert.equal(await this.pythProxy.validTimePeriodSeconds(), 30); - - // Then prices should be available - for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) { - const price_id = - "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - - // Expect getPrice to work (not revert) - await this.pythProxy.getPrice(price_id); - } - - // One minute passes - await time.increase(time.duration.minutes(1)); - - // The prices should become unavailable now. - for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) { - const price_id = - "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - - await expectRevertCustomError( - this.pythProxy.getPrice(price_id), - "StalePrice" - ); - } - - // Setting the validity time to 120 seconds - await setValidPeriodTo(this.pythProxy, 120, 2); - assert.equal(await this.pythProxy.validTimePeriodSeconds(), 120); - - // Then prices should be available because the valid period is now 120 seconds - for (var i = 1; i <= RAW_BATCH_ATTESTATION_COUNT; i++) { - const price_id = - "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - let priceFeedResult = await this.pythProxy.queryPriceFeed(price_id); - - // Expect getPrice to work (not revert) - await this.pythProxy.getPrice(price_id); - } - }); - - it("should use prev price and timestamp on unknown attestation status", async function () { - const latestTime = await time.latest(); - let rawBatch = generateRawUnknownBatchAttestation( - latestTime, - latestTime, - 1337, // price - 1500, // ema price - latestTime - 10, - 1000 // prev price - ); - - const receipt = await updatePriceFeeds(this.pythProxy, [rawBatch]); - expectEvent(receipt, "PriceFeedUpdate", { - price: "1000", - }); - - // Then prices should be available because the valid period is now 120 seconds - for (var i = 1; i <= RAW_UNKNOWN_BATCH_ATTESTATION_COUNT; i++) { - const price_id = - "0x" + (255 - (i % 256)).toString(16).padStart(2, "0").repeat(32); - - const price = await this.pythProxy.getPrice(price_id); - assert.equal(price.price, "1000"); - assert.equal(price.publishTime, (latestTime - 10).toString()); - - const emaPrice = await this.pythProxy.getEmaPrice(price_id); - assert.equal(emaPrice.price, "1500"); - assert.equal(emaPrice.publishTime, (latestTime - 10).toString()); - } - }); - // Governance // Logics that apply to all governance messages @@ -980,19 +556,7 @@ contract("Pyth", function () { ) ); - let rawBatch = generateRawBatchAttestation(100, 100, 1337); - await expectRevertCustomError( - updatePriceFeeds(this.pythProxy, [rawBatch]), - "InvalidUpdateDataSource" - ); - - await updatePriceFeeds( - this.pythProxy, - [rawBatch], - 0, - governance.CHAINS.acala, - "0x0000000000000000000000000000000000000000000000000000000000001111" - ); + // TODO: try to publish prices }); it("Setting fee should work", async function () { @@ -1019,13 +583,7 @@ contract("Pyth", function () { assert.equal(await this.pythProxy.singleUpdateFeeInWei(), "5000"); - let rawBatch = generateRawBatchAttestation(100, 100, 1337); - await expectRevertCustomError( - updatePriceFeeds(this.pythProxy, [rawBatch], 0), - "InsufficientFee" - ); - - await updatePriceFeeds(this.pythProxy, [rawBatch], 5000); + // TODO: check that fee is applied }); it("Setting valid period should work", async function () { From e607568c9c045ef863372e1b5d1306732d8a48d4 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Mon, 25 Mar 2024 11:52:19 +0000 Subject: [PATCH 02/10] refactor(target_chains/ethereum): remove legacy batch updates support from updatePriceFeeds --- .../contracts/contracts/pyth/Pyth.sol | 58 +--------- .../contracts/forge-test/GasBenchmark.t.sol | 101 ++++++------------ .../forge-test/VerificationExperiments.t.sol | 19 ++-- .../forge-test/utils/PythTestUtils.t.sol | 46 -------- 4 files changed, 48 insertions(+), 176 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index 6b08e38683..3c99fa4570 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol @@ -62,29 +62,14 @@ abstract contract Pyth is PythSetters.setSingleUpdateFeeInWei(singleUpdateFeeInWei); } - function updatePriceBatchFromVm(bytes calldata encodedVm) private { - parseAndProcessBatchPriceAttestation( - parseAndVerifyBatchAttestationVM(encodedVm) - ); - } - function updatePriceFeeds( bytes[] calldata updateData ) public payable override { uint totalNumUpdates = 0; for (uint i = 0; i < updateData.length; ) { - if ( - updateData[i].length > 4 && - UnsafeCalldataBytesLib.toUint32(updateData[i], 0) == - ACCUMULATOR_MAGIC - ) { - totalNumUpdates += updatePriceInfosFromAccumulatorUpdate( - updateData[i] - ); - } else { - updatePriceBatchFromVm(updateData[i]); - totalNumUpdates += 1; - } + totalNumUpdates += updatePriceInfosFromAccumulatorUpdate( + updateData[i] + ); unchecked { i++; @@ -138,43 +123,6 @@ abstract contract Pyth is return isValidDataSource(vm.emitterChainId, vm.emitterAddress); } - function parseAndProcessBatchPriceAttestation( - IWormhole.VM memory vm - ) internal { - // Most of the math operations below are simple additions. - // In the places that there is more complex operation there is - // a comment explaining why it is safe. Also, byteslib - // operations have proper require. - unchecked { - bytes memory encoded = vm.payload; - ( - uint index, - uint nAttestations, - uint attestationSize - ) = parseBatchAttestationHeader(encoded); - - // Deserialize each attestation - for (uint j = 0; j < nAttestations; j++) { - ( - PythInternalStructs.PriceInfo memory info, - bytes32 priceId - ) = parseSingleAttestationFromBatch( - encoded, - index, - attestationSize - ); - - // Respect specified attestation size for forward-compat - index += attestationSize; - - // Store the attestation - updateLatestPriceIfNecessary(priceId, info); - } - - emit BatchPriceFeedUpdate(vm.emitterChainId, vm.sequence); - } - } - function parseSingleAttestationFromBatch( bytes memory encoded, uint index, diff --git a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol index 7157858947..1041b13c60 100644 --- a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol +++ b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol @@ -32,21 +32,21 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { // Cached prices are populated in the setUp PythStructs.Price[] cachedPrices; - bytes[] cachedPricesWhBatchUpdateData; - uint cachedPricesWhBatchUpdateFee; uint64[] cachedPricesPublishTimes; bytes[][] cachedPricesWhMerkleUpdateData; // i th element contains the update data for the first i prices + bytes[] allCachedPricesWhMerkleUpdateData; // the update data for all prices uint[] cachedPricesWhMerkleUpdateFee; // i th element contains the update fee for the first i prices + uint allCachedPricesWhMerkleUpdateFee; // the update fee for all prices // Fresh prices are different prices that can be used // as a fresh price to update the prices PythStructs.Price[] freshPrices; - bytes[] freshPricesWhBatchUpdateData; - uint freshPricesWhBatchUpdateFee; uint64[] freshPricesPublishTimes; bytes[][] freshPricesWhMerkleUpdateData; // i th element contains the update data for the first i prices + bytes[] allFreshPricesWhMerkleUpdateData; // the update data for all prices uint[] freshPricesWhMerkleUpdateFee; // i th element contains the update fee for the first i prices + uint allFreshPricesWhMerkleUpdateFee; // the update fee for all prices uint64 sequence; uint randomSeed; @@ -104,19 +104,23 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { freshPricesWhMerkleUpdateData.push(updateData); freshPricesWhMerkleUpdateFee.push(updateFee); } + allCachedPricesWhMerkleUpdateData = cachedPricesWhMerkleUpdateData[ + NUM_PRICES - 1 + ]; + allCachedPricesWhMerkleUpdateFee = cachedPricesWhMerkleUpdateFee[ + NUM_PRICES - 1 + ]; + allFreshPricesWhMerkleUpdateData = freshPricesWhMerkleUpdateData[ + NUM_PRICES - 1 + ]; + allFreshPricesWhMerkleUpdateFee = freshPricesWhMerkleUpdateFee[ + NUM_PRICES - 1 + ]; + // Populate the contract with the initial prices - ( - cachedPricesWhBatchUpdateData, - cachedPricesWhBatchUpdateFee - ) = generateWhBatchUpdateDataAndFee(cachedPrices); - pyth.updatePriceFeeds{value: cachedPricesWhBatchUpdateFee}( - cachedPricesWhBatchUpdateData + pyth.updatePriceFeeds{value: allCachedPricesWhMerkleUpdateFee}( + allCachedPricesWhMerkleUpdateData ); - - ( - freshPricesWhBatchUpdateData, - freshPricesWhBatchUpdateFee - ) = generateWhBatchUpdateDataAndFee(freshPrices); } function getRand() internal returns (uint val) { @@ -124,23 +128,6 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { val = uint(keccak256(abi.encode(randomSeed))); } - function generateWhBatchUpdateDataAndFee( - PythStructs.Price[] memory prices - ) internal returns (bytes[] memory updateData, uint updateFee) { - bytes memory vaa = generateWhBatchUpdate( - pricesToPriceAttestations(priceIds, prices), - sequence, - NUM_GUARDIAN_SIGNERS - ); - - ++sequence; - - updateData = new bytes[](1); - updateData[0] = vaa; - - updateFee = pyth.getUpdateFee(updateData); - } - function generateWhMerkleUpdateDataAndFee( PythStructs.Price[] memory prices ) internal returns (bytes[] memory updateData, uint updateFee) { @@ -155,18 +142,6 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { updateFee = pyth.getUpdateFee(updateData); } - function testBenchmarkUpdatePriceFeedsWhBatchFresh() public { - pyth.updatePriceFeeds{value: freshPricesWhBatchUpdateFee}( - freshPricesWhBatchUpdateData - ); - } - - function testBenchmarkUpdatePriceFeedsWhBatchNotFresh() public { - pyth.updatePriceFeeds{value: cachedPricesWhBatchUpdateFee}( - cachedPricesWhBatchUpdateData - ); - } - function testBenchmarkUpdatePriceFeedsWhMerkle1FeedFresh() public { pyth.updatePriceFeeds{value: freshPricesWhMerkleUpdateFee[0]}( freshPricesWhMerkleUpdateData[0] @@ -227,23 +202,23 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { ); } - function testBenchmarkUpdatePriceFeedsIfNecessaryWhBatchFresh() public { + function testBenchmarkUpdatePriceFeedsIfNecessaryWhMerkleFresh() public { // Since the prices have advanced, the publishTimes are newer than one in // the contract and hence, the call should succeed. - pyth.updatePriceFeedsIfNecessary{value: freshPricesWhBatchUpdateFee}( - freshPricesWhBatchUpdateData, - priceIds, - freshPricesPublishTimes - ); + pyth.updatePriceFeedsIfNecessary{ + value: allFreshPricesWhMerkleUpdateFee + }(allFreshPricesWhMerkleUpdateData, priceIds, freshPricesPublishTimes); } - function testBenchmarkUpdatePriceFeedsIfNecessaryWhBatchNotFresh() public { + function testBenchmarkUpdatePriceFeedsIfNecessaryWhMerkleNotFresh() public { // Since the price is not advanced, the publishTimes are the same as the // ones in the contract. vm.expectRevert(PythErrors.NoFreshUpdate.selector); - pyth.updatePriceFeedsIfNecessary{value: cachedPricesWhBatchUpdateFee}( - cachedPricesWhBatchUpdateData, + pyth.updatePriceFeedsIfNecessary{ + value: allCachedPricesWhMerkleUpdateFee + }( + allCachedPricesWhMerkleUpdateData, priceIds, cachedPricesPublishTimes ); @@ -253,8 +228,8 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { bytes32[] memory ids = new bytes32[](1); ids[0] = priceIds[0]; - pyth.parsePriceFeedUpdates{value: freshPricesWhBatchUpdateFee}( - freshPricesWhBatchUpdateData, + pyth.parsePriceFeedUpdates{value: allFreshPricesWhMerkleUpdateFee}( + allFreshPricesWhMerkleUpdateData, ids, 0, 50 @@ -266,8 +241,8 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { ids[0] = priceIds[0]; ids[1] = priceIds[1]; - pyth.parsePriceFeedUpdates{value: freshPricesWhBatchUpdateFee}( - freshPricesWhBatchUpdateData, + pyth.parsePriceFeedUpdates{value: allFreshPricesWhMerkleUpdateFee}( + allFreshPricesWhMerkleUpdateData, ids, 0, 50 @@ -403,8 +378,8 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { ids[0] = priceIds[0]; vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); - pyth.parsePriceFeedUpdates{value: freshPricesWhBatchUpdateFee}( - freshPricesWhBatchUpdateData, + pyth.parsePriceFeedUpdates{value: allFreshPricesWhMerkleUpdateFee}( + allFreshPricesWhMerkleUpdateData, ids, 50, 100 @@ -427,10 +402,6 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { pyth.getEmaPrice(priceIds[0]); } - function testBenchmarkGetUpdateFeeWhBatch() public view { - pyth.getUpdateFee(freshPricesWhBatchUpdateData); - } - function testBenchmarkGetUpdateFeeWhMerkle1() public view { pyth.getUpdateFee(freshPricesWhMerkleUpdateData[0]); } @@ -450,8 +421,4 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { function testBenchmarkGetUpdateFeeWhMerkle5() public view { pyth.getUpdateFee(freshPricesWhMerkleUpdateData[4]); } - - function testBenchmarkWormholeParseAndVerifyVMBatchAttestation() public { - wormhole.parseAndVerifyVM(freshPricesWhBatchUpdateData[0]); - } } diff --git a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol index 866d5d54fd..07940d95a5 100644 --- a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol +++ b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol @@ -35,6 +35,9 @@ contract VerificationExperiments is // Private key for the threshold signature uint256 THRESHOLD_KEY = 1234; + // We will have less than 512 price for a foreseeable future. + uint8 constant MERKLE_TREE_DEPTH = 9; + PythExperimental public pyth; bytes32[] priceIds; @@ -106,7 +109,7 @@ contract VerificationExperiments is setRandSeed(12345); for (uint i = 0; i < NUM_PRICES; ++i) { - uint64 publishTime = uint64(getRandUint() % 10); + uint64 publishTime = uint64(getRandUint() % 10) + 1; // to make sure prevPublishTime is >= 0 cachedPrices.push( PythStructs.Price( @@ -147,17 +150,17 @@ contract VerificationExperiments is // Generate the update payloads for the various verification systems - whMerkleUpdateDepth0 = generateWhMerkleUpdate( + whMerkleUpdateDepth0 = generateSingleWhMerkleUpdate( priceIds[0], freshPrices[0], 0 ); - whMerkleUpdateDepth1 = generateWhMerkleUpdate( + whMerkleUpdateDepth1 = generateSingleWhMerkleUpdate( priceIds[0], freshPrices[0], 1 ); - whMerkleUpdateDepth8 = generateWhMerkleUpdate( + whMerkleUpdateDepth8 = generateSingleWhMerkleUpdate( priceIds[0], freshPrices[0], 8 @@ -188,9 +191,9 @@ contract VerificationExperiments is function generateWormholeUpdateDataAndFee( PythStructs.Price[] memory prices ) internal returns (bytes[] memory updateData, uint updateFee) { - bytes memory vaa = generateWhBatchUpdate( - pricesToPriceAttestations(priceIds, prices), - sequence, + bytes memory vaa = generateWhMerkleUpdate( + pricesToPriceFeedMessages(priceIds, prices), + MERKLE_TREE_DEPTH, NUM_GUARDIAN_SIGNERS ); @@ -252,7 +255,7 @@ contract VerificationExperiments is } // Generate a wormhole-attested merkle proof with the given depth. - function generateWhMerkleUpdate( + function generateSingleWhMerkleUpdate( bytes32 priceId, PythStructs.Price memory price, uint depth diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index ade8d173a4..9fd2940fac 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -362,49 +362,3 @@ abstract contract PythTestUtils is Test, WormholeTestUtils { } } } - -contract PythTestUtilsTest is - Test, - WormholeTestUtils, - PythTestUtils, - IPythEvents -{ - function testGenerateWhBatchUpdateWorks() public { - IPyth pyth = IPyth( - setUpPyth( - setUpWormholeReceiver( - 1 // Number of guardians - ) - ) - ); - - bytes32[] memory priceIds = new bytes32[](1); - priceIds[ - 0 - ] = 0x0000000000000000000000000000000000000000000000000000000000000222; - - PythStructs.Price[] memory prices = new PythStructs.Price[](1); - prices[0] = PythStructs.Price( - 100, // Price - 10, // Confidence - -5, // Exponent - 1 // Publish time - ); - - bytes memory vaa = generateWhBatchUpdate( - pricesToPriceAttestations(priceIds, prices), - 1, // Sequence - 1 // No. Signers - ); - - bytes[] memory updateData = new bytes[](1); - updateData[0] = vaa; - - uint updateFee = pyth.getUpdateFee(updateData); - - vm.expectEmit(true, false, false, true); - emit PriceFeedUpdate(priceIds[0], 1, 100, 10); - - pyth.updatePriceFeeds{value: updateFee}(updateData); - } -} From 7b792121dbc933c9c94abe073c1dcf9ad2f359b6 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Tue, 26 Mar 2024 11:34:58 +0000 Subject: [PATCH 03/10] refactor(target_chains/ethereum): remove legacy batch updates support from parsePriceFeedUpdates and getUpdateFee --- .../contracts/contracts/pyth/Pyth.sol | 77 +----- .../Pyth.WormholeMerkleAccumulator.t.sol | 3 +- .../ethereum/contracts/forge-test/Pyth.t.sol | 240 +++++++----------- .../forge-test/VerificationExperiments.t.sol | 7 +- .../forge-test/utils/PythTestUtils.t.sol | 49 +++- 5 files changed, 136 insertions(+), 240 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index 3c99fa4570..7a6292846c 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/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(); } } diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol index ac87170be9..e8acda2604 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol +++ b/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; diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index b811ed1ef0..c2837c6eda 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/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] } diff --git a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol index 07940d95a5..e97edc2890 100644 --- a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol +++ b/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; diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index 9fd2940fac..7d8a340a2c 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/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, From 9d0957f40574e770992b377017b87b9a761c1ce5 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Tue, 26 Mar 2024 11:32:47 +0000 Subject: [PATCH 04/10] refactor(target_chains/ethereum): move unused functions from Pyth to VerificationExperiments tests --- .../contracts/contracts/pyth/Pyth.sol | 133 ------------------ .../forge-test/VerificationExperiments.t.sol | 133 ++++++++++++++++++ 2 files changed, 133 insertions(+), 133 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index 7a6292846c..53b3dcd308 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol @@ -117,127 +117,6 @@ abstract contract Pyth is return getTotalFee(totalNumUpdates); } - function verifyPythVM( - IWormhole.VM memory vm - ) private view returns (bool valid) { - return isValidDataSource(vm.emitterChainId, vm.emitterAddress); - } - - function parseSingleAttestationFromBatch( - bytes memory encoded, - uint index, - uint attestationSize - ) - internal - pure - returns (PythInternalStructs.PriceInfo memory info, bytes32 priceId) - { - unchecked { - // 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; - - priceId = UnsafeBytesLib.toBytes32( - encoded, - index + attestationIndex - ); - attestationIndex += 32; - - info.price = int64( - UnsafeBytesLib.toUint64(encoded, index + attestationIndex) - ); - attestationIndex += 8; - - info.conf = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - - info.expo = int32( - UnsafeBytesLib.toUint32(encoded, index + attestationIndex) - ); - attestationIndex += 4; - - info.emaPrice = int64( - UnsafeBytesLib.toUint64(encoded, index + attestationIndex) - ); - attestationIndex += 8; - - info.emaConf = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - - { - // Status is an enum (encoded as uint8) with the following values: - // 0 = UNKNOWN: The price feed is not currently updating for an unknown reason. - // 1 = TRADING: The price feed is updating as expected. - // 2 = HALTED: The price feed is not currently updating because trading in the product has been halted. - // 3 = AUCTION: The price feed is not currently updating because an auction is setting the price. - uint8 status = UnsafeBytesLib.toUint8( - encoded, - index + attestationIndex - ); - attestationIndex += 1; - - // Unused uint32 numPublishers - attestationIndex += 4; - - // Unused uint32 numPublishers - attestationIndex += 4; - - // Unused uint64 attestationTime - attestationIndex += 8; - - info.publishTime = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - - if (status == 1) { - // status == TRADING - attestationIndex += 24; - } else { - // If status is not trading then the latest available price is - // the previous price info that are passed here. - - // Previous publish time - info.publishTime = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - - // Previous price - info.price = int64( - UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ) - ); - attestationIndex += 8; - - // Previous confidence - info.conf = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - } - } - - if (attestationIndex > attestationSize) - revert PythErrors.InvalidUpdateData(); - } - } - // This is an overwrite of the same method in AbstractPyth.sol // to be more gas efficient. function updatePriceFeedsIfNecessary( @@ -366,18 +245,6 @@ abstract contract Pyth is } } - function parseAndVerifyBatchAttestationVM( - bytes calldata encodedVm - ) internal view returns (IWormhole.VM memory vm) { - { - bool valid; - (vm, valid, ) = wormhole().parseAndVerifyVM(encodedVm); - if (!valid) revert PythErrors.InvalidWormholeVaa(); - } - - if (!verifyPythVM(vm)) revert PythErrors.InvalidUpdateDataSource(); - } - function parsePriceFeedUpdatesInternal( bytes[] calldata updateData, bytes32[] calldata priceIds, diff --git a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol index e97edc2890..ffd4d38738 100644 --- a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol +++ b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol @@ -429,6 +429,24 @@ contract PythExperimental is Pyth { updateLatestPriceIfNecessary(priceId, info); } + function parseAndVerifyBatchAttestationVM( + bytes calldata encodedVm + ) internal view returns (IWormhole.VM memory vm) { + { + bool valid; + (vm, valid, ) = wormhole().parseAndVerifyVM(encodedVm); + if (!valid) revert PythErrors.InvalidWormholeVaa(); + } + + if (!verifyPythVM(vm)) revert PythErrors.InvalidUpdateDataSource(); + } + + function verifyPythVM( + IWormhole.VM memory vm + ) private view returns (bool valid) { + return isValidDataSource(vm.emitterChainId, vm.emitterAddress); + } + // Update a single price feed via a threshold-signed merkle proof. // data is expected to be a serialized PriceAttestation function updatePriceFeedsThresholdMerkle( @@ -481,6 +499,121 @@ contract PythExperimental is Pyth { updateLatestPriceIfNecessary(priceId, info); } + function parseSingleAttestationFromBatch( + bytes memory encoded, + uint index, + uint attestationSize + ) + internal + pure + returns (PythInternalStructs.PriceInfo memory info, bytes32 priceId) + { + unchecked { + // 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; + + priceId = UnsafeBytesLib.toBytes32( + encoded, + index + attestationIndex + ); + attestationIndex += 32; + + info.price = int64( + UnsafeBytesLib.toUint64(encoded, index + attestationIndex) + ); + attestationIndex += 8; + + info.conf = UnsafeBytesLib.toUint64( + encoded, + index + attestationIndex + ); + attestationIndex += 8; + + info.expo = int32( + UnsafeBytesLib.toUint32(encoded, index + attestationIndex) + ); + attestationIndex += 4; + + info.emaPrice = int64( + UnsafeBytesLib.toUint64(encoded, index + attestationIndex) + ); + attestationIndex += 8; + + info.emaConf = UnsafeBytesLib.toUint64( + encoded, + index + attestationIndex + ); + attestationIndex += 8; + + { + // Status is an enum (encoded as uint8) with the following values: + // 0 = UNKNOWN: The price feed is not currently updating for an unknown reason. + // 1 = TRADING: The price feed is updating as expected. + // 2 = HALTED: The price feed is not currently updating because trading in the product has been halted. + // 3 = AUCTION: The price feed is not currently updating because an auction is setting the price. + uint8 status = UnsafeBytesLib.toUint8( + encoded, + index + attestationIndex + ); + attestationIndex += 1; + + // Unused uint32 numPublishers + attestationIndex += 4; + + // Unused uint32 numPublishers + attestationIndex += 4; + + // Unused uint64 attestationTime + attestationIndex += 8; + + info.publishTime = UnsafeBytesLib.toUint64( + encoded, + index + attestationIndex + ); + attestationIndex += 8; + + if (status == 1) { + // status == TRADING + attestationIndex += 24; + } else { + // If status is not trading then the latest available price is + // the previous price info that are passed here. + + // Previous publish time + info.publishTime = UnsafeBytesLib.toUint64( + encoded, + index + attestationIndex + ); + attestationIndex += 8; + + // Previous price + info.price = int64( + UnsafeBytesLib.toUint64( + encoded, + index + attestationIndex + ) + ); + attestationIndex += 8; + + // Previous confidence + info.conf = UnsafeBytesLib.toUint64( + encoded, + index + attestationIndex + ); + attestationIndex += 8; + } + } + + if (attestationIndex > attestationSize) + revert PythErrors.InvalidUpdateData(); + } + } + // Verify that signature is a valid ECDSA signature of messageHash by signer. function verifySignature( bytes32 messageHash, From f3e247b9a37d9ecf67d0dee2bcc9e240e7821507 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Wed, 27 Mar 2024 17:19:47 +0000 Subject: [PATCH 05/10] refactor(target_chains/ethereum): clean up unused batch update code --- .../contracts/contracts/pyth/Pyth.sol | 70 ------------------- .../forge-test/utils/PythTestUtils.t.sol | 20 ------ .../ethereum/sdk/solidity/IPythEvents.sol | 5 -- .../ethereum/sdk/solidity/MockPyth.sol | 11 --- .../sdk/solidity/abis/AbstractPyth.json | 19 ----- .../ethereum/sdk/solidity/abis/IPyth.json | 19 ----- .../sdk/solidity/abis/IPythEvents.json | 19 ----- .../ethereum/sdk/solidity/abis/MockPyth.json | 19 ----- 8 files changed, 182 deletions(-) diff --git a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol index 53b3dcd308..3c17bb0ab7 100644 --- a/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol +++ b/target_chains/ethereum/contracts/contracts/pyth/Pyth.sol @@ -175,76 +175,6 @@ abstract contract Pyth is if (price.publishTime == 0) revert PythErrors.PriceFeedNotFound(); } - function parseBatchAttestationHeader( - bytes memory encoded - ) - internal - pure - returns (uint index, uint nAttestations, uint attestationSize) - { - unchecked { - index = 0; - - // Check header - { - uint32 magic = UnsafeBytesLib.toUint32(encoded, index); - index += 4; - if (magic != 0x50325748) revert PythErrors.InvalidUpdateData(); - - uint16 versionMajor = UnsafeBytesLib.toUint16(encoded, index); - index += 2; - if (versionMajor != 3) revert PythErrors.InvalidUpdateData(); - - // This value is only used as the check below which currently - // never reverts - // uint16 versionMinor = UnsafeBytesLib.toUint16(encoded, index); - index += 2; - - // This check is always false as versionMinor is 0, so it is commented. - // in the future that the minor version increases this will have effect. - // if(versionMinor < 0) revert InvalidUpdateData(); - - uint16 hdrSize = UnsafeBytesLib.toUint16(encoded, index); - index += 2; - - // NOTE(2022-04-19): Currently, only payloadId comes after - // hdrSize. Future extra header fields must be read using a - // separate offset to respect hdrSize, i.e.: - // - // uint hdrIndex = 0; - // bpa.header.payloadId = UnsafeBytesLib.toUint8(encoded, index + hdrIndex); - // hdrIndex += 1; - // - // bpa.header.someNewField = UnsafeBytesLib.toUint32(encoded, index + hdrIndex); - // hdrIndex += 4; - // - // // Skip remaining unknown header bytes - // index += bpa.header.hdrSize; - - uint8 payloadId = UnsafeBytesLib.toUint8(encoded, index); - - // Skip remaining unknown header bytes - index += hdrSize; - - // Payload ID of 2 required for batch headerBa - if (payloadId != 2) revert PythErrors.InvalidUpdateData(); - } - - // Parse the number of attestations - nAttestations = UnsafeBytesLib.toUint16(encoded, index); - index += 2; - - // Parse the attestation size - attestationSize = UnsafeBytesLib.toUint16(encoded, index); - index += 2; - - // Given the message is valid the arithmetic below should not overflow, and - // even if it overflows then the require would fail. - if (encoded.length != (index + (attestationSize * nAttestations))) - revert PythErrors.InvalidUpdateData(); - } - } - function parsePriceFeedUpdatesInternal( bytes[] calldata updateData, bytes32[] calldata priceIds, diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index 7d8a340a2c..752156e4d7 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -324,26 +324,6 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { ); } - // Generates a VAA for the given attestations. - // This method calls generatePriceFeedUpdatePayload and then creates a VAA with it. - // The VAAs generated from this method use block timestamp as their timestamp. - function generateWhBatchUpdate( - PriceAttestation[] memory attestations, - uint64 sequence, - uint8 numSigners - ) public returns (bytes memory vaa) { - bytes memory payload = generatePriceFeedUpdatePayload(attestations); - - vaa = generateVaa( - uint32(block.timestamp), - SOURCE_EMITTER_CHAIN_ID, - SOURCE_EMITTER_ADDRESS, - sequence, - payload, - numSigners - ); - } - function pricesToPriceAttestations( bytes32[] memory priceIds, PythStructs.Price[] memory prices diff --git a/target_chains/ethereum/sdk/solidity/IPythEvents.sol b/target_chains/ethereum/sdk/solidity/IPythEvents.sol index ac4c05cec0..a293776162 100644 --- a/target_chains/ethereum/sdk/solidity/IPythEvents.sol +++ b/target_chains/ethereum/sdk/solidity/IPythEvents.sol @@ -15,9 +15,4 @@ interface IPythEvents { int64 price, uint64 conf ); - - /// @dev Emitted when a batch price update is processed successfully. - /// @param chainId ID of the source chain that the batch price update comes from. - /// @param sequenceNumber Sequence number of the batch price update. - event BatchPriceFeedUpdate(uint16 chainId, uint64 sequenceNumber); } diff --git a/target_chains/ethereum/sdk/solidity/MockPyth.sol b/target_chains/ethereum/sdk/solidity/MockPyth.sol index c0b117477b..7c0217028a 100644 --- a/target_chains/ethereum/sdk/solidity/MockPyth.sol +++ b/target_chains/ethereum/sdk/solidity/MockPyth.sol @@ -7,7 +7,6 @@ import "./PythErrors.sol"; contract MockPyth is AbstractPyth { mapping(bytes32 => PythStructs.PriceFeed) priceFeeds; - uint64 sequenceNumber; uint singleUpdateFeeInWei; uint validTimePeriod; @@ -41,10 +40,6 @@ contract MockPyth is AbstractPyth { uint requiredFee = getUpdateFee(updateData); if (msg.value < requiredFee) revert PythErrors.InsufficientFee(); - // Chain ID is id of the source chain that the price update comes from. Since it is just a mock contract - // We set it to 1. - uint16 chainId = 1; - for (uint i = 0; i < updateData.length; i++) { PythStructs.PriceFeed memory priceFeed = abi.decode( updateData[i], @@ -64,12 +59,6 @@ contract MockPyth is AbstractPyth { ); } } - - // In the real contract, the input of this function contains multiple batches that each contain multiple prices. - // This event is emitted when a batch is processed. In this mock contract we consider there is only one batch of prices. - // Each batch has (chainId, sequenceNumber) as it's unique identifier. Here chainId is set to 1 and an increasing sequence number is used. - emit BatchPriceFeedUpdate(chainId, sequenceNumber); - sequenceNumber += 1; } function getUpdateFee( diff --git a/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json b/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json index be0f0f40f2..7b6724065f 100644 --- a/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json +++ b/target_chains/ethereum/sdk/solidity/abis/AbstractPyth.json @@ -14,25 +14,6 @@ "name": "StalePrice", "type": "error" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint16", - "name": "chainId", - "type": "uint16" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "sequenceNumber", - "type": "uint64" - } - ], - "name": "BatchPriceFeedUpdate", - "type": "event" - }, { "anonymous": false, "inputs": [ diff --git a/target_chains/ethereum/sdk/solidity/abis/IPyth.json b/target_chains/ethereum/sdk/solidity/abis/IPyth.json index 8932f3a676..28841e1def 100644 --- a/target_chains/ethereum/sdk/solidity/abis/IPyth.json +++ b/target_chains/ethereum/sdk/solidity/abis/IPyth.json @@ -1,23 +1,4 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint16", - "name": "chainId", - "type": "uint16" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "sequenceNumber", - "type": "uint64" - } - ], - "name": "BatchPriceFeedUpdate", - "type": "event" - }, { "anonymous": false, "inputs": [ diff --git a/target_chains/ethereum/sdk/solidity/abis/IPythEvents.json b/target_chains/ethereum/sdk/solidity/abis/IPythEvents.json index b9451f8ab4..1a89732089 100644 --- a/target_chains/ethereum/sdk/solidity/abis/IPythEvents.json +++ b/target_chains/ethereum/sdk/solidity/abis/IPythEvents.json @@ -1,23 +1,4 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint16", - "name": "chainId", - "type": "uint16" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "sequenceNumber", - "type": "uint64" - } - ], - "name": "BatchPriceFeedUpdate", - "type": "event" - }, { "anonymous": false, "inputs": [ diff --git a/target_chains/ethereum/sdk/solidity/abis/MockPyth.json b/target_chains/ethereum/sdk/solidity/abis/MockPyth.json index 71f90d2252..a917b56a28 100644 --- a/target_chains/ethereum/sdk/solidity/abis/MockPyth.json +++ b/target_chains/ethereum/sdk/solidity/abis/MockPyth.json @@ -45,25 +45,6 @@ "name": "StalePrice", "type": "error" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint16", - "name": "chainId", - "type": "uint16" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "sequenceNumber", - "type": "uint64" - } - ], - "name": "BatchPriceFeedUpdate", - "type": "event" - }, { "anonymous": false, "inputs": [ From 4dbb0278f06f87f9ecea618646e0c3cf7a46af49 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Wed, 27 Mar 2024 17:20:03 +0000 Subject: [PATCH 06/10] refactor(target_chains/ethereum): rename attestations to messages in PythTest --- .../ethereum/contracts/forge-test/Pyth.t.sol | 275 +++++++++--------- 1 file changed, 130 insertions(+), 145 deletions(-) diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index c2837c6eda..41e995685c 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol @@ -29,57 +29,55 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { pyth = IPyth(setUpPyth(setUpWormholeReceiver(NUM_GUARDIAN_SIGNERS))); } - function generateRandomPriceAttestations( + function generateRandomPriceMessages( uint length ) internal - returns ( - bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) + returns (bytes32[] memory priceIds, PriceFeedMessage[] memory messages) { - attestations = new PriceFeedMessage[](length); + messages = new PriceFeedMessage[](length); priceIds = new bytes32[](length); for (uint i = 0; i < length; i++) { - 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].publishTime = getRandUint64(); - attestations[i].prevPublishTime = getRandUint64(); - - priceIds[i] = attestations[i].priceId; + messages[i].priceId = bytes32(i + 1); // price ids should be non-zero and unique + messages[i].price = getRandInt64(); + messages[i].conf = getRandUint64(); + messages[i].expo = getRandInt32(); + messages[i].emaPrice = getRandInt64(); + messages[i].emaConf = getRandUint64(); + messages[i].publishTime = getRandUint64(); + messages[i].prevPublishTime = getRandUint64(); + + priceIds[i] = messages[i].priceId; } } - // This method divides attestations into a couple of batches and creates + // This method divides messages into a couple of batches and creates // updateData for them. It returns the updateData and the updateFee - function createBatchedUpdateDataFromAttestationsWithConfig( - PriceFeedMessage[] memory attestations, + function createBatchedUpdateDataFromMessagesWithConfig( + PriceFeedMessage[] memory messages, MerkleUpdateConfig memory config ) internal returns (bytes[] memory updateData, uint updateFee) { - uint batchSize = 1 + (getRandUint() % attestations.length); - uint numBatches = (attestations.length + batchSize - 1) / batchSize; + uint batchSize = 1 + (getRandUint() % messages.length); + uint numBatches = (messages.length + batchSize - 1) / batchSize; updateData = new bytes[](numBatches); - for (uint i = 0; i < attestations.length; i += batchSize) { + for (uint i = 0; i < messages.length; i += batchSize) { uint len = batchSize; - if (attestations.length - i < len) { - len = attestations.length - i; + if (messages.length - i < len) { + len = messages.length - i; } - PriceFeedMessage[] - memory batchAttestations = new PriceFeedMessage[](len); + PriceFeedMessage[] memory batchMessages = new PriceFeedMessage[]( + len + ); for (uint j = i; j < i + len; j++) { - batchAttestations[j - i] = attestations[j]; + batchMessages[j - i] = messages[j]; } updateData[i / batchSize] = generateWhMerkleUpdateWithSource( - batchAttestations, + batchMessages, config ); } @@ -87,14 +85,11 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { updateFee = pyth.getUpdateFee(updateData); } - function createBatchedUpdateDataFromAttestations( - PriceFeedMessage[] memory attestations + function createBatchedUpdateDataFromMessages( + PriceFeedMessage[] memory messages ) internal returns (bytes[] memory updateData, uint updateFee) { - ( - updateData, - updateFee - ) = createBatchedUpdateDataFromAttestationsWithConfig( - attestations, + (updateData, updateFee) = createBatchedUpdateDataFromMessagesWithConfig( + messages, MerkleUpdateConfig( MERKLE_TREE_DEPTH, NUM_GUARDIAN_SIGNERS, @@ -108,35 +103,32 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { /// Testing parsePriceFeedUpdates method. function testParsePriceFeedUpdatesWorks(uint seed) public { setRandSeed(seed); - uint numAttestations = 1 + (getRandUint() % 10); + uint numMessages = 1 + (getRandUint() % 10); ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{ value: updateFee }(updateData, priceIds, 0, MAX_UINT64); - for (uint i = 0; i < numAttestations; i++) { + for (uint i = 0; i < numMessages; i++) { assertEq(priceFeeds[i].id, priceIds[i]); - assertEq(priceFeeds[i].price.price, attestations[i].price); - assertEq(priceFeeds[i].price.conf, attestations[i].conf); - assertEq(priceFeeds[i].price.expo, attestations[i].expo); - assertEq( - priceFeeds[i].price.publishTime, - attestations[i].publishTime - ); - 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].price.price, messages[i].price); + assertEq(priceFeeds[i].price.conf, messages[i].conf); + assertEq(priceFeeds[i].price.expo, messages[i].expo); + assertEq(priceFeeds[i].price.publishTime, messages[i].publishTime); + assertEq(priceFeeds[i].emaPrice.price, messages[i].emaPrice); + assertEq(priceFeeds[i].emaPrice.conf, messages[i].emaConf); + assertEq(priceFeeds[i].emaPrice.expo, messages[i].expo); assertEq( priceFeeds[i].emaPrice.publishTime, - attestations[i].publishTime + messages[i].publishTime ); } } @@ -145,23 +137,23 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { uint seed ) public { setRandSeed(seed); - uint numAttestations = 1 + (getRandUint() % 30); + uint numMessages = 1 + (getRandUint() % 30); ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); - // Shuffle the attestations - for (uint i = 1; i < numAttestations; i++) { + // Shuffle the messages + for (uint i = 1; i < numMessages; i++) { uint swapWith = getRandUint() % (i + 1); - (attestations[i], attestations[swapWith]) = ( - attestations[swapWith], - attestations[i] + (messages[i], messages[swapWith]) = ( + messages[swapWith], + messages[i] ); (priceIds[i], priceIds[swapWith]) = ( priceIds[swapWith], @@ -169,48 +161,43 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { ); } - // Select only first numSelectedAttestations. numSelectedAttestations will be in [0, numAttestations] - uint numSelectedAttestations = getRandUint() % (numAttestations + 1); + // Select only first numSelectedMessages. numSelectedMessages will be in [0, numMessages] + uint numSelectedMessages = getRandUint() % (numMessages + 1); - PriceFeedMessage[] memory selectedAttestations = new PriceFeedMessage[]( - numSelectedAttestations - ); - bytes32[] memory selectedPriceIds = new bytes32[]( - numSelectedAttestations + PriceFeedMessage[] memory selectedMessages = new PriceFeedMessage[]( + numSelectedMessages ); + bytes32[] memory selectedPriceIds = new bytes32[](numSelectedMessages); - for (uint i = 0; i < numSelectedAttestations; i++) { - selectedAttestations[i] = attestations[i]; + for (uint i = 0; i < numSelectedMessages; i++) { + selectedMessages[i] = messages[i]; selectedPriceIds[i] = priceIds[i]; } - // Only parse selected attestations + // Only parse selected messages PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{ value: updateFee }(updateData, selectedPriceIds, 0, MAX_UINT64); - for (uint i = 0; i < numSelectedAttestations; i++) { + for (uint i = 0; i < numSelectedMessages; i++) { assertEq(priceFeeds[i].id, selectedPriceIds[i]); - assertEq(priceFeeds[i].price.expo, selectedAttestations[i].expo); + assertEq(priceFeeds[i].price.expo, selectedMessages[i].expo); assertEq( priceFeeds[i].emaPrice.price, - selectedAttestations[i].emaPrice + selectedMessages[i].emaPrice ); - assertEq( - priceFeeds[i].emaPrice.conf, - selectedAttestations[i].emaConf - ); - assertEq(priceFeeds[i].emaPrice.expo, selectedAttestations[i].expo); + assertEq(priceFeeds[i].emaPrice.conf, selectedMessages[i].emaConf); + assertEq(priceFeeds[i].emaPrice.expo, selectedMessages[i].expo); - assertEq(priceFeeds[i].price.price, selectedAttestations[i].price); - assertEq(priceFeeds[i].price.conf, selectedAttestations[i].conf); + assertEq(priceFeeds[i].price.price, selectedMessages[i].price); + assertEq(priceFeeds[i].price.conf, selectedMessages[i].conf); assertEq( priceFeeds[i].price.publishTime, - selectedAttestations[i].publishTime + selectedMessages[i].publishTime ); assertEq( priceFeeds[i].emaPrice.publishTime, - selectedAttestations[i].publishTime + selectedMessages[i].publishTime ); } } @@ -218,20 +205,20 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { function testParsePriceFeedUpdatesWorksWithOverlappingWithinTimeRangeUpdates() public { - PriceFeedMessage[] memory attestations = new PriceFeedMessage[](2); + PriceFeedMessage[] memory messages = new PriceFeedMessage[](2); - attestations[0].priceId = bytes32(uint(1)); - attestations[0].price = 1000; - attestations[0].publishTime = 10; + messages[0].priceId = bytes32(uint(1)); + messages[0].price = 1000; + messages[0].publishTime = 10; - attestations[1].priceId = bytes32(uint(1)); - attestations[1].price = 2000; - attestations[1].publishTime = 20; + messages[1].priceId = bytes32(uint(1)); + messages[1].price = 2000; + messages[1].publishTime = 20; ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); bytes32[] memory priceIds = new bytes32[](1); priceIds[0] = bytes32(uint(1)); @@ -254,20 +241,20 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { function testParsePriceFeedUpdatesWorksWithOverlappingMixedTimeRangeUpdates() public { - PriceFeedMessage[] memory attestations = new PriceFeedMessage[](2); + PriceFeedMessage[] memory messages = new PriceFeedMessage[](2); - attestations[0].priceId = bytes32(uint(1)); - attestations[0].price = 1000; - attestations[0].publishTime = 10; + messages[0].priceId = bytes32(uint(1)); + messages[0].price = 1000; + messages[0].publishTime = 10; - attestations[1].priceId = bytes32(uint(1)); - attestations[1].price = 2000; - attestations[1].publishTime = 20; + messages[1].priceId = bytes32(uint(1)); + messages[1].price = 2000; + messages[1].publishTime = 20; ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); bytes32[] memory priceIds = new bytes32[](1); priceIds[0] = bytes32(uint(1)); @@ -295,18 +282,18 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { } function testParsePriceFeedUpdatesRevertsIfUpdateFeeIsNotPaid() public { - uint numAttestations = 10; + uint numMessages = 10; ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); - // Since attestations are not empty the fee should be at least 1 + // Since messages are not empty the fee should be at least 1 assertGe(updateFee, 1); vm.expectRevert(PythErrors.InsufficientFee.selector); @@ -323,17 +310,17 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { uint seed ) public { setRandSeed(seed); - uint numAttestations = 1 + (getRandUint() % 10); + uint numMessages = 1 + (getRandUint() % 10); ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestationsWithConfig( - attestations, + ) = createBatchedUpdateDataFromMessagesWithConfig( + messages, MerkleUpdateConfig( MERKLE_TREE_DEPTH, NUM_GUARDIAN_SIGNERS, @@ -356,17 +343,17 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { function testParsePriceFeedUpdatesRevertsIfUpdateSourceChainIsInvalid() public { - uint numAttestations = 10; + uint numMessages = 10; ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestationsWithConfig( - attestations, + ) = createBatchedUpdateDataFromMessagesWithConfig( + messages, MerkleUpdateConfig( MERKLE_TREE_DEPTH, NUM_GUARDIAN_SIGNERS, @@ -388,14 +375,14 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { function testParsePriceFeedUpdatesRevertsIfUpdateSourceAddressIsInvalid() public { - uint numAttestations = 10; + uint numMessages = 10; ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); - (bytes[] memory updateData, uint updateFee) = createBatchedUpdateDataFromAttestationsWithConfig( - attestations, + (bytes[] memory updateData, uint updateFee) = createBatchedUpdateDataFromMessagesWithConfig( + messages, MerkleUpdateConfig( MERKLE_TREE_DEPTH, NUM_GUARDIAN_SIGNERS, @@ -415,16 +402,16 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { } function testParsePriceFeedUpdatesRevertsIfPriceIdNotIncluded() public { - PriceFeedMessage[] memory attestations = new PriceFeedMessage[](1); + PriceFeedMessage[] memory messages = new PriceFeedMessage[](1); - attestations[0].priceId = bytes32(uint(1)); - attestations[0].price = 1000; - attestations[0].publishTime = 10; + messages[0].priceId = bytes32(uint(1)); + messages[0].price = 1000; + messages[0].publishTime = 10; ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); bytes32[] memory priceIds = new bytes32[](1); priceIds[0] = bytes32(uint(2)); @@ -439,20 +426,20 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { } function testParsePriceFeedUpdateRevertsIfPricesOutOfTimeRange() public { - uint numAttestations = 10; + uint numMessages = 10; ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); - for (uint i = 0; i < numAttestations; i++) { - attestations[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200] + for (uint i = 0; i < numMessages; i++) { + messages[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200] } ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); // Request for parse within the given time range should work pyth.parsePriceFeedUpdates{value: updateFee}( @@ -473,20 +460,20 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { } function testParsePriceFeedUpdatesLatestPriceIfNecessary() public { - uint numAttestations = 10; + uint numMessages = 10; ( bytes32[] memory priceIds, - PriceFeedMessage[] memory attestations - ) = generateRandomPriceAttestations(numAttestations); + PriceFeedMessage[] memory messages + ) = generateRandomPriceMessages(numMessages); - for (uint i = 0; i < numAttestations; i++) { - attestations[i].publishTime = uint64((getRandUint() % 101)); // All between [0, 100] + for (uint i = 0; i < numMessages; i++) { + messages[i].publishTime = uint64((getRandUint() % 101)); // All between [0, 100] } ( bytes[] memory updateData, uint updateFee - ) = createBatchedUpdateDataFromAttestations(attestations); + ) = createBatchedUpdateDataFromMessages(messages); // Request for parse within the given time range should work and update the latest price pyth.parsePriceFeedUpdates{value: updateFee}( @@ -497,20 +484,18 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { ); // Check if the latest price is updated - for (uint i = 0; i < numAttestations; i++) { + for (uint i = 0; i < numMessages; i++) { assertEq( pyth.getPriceUnsafe(priceIds[i]).publishTime, - attestations[i].publishTime + messages[i].publishTime ); } - for (uint i = 0; i < numAttestations; i++) { - attestations[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200] + for (uint i = 0; i < numMessages; i++) { + messages[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200] } - (updateData, updateFee) = createBatchedUpdateDataFromAttestations( - attestations - ); + (updateData, updateFee) = createBatchedUpdateDataFromMessages(messages); // Request for parse after the time range should revert. vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); @@ -522,7 +507,7 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { ); // parse function reverted so publishTimes should remain less than or equal to 100 - for (uint i = 0; i < numAttestations; i++) { + for (uint i = 0; i < numMessages; i++) { assertGe(100, pyth.getPriceUnsafe(priceIds[i]).publishTime); } @@ -535,10 +520,10 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { ); // Check if the latest price is updated - for (uint i = 0; i < numAttestations; i++) { + for (uint i = 0; i < numMessages; i++) { assertEq( pyth.getPriceUnsafe(priceIds[i]).publishTime, - attestations[i].publishTime + messages[i].publishTime ); } } From 047952fadc50095195ec3bfd38992209f080ef32 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Wed, 27 Mar 2024 17:20:13 +0000 Subject: [PATCH 07/10] refactor(target_chains/ethereum): rename tests and vars in GasBenchmark --- .../contracts/forge-test/GasBenchmark.t.sol | 245 +++++++++--------- 1 file changed, 123 insertions(+), 122 deletions(-) diff --git a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol index 1041b13c60..8a09a131f4 100644 --- a/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol +++ b/target_chains/ethereum/contracts/forge-test/GasBenchmark.t.sol @@ -34,19 +34,19 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { PythStructs.Price[] cachedPrices; uint64[] cachedPricesPublishTimes; - bytes[][] cachedPricesWhMerkleUpdateData; // i th element contains the update data for the first i prices - bytes[] allCachedPricesWhMerkleUpdateData; // the update data for all prices - uint[] cachedPricesWhMerkleUpdateFee; // i th element contains the update fee for the first i prices - uint allCachedPricesWhMerkleUpdateFee; // the update fee for all prices + bytes[][] cachedPricesUpdateData; // i th element contains the update data for the first i prices + bytes[] allCachedPricesUpdateData; // the update data for all prices + uint[] cachedPricesUpdateFee; // i th element contains the update fee for the first i prices + uint allCachedPricesUpdateFee; // the update fee for all prices // Fresh prices are different prices that can be used // as a fresh price to update the prices PythStructs.Price[] freshPrices; uint64[] freshPricesPublishTimes; - bytes[][] freshPricesWhMerkleUpdateData; // i th element contains the update data for the first i prices - bytes[] allFreshPricesWhMerkleUpdateData; // the update data for all prices - uint[] freshPricesWhMerkleUpdateFee; // i th element contains the update fee for the first i prices - uint allFreshPricesWhMerkleUpdateFee; // the update fee for all prices + bytes[][] freshPricesUpdateData; // i th element contains the update data for the first i prices + bytes[] allFreshPricesUpdateData; // the update data for all prices + uint[] freshPricesUpdateFee; // i th element contains the update fee for the first i prices + uint allFreshPricesUpdateFee; // the update fee for all prices uint64 sequence; uint randomSeed; @@ -92,34 +92,24 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { ( bytes[] memory updateData, uint updateFee - ) = generateWhMerkleUpdateDataAndFee(cachedPrices); + ) = generateUpdateDataAndFee(cachedPrices); - cachedPricesWhMerkleUpdateData.push(updateData); - cachedPricesWhMerkleUpdateFee.push(updateFee); + cachedPricesUpdateData.push(updateData); + cachedPricesUpdateFee.push(updateFee); - (updateData, updateFee) = generateWhMerkleUpdateDataAndFee( - freshPrices - ); + (updateData, updateFee) = generateUpdateDataAndFee(freshPrices); - freshPricesWhMerkleUpdateData.push(updateData); - freshPricesWhMerkleUpdateFee.push(updateFee); + freshPricesUpdateData.push(updateData); + freshPricesUpdateFee.push(updateFee); } - allCachedPricesWhMerkleUpdateData = cachedPricesWhMerkleUpdateData[ - NUM_PRICES - 1 - ]; - allCachedPricesWhMerkleUpdateFee = cachedPricesWhMerkleUpdateFee[ - NUM_PRICES - 1 - ]; - allFreshPricesWhMerkleUpdateData = freshPricesWhMerkleUpdateData[ - NUM_PRICES - 1 - ]; - allFreshPricesWhMerkleUpdateFee = freshPricesWhMerkleUpdateFee[ - NUM_PRICES - 1 - ]; + allCachedPricesUpdateData = cachedPricesUpdateData[NUM_PRICES - 1]; + allCachedPricesUpdateFee = cachedPricesUpdateFee[NUM_PRICES - 1]; + allFreshPricesUpdateData = freshPricesUpdateData[NUM_PRICES - 1]; + allFreshPricesUpdateFee = freshPricesUpdateFee[NUM_PRICES - 1]; // Populate the contract with the initial prices - pyth.updatePriceFeeds{value: allCachedPricesWhMerkleUpdateFee}( - allCachedPricesWhMerkleUpdateData + pyth.updatePriceFeeds{value: allCachedPricesUpdateFee}( + allCachedPricesUpdateData ); } @@ -128,7 +118,7 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { val = uint(keccak256(abi.encode(randomSeed))); } - function generateWhMerkleUpdateDataAndFee( + function generateUpdateDataAndFee( PythStructs.Price[] memory prices ) internal returns (bytes[] memory updateData, uint updateFee) { updateData = new bytes[](1); @@ -142,83 +132,83 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { updateFee = pyth.getUpdateFee(updateData); } - function testBenchmarkUpdatePriceFeedsWhMerkle1FeedFresh() public { - pyth.updatePriceFeeds{value: freshPricesWhMerkleUpdateFee[0]}( - freshPricesWhMerkleUpdateData[0] + function testBenchmarkUpdatePriceFeeds1FeedFresh() public { + pyth.updatePriceFeeds{value: freshPricesUpdateFee[0]}( + freshPricesUpdateData[0] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle2FeedsFresh() public { - pyth.updatePriceFeeds{value: freshPricesWhMerkleUpdateFee[1]}( - freshPricesWhMerkleUpdateData[1] + function testBenchmarkUpdatePriceFeeds2FeedsFresh() public { + pyth.updatePriceFeeds{value: freshPricesUpdateFee[1]}( + freshPricesUpdateData[1] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle3FeedsFresh() public { - pyth.updatePriceFeeds{value: freshPricesWhMerkleUpdateFee[2]}( - freshPricesWhMerkleUpdateData[2] + function testBenchmarkUpdatePriceFeeds3FeedsFresh() public { + pyth.updatePriceFeeds{value: freshPricesUpdateFee[2]}( + freshPricesUpdateData[2] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle4FeedsFresh() public { - pyth.updatePriceFeeds{value: freshPricesWhMerkleUpdateFee[3]}( - freshPricesWhMerkleUpdateData[3] + function testBenchmarkUpdatePriceFeeds4FeedsFresh() public { + pyth.updatePriceFeeds{value: freshPricesUpdateFee[3]}( + freshPricesUpdateData[3] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle5FeedsFresh() public { - pyth.updatePriceFeeds{value: freshPricesWhMerkleUpdateFee[4]}( - freshPricesWhMerkleUpdateData[4] + function testBenchmarkUpdatePriceFeeds5FeedsFresh() public { + pyth.updatePriceFeeds{value: freshPricesUpdateFee[4]}( + freshPricesUpdateData[4] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle1FeedNotFresh() public { - pyth.updatePriceFeeds{value: cachedPricesWhMerkleUpdateFee[0]}( - cachedPricesWhMerkleUpdateData[0] + function testBenchmarkUpdatePriceFeeds1FeedNotFresh() public { + pyth.updatePriceFeeds{value: cachedPricesUpdateFee[0]}( + cachedPricesUpdateData[0] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle2FeedsNotFresh() public { - pyth.updatePriceFeeds{value: cachedPricesWhMerkleUpdateFee[1]}( - cachedPricesWhMerkleUpdateData[1] + function testBenchmarkUpdatePriceFeeds2FeedsNotFresh() public { + pyth.updatePriceFeeds{value: cachedPricesUpdateFee[1]}( + cachedPricesUpdateData[1] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle3FeedsNotFresh() public { - pyth.updatePriceFeeds{value: cachedPricesWhMerkleUpdateFee[2]}( - cachedPricesWhMerkleUpdateData[2] + function testBenchmarkUpdatePriceFeeds3FeedsNotFresh() public { + pyth.updatePriceFeeds{value: cachedPricesUpdateFee[2]}( + cachedPricesUpdateData[2] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle4FeedsNotFresh() public { - pyth.updatePriceFeeds{value: cachedPricesWhMerkleUpdateFee[3]}( - cachedPricesWhMerkleUpdateData[3] + function testBenchmarkUpdatePriceFeeds4FeedsNotFresh() public { + pyth.updatePriceFeeds{value: cachedPricesUpdateFee[3]}( + cachedPricesUpdateData[3] ); } - function testBenchmarkUpdatePriceFeedsWhMerkle5FeedsNotFresh() public { - pyth.updatePriceFeeds{value: cachedPricesWhMerkleUpdateFee[4]}( - cachedPricesWhMerkleUpdateData[4] + function testBenchmarkUpdatePriceFeeds5FeedsNotFresh() public { + pyth.updatePriceFeeds{value: cachedPricesUpdateFee[4]}( + cachedPricesUpdateData[4] ); } - function testBenchmarkUpdatePriceFeedsIfNecessaryWhMerkleFresh() public { + function testBenchmarkUpdatePriceFeedsIfNecessaryFresh() public { // Since the prices have advanced, the publishTimes are newer than one in // the contract and hence, the call should succeed. - pyth.updatePriceFeedsIfNecessary{ - value: allFreshPricesWhMerkleUpdateFee - }(allFreshPricesWhMerkleUpdateData, priceIds, freshPricesPublishTimes); + pyth.updatePriceFeedsIfNecessary{value: allFreshPricesUpdateFee}( + allFreshPricesUpdateData, + priceIds, + freshPricesPublishTimes + ); } - function testBenchmarkUpdatePriceFeedsIfNecessaryWhMerkleNotFresh() public { + function testBenchmarkUpdatePriceFeedsIfNecessaryNotFresh() public { // Since the price is not advanced, the publishTimes are the same as the // ones in the contract. vm.expectRevert(PythErrors.NoFreshUpdate.selector); - pyth.updatePriceFeedsIfNecessary{ - value: allCachedPricesWhMerkleUpdateFee - }( - allCachedPricesWhMerkleUpdateData, + pyth.updatePriceFeedsIfNecessary{value: allCachedPricesUpdateFee}( + allCachedPricesUpdateData, priceIds, cachedPricesPublishTimes ); @@ -228,8 +218,8 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { bytes32[] memory ids = new bytes32[](1); ids[0] = priceIds[0]; - pyth.parsePriceFeedUpdates{value: allFreshPricesWhMerkleUpdateFee}( - allFreshPricesWhMerkleUpdateData, + pyth.parsePriceFeedUpdates{value: allFreshPricesUpdateFee}( + allFreshPricesUpdateData, ids, 0, 50 @@ -241,103 +231,114 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { ids[0] = priceIds[0]; ids[1] = priceIds[1]; - pyth.parsePriceFeedUpdates{value: allFreshPricesWhMerkleUpdateFee}( - allFreshPricesWhMerkleUpdateData, + pyth.parsePriceFeedUpdates{value: allFreshPricesUpdateFee}( + allFreshPricesUpdateData, ids, 0, 50 ); } - function testBenchmarkParsePriceFeedUpdatesUniqueForWhMerkle() public { + function testBenchmarkParsePriceFeedUpdatesUniqueFor() public { bytes32[] memory ids = new bytes32[](1); ids[0] = priceIds[0]; - pyth.parsePriceFeedUpdatesUnique{ - value: freshPricesWhMerkleUpdateFee[0] - }( - freshPricesWhMerkleUpdateData[0], + pyth.parsePriceFeedUpdatesUnique{value: freshPricesUpdateFee[0]}( + freshPricesUpdateData[0], ids, uint64(freshPrices[0].publishTime), 100 ); } - function testBenchmarkParsePriceFeedUpdatesUniqueWhMerkleForOnePriceFeedNotWithinRange() + function testBenchmarkParsePriceFeedUpdatesUniqueForOnePriceFeedNotWithinRange() public { bytes32[] memory ids = new bytes32[](1); ids[0] = priceIds[0]; vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); - pyth.parsePriceFeedUpdatesUnique{ - value: freshPricesWhMerkleUpdateFee[0] - }( - freshPricesWhMerkleUpdateData[0], + pyth.parsePriceFeedUpdatesUnique{value: freshPricesUpdateFee[0]}( + freshPricesUpdateData[0], ids, uint64(freshPrices[0].publishTime) - 1, 100 ); } - function testBenchmarkParsePriceFeedUpdatesForWhMerkle1() public { + function testBenchmarkParsePriceFeedUpdates1() public { uint numIds = 1; bytes32[] memory ids = new bytes32[](numIds); for (uint i = 0; i < numIds; i++) { ids[i] = priceIds[i]; } - pyth.parsePriceFeedUpdates{ - value: freshPricesWhMerkleUpdateFee[numIds - 1] - }(freshPricesWhMerkleUpdateData[numIds - 1], ids, 0, 50); + pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}( + freshPricesUpdateData[numIds - 1], + ids, + 0, + 50 + ); } - function testBenchmarkParsePriceFeedUpdatesForWhMerkle2() public { + function testBenchmarkParsePriceFeedUpdates2() public { uint numIds = 2; bytes32[] memory ids = new bytes32[](numIds); for (uint i = 0; i < numIds; i++) { ids[i] = priceIds[i]; } - pyth.parsePriceFeedUpdates{ - value: freshPricesWhMerkleUpdateFee[numIds - 1] - }(freshPricesWhMerkleUpdateData[numIds - 1], ids, 0, 50); + pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}( + freshPricesUpdateData[numIds - 1], + ids, + 0, + 50 + ); } - function testBenchmarkParsePriceFeedUpdatesForWhMerkle3() public { + function testBenchmarkParsePriceFeedUpdates3() public { uint numIds = 3; bytes32[] memory ids = new bytes32[](numIds); for (uint i = 0; i < numIds; i++) { ids[i] = priceIds[i]; } - pyth.parsePriceFeedUpdates{ - value: freshPricesWhMerkleUpdateFee[numIds - 1] - }(freshPricesWhMerkleUpdateData[numIds - 1], ids, 0, 50); + pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}( + freshPricesUpdateData[numIds - 1], + ids, + 0, + 50 + ); } - function testBenchmarkParsePriceFeedUpdatesForWhMerkle4() public { + function testBenchmarkParsePriceFeedUpdates4() public { uint numIds = 4; bytes32[] memory ids = new bytes32[](numIds); for (uint i = 0; i < numIds; i++) { ids[i] = priceIds[i]; } - pyth.parsePriceFeedUpdates{ - value: freshPricesWhMerkleUpdateFee[numIds - 1] - }(freshPricesWhMerkleUpdateData[numIds - 1], ids, 0, 50); + pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}( + freshPricesUpdateData[numIds - 1], + ids, + 0, + 50 + ); } - function testBenchmarkParsePriceFeedUpdatesForWhMerkle5() public { + function testBenchmarkParsePriceFeedUpdates5() public { uint numIds = 5; bytes32[] memory ids = new bytes32[](numIds); for (uint i = 0; i < numIds; i++) { ids[i] = priceIds[i]; } - pyth.parsePriceFeedUpdates{ - value: freshPricesWhMerkleUpdateFee[numIds - 1] - }(freshPricesWhMerkleUpdateData[numIds - 1], ids, 0, 50); + pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[numIds - 1]}( + freshPricesUpdateData[numIds - 1], + ids, + 0, + 50 + ); } function testBenchmarkParsePriceFeedUpdatesForAllPriceFeedsShuffledSubsetPriceIds() @@ -348,38 +349,38 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { ids[0] = priceIds[4]; ids[1] = priceIds[2]; ids[2] = priceIds[0]; - pyth.parsePriceFeedUpdates{value: freshPricesWhMerkleUpdateFee[4]}( // updateFee based on number of priceFeeds in updateData - freshPricesWhMerkleUpdateData[4], + pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[4]}( // updateFee based on number of priceFeeds in updateData + freshPricesUpdateData[4], ids, 0, 50 ); } - function testBenchmarkParsePriceFeedUpdatesWhMerkleForOnePriceFeedNotWithinRange() + function testBenchmarkParsePriceFeedUpdatesForOnePriceFeedNotWithinRange() public { bytes32[] memory ids = new bytes32[](1); ids[0] = priceIds[0]; vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); - pyth.parsePriceFeedUpdates{value: freshPricesWhMerkleUpdateFee[0]}( - freshPricesWhMerkleUpdateData[0], + pyth.parsePriceFeedUpdates{value: freshPricesUpdateFee[0]}( + freshPricesUpdateData[0], ids, 50, 100 ); } - function testBenchmarkParsePriceFeedUpdatesForOnePriceFeedNotWithinRange() + function testBenchmarkParsePriceFeedUpdatesForAllPriceFeedsNotWithinRange() public { bytes32[] memory ids = new bytes32[](1); ids[0] = priceIds[0]; vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); - pyth.parsePriceFeedUpdates{value: allFreshPricesWhMerkleUpdateFee}( - allFreshPricesWhMerkleUpdateData, + pyth.parsePriceFeedUpdates{value: allFreshPricesUpdateFee}( + allFreshPricesUpdateData, ids, 50, 100 @@ -402,23 +403,23 @@ contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils { pyth.getEmaPrice(priceIds[0]); } - function testBenchmarkGetUpdateFeeWhMerkle1() public view { - pyth.getUpdateFee(freshPricesWhMerkleUpdateData[0]); + function testBenchmarkGetUpdateFee1() public view { + pyth.getUpdateFee(freshPricesUpdateData[0]); } - function testBenchmarkGetUpdateFeeWhMerkle2() public view { - pyth.getUpdateFee(freshPricesWhMerkleUpdateData[1]); + function testBenchmarkGetUpdateFee2() public view { + pyth.getUpdateFee(freshPricesUpdateData[1]); } - function testBenchmarkGetUpdateFeeWhMerkle3() public view { - pyth.getUpdateFee(freshPricesWhMerkleUpdateData[2]); + function testBenchmarkGetUpdateFee3() public view { + pyth.getUpdateFee(freshPricesUpdateData[2]); } - function testBenchmarkGetUpdateFeeWhMerkle4() public view { - pyth.getUpdateFee(freshPricesWhMerkleUpdateData[3]); + function testBenchmarkGetUpdateFee4() public view { + pyth.getUpdateFee(freshPricesUpdateData[3]); } - function testBenchmarkGetUpdateFeeWhMerkle5() public view { - pyth.getUpdateFee(freshPricesWhMerkleUpdateData[4]); + function testBenchmarkGetUpdateFee5() public view { + pyth.getUpdateFee(freshPricesUpdateData[4]); } } From 21546ec8419ea0e0a8c5690539b6e7a98ccbcbba Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Thu, 28 Mar 2024 14:02:09 +0000 Subject: [PATCH 08/10] refactor(target_chains/ethereum): remove price attestations from tests --- .../forge-test/VerificationExperiments.t.sol | 199 +++++++----------- .../forge-test/utils/PythTestUtils.t.sol | 102 +-------- 2 files changed, 74 insertions(+), 227 deletions(-) diff --git a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol index ffd4d38738..d3e218423f 100644 --- a/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol +++ b/target_chains/ethereum/contracts/forge-test/VerificationExperiments.t.sol @@ -179,7 +179,7 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { thresholdUpdate = generateThresholdUpdate(priceIds[0], freshPrices[0]); - nativeUpdate = generateAttestationPayload(priceIds[0], freshPrices[0]); + nativeUpdate = generateMessagePayload(priceIds[0], freshPrices[0]); } // Get the payload for a wormhole batch price update @@ -201,20 +201,18 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { } // Helper function to serialize a single price update to bytes. - // Returns a serialized PriceAttestation. - function generateAttestationPayload( + // Returns a serialized PriceFeedMessage. + function generateMessagePayload( bytes32 priceId, PythStructs.Price memory price ) internal returns (bytes memory data) { - bytes32[] memory attestationPriceIds = new bytes32[](1); - attestationPriceIds[0] = priceId; + bytes32[] memory messagePriceIds = new bytes32[](1); + messagePriceIds[0] = priceId; PythStructs.Price[] memory prices = new PythStructs.Price[](1); prices[0] = price; - PriceAttestation[] memory attestation = pricesToPriceAttestations( - attestationPriceIds, - prices - ); - data = generatePriceFeedUpdatePayload(attestation); + data = encodePriceFeedMessages( + pricesToPriceFeedMessages(messagePriceIds, prices) + )[0]; return data; } @@ -230,7 +228,7 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { internal returns (bytes32 root, bytes memory data, bytes32[] memory proof) { - data = generateAttestationPayload(priceId, price); + data = generateMessagePayload(priceId, price); bytes32 curNodeHash = keccak256(data); proof = new bytes32[](depth); @@ -287,7 +285,7 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { bytes32[] memory proof ) = generateMerkleProof(priceId, price, depth); - data = generateAttestationPayload(priceId, price); + data = generateMessagePayload(priceId, price); bytes32 hash = keccak256(data); (uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, root); @@ -296,12 +294,12 @@ contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils { return ThresholdMerkleUpdate(signature, root, data, proof); } - // Generate a threshold-signed price attestation. + // Generate a threshold-signed price feed message. function generateThresholdUpdate( bytes32 priceId, PythStructs.Price memory price ) internal returns (ThresholdUpdate memory update) { - bytes memory data = generateAttestationPayload(priceId, price); + bytes memory data = generateMessagePayload(priceId, price); bytes32 hash = keccak256(data); (uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, hash); @@ -409,13 +407,13 @@ contract PythExperimental is Pyth { } // Update a single price feed via a wormhole-attested merkle proof. - // data is expected to be a serialized PriceAttestation + // data is expected to be a serialized PriceFeedMessage function updatePriceFeedsWhMerkle( bytes calldata rootVaa, bytes memory data, bytes32[] memory proof ) public payable { - IWormhole.VM memory vm = parseAndVerifyBatchAttestationVM(rootVaa); + IWormhole.VM memory vm = parseAndVerifyBatchMessageVM(rootVaa); assert(vm.payload.length == 32); bytes32 expectedRoot = UnsafeBytesLib.toBytes32(vm.payload, 0); @@ -424,12 +422,14 @@ contract PythExperimental is Pyth { ( PythInternalStructs.PriceInfo memory info, - bytes32 priceId - ) = parseSingleAttestationFromBatch(data, 0, data.length); + bytes32 priceId, + + ) = parsePriceFeedMessageFromMemory(data, 0); + updateLatestPriceIfNecessary(priceId, info); } - function parseAndVerifyBatchAttestationVM( + function parseAndVerifyBatchMessageVM( bytes calldata encodedVm ) internal view returns (IWormhole.VM memory vm) { { @@ -448,7 +448,7 @@ contract PythExperimental is Pyth { } // Update a single price feed via a threshold-signed merkle proof. - // data is expected to be a serialized PriceAttestation + // data is expected to be a serialized PriceFeedMessage function updatePriceFeedsThresholdMerkle( bytes memory rootSignature, bytes32 rootHash, @@ -463,13 +463,14 @@ contract PythExperimental is Pyth { ( PythInternalStructs.PriceInfo memory info, - bytes32 priceId - ) = parseSingleAttestationFromBatch(data, 0, data.length); + bytes32 priceId, + + ) = parsePriceFeedMessageFromMemory(data, 0); updateLatestPriceIfNecessary(priceId, info); } // Update a single price feed via a threshold-signed price update. - // data is expected to be a serialized PriceAttestation. + // data is expected to be a serialized PriceFeedMessage. function updatePriceFeedsThreshold( bytes memory signature, bytes memory data @@ -480,13 +481,14 @@ contract PythExperimental is Pyth { ( PythInternalStructs.PriceInfo memory info, - bytes32 priceId - ) = parseSingleAttestationFromBatch(data, 0, data.length); + bytes32 priceId, + + ) = parsePriceFeedMessageFromMemory(data, 0); updateLatestPriceIfNecessary(priceId, info); } // Update a single price feed via a "native" price update (i.e., using the default ethereum tx signature for authentication). - // data is expected to be a serialized PriceAttestation. + // data is expected to be a serialized PriceFeedMessage. // This function represents the lower bound on how much gas we can use. function updatePriceFeedsNative(bytes memory data) public payable { // TODO: this function should have a check on the sender. @@ -494,122 +496,67 @@ contract PythExperimental is Pyth { ( PythInternalStructs.PriceInfo memory info, - bytes32 priceId - ) = parseSingleAttestationFromBatch(data, 0, data.length); + bytes32 priceId, + + ) = parsePriceFeedMessageFromMemory(data, 0); updateLatestPriceIfNecessary(priceId, info); } - function parseSingleAttestationFromBatch( - bytes memory encoded, - uint index, - uint attestationSize + function parsePriceFeedMessageFromMemory( + bytes memory encodedPriceFeed, + uint offset ) internal pure - returns (PythInternalStructs.PriceInfo memory info, bytes32 priceId) + returns ( + PythInternalStructs.PriceInfo memory priceInfo, + bytes32 priceId, + uint64 prevPublishTime + ) { unchecked { - // NOTE: We don't advance the global index immediately. - // attestationIndex is an attestation-local offset used - // for readability and easier debugging. - uint attestationIndex = 0; + priceId = UnsafeBytesLib.toBytes32(encodedPriceFeed, offset); + offset += 32; - // Unused bytes32 product id - attestationIndex += 32; - - priceId = UnsafeBytesLib.toBytes32( - encoded, - index + attestationIndex + priceInfo.price = int64( + UnsafeBytesLib.toUint64(encodedPriceFeed, offset) ); - attestationIndex += 32; + offset += 8; - info.price = int64( - UnsafeBytesLib.toUint64(encoded, index + attestationIndex) - ); - attestationIndex += 8; + priceInfo.conf = UnsafeBytesLib.toUint64(encodedPriceFeed, offset); + offset += 8; - info.conf = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex + priceInfo.expo = int32( + UnsafeBytesLib.toUint32(encodedPriceFeed, offset) ); - attestationIndex += 8; - - info.expo = int32( - UnsafeBytesLib.toUint32(encoded, index + attestationIndex) + offset += 4; + + // Publish time is i64 in some environments due to the standard in that + // environment. This would not cause any problem because since the signed + // integer is represented in two's complement, the value would be the same + // in both cases (for a million year at least) + priceInfo.publishTime = UnsafeBytesLib.toUint64( + encodedPriceFeed, + offset ); - attestationIndex += 4; + offset += 8; - info.emaPrice = int64( - UnsafeBytesLib.toUint64(encoded, index + attestationIndex) + // We do not store this field because it is not used on the latest feed queries. + prevPublishTime = UnsafeBytesLib.toUint64(encodedPriceFeed, offset); + offset += 8; + + priceInfo.emaPrice = int64( + UnsafeBytesLib.toUint64(encodedPriceFeed, offset) ); - attestationIndex += 8; + offset += 8; - info.emaConf = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex + priceInfo.emaConf = UnsafeBytesLib.toUint64( + encodedPriceFeed, + offset ); - attestationIndex += 8; - - { - // Status is an enum (encoded as uint8) with the following values: - // 0 = UNKNOWN: The price feed is not currently updating for an unknown reason. - // 1 = TRADING: The price feed is updating as expected. - // 2 = HALTED: The price feed is not currently updating because trading in the product has been halted. - // 3 = AUCTION: The price feed is not currently updating because an auction is setting the price. - uint8 status = UnsafeBytesLib.toUint8( - encoded, - index + attestationIndex - ); - attestationIndex += 1; - - // Unused uint32 numPublishers - attestationIndex += 4; - - // Unused uint32 numPublishers - attestationIndex += 4; - - // Unused uint64 attestationTime - attestationIndex += 8; - - info.publishTime = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - - if (status == 1) { - // status == TRADING - attestationIndex += 24; - } else { - // If status is not trading then the latest available price is - // the previous price info that are passed here. - - // Previous publish time - info.publishTime = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - - // Previous price - info.price = int64( - UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ) - ); - attestationIndex += 8; - - // Previous confidence - info.conf = UnsafeBytesLib.toUint64( - encoded, - index + attestationIndex - ); - attestationIndex += 8; - } - } + offset += 8; - if (attestationIndex > attestationSize) + if (offset > encodedPriceFeed.length) revert PythErrors.InvalidUpdateData(); } } @@ -665,7 +612,7 @@ struct WormholeMerkleUpdate { // The serialized bytes of a wormhole VAA // The payload of this VAA is a single 32-byte root hash for the merkle tree. bytes rootVaa; - // The serialized bytes of a PriceAttestation + // The serialized bytes of a PriceFeedMessage bytes data; // The chain of proof nodes. bytes32[] proof; @@ -677,7 +624,7 @@ struct WormholeMerkleUpdate { struct ThresholdMerkleUpdate { bytes rootSignature; bytes32 rootHash; - // The serialized bytes of a PriceAttestation + // The serialized bytes of a PriceFeedMessage bytes data; // The chain of proof nodes. bytes32[] proof; @@ -689,6 +636,6 @@ struct ThresholdMerkleUpdate { struct ThresholdUpdate { // Signature of the hash of the data. bytes signature; - // The serialized bytes of a PriceAttestation + // The serialized bytes of a PriceFeedMessage bytes data; } diff --git a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol index 752156e4d7..db9bf94c48 100644 --- a/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol +++ b/target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol @@ -59,30 +59,7 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { return SINGLE_UPDATE_FEE_IN_WEI; } - /// Utilities to help generating price attestations and VAAs for them - - enum PriceAttestationStatus { - Unknown, - Trading - } - - struct PriceAttestation { - bytes32 productId; - bytes32 priceId; - int64 price; - uint64 conf; - int32 expo; - int64 emaPrice; - uint64 emaConf; - PriceAttestationStatus status; - uint32 numPublishers; - uint32 maxNumPublishers; - uint64 attestationTime; - uint64 publishTime; - uint64 prevPublishTime; - int64 prevPrice; - uint64 prevConf; - } + /// Utilities to help generating price feed messages and VAAs for them struct PriceFeedMessage { bytes32 priceId; @@ -277,83 +254,6 @@ abstract contract PythTestUtils is Test, WormholeTestUtils, RandTestUtils { ); } - // 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. - function generatePriceFeedUpdatePayload( - PriceAttestation[] memory attestations - ) public pure returns (bytes memory payload) { - bytes memory encodedAttestations = new bytes(0); - - for (uint i = 0; i < attestations.length; ++i) { - // encodePacked uses padding for arrays and we don't want it, so we manually concat them. - encodedAttestations = abi.encodePacked( - encodedAttestations, - attestations[i].productId, - attestations[i].priceId, - attestations[i].price, - attestations[i].conf, - attestations[i].expo, - attestations[i].emaPrice, - attestations[i].emaConf - ); - - // Breaking this in two encodePackes because of the limited EVM stack. - encodedAttestations = abi.encodePacked( - encodedAttestations, - uint8(attestations[i].status), - attestations[i].numPublishers, - attestations[i].maxNumPublishers, - attestations[i].attestationTime, - attestations[i].publishTime, - attestations[i].prevPublishTime, - attestations[i].prevPrice, - attestations[i].prevConf - ); - } - - payload = abi.encodePacked( - uint32(0x50325748), // Magic - uint16(3), // Major version - uint16(0), // Minor version - uint16(1), // Header size of 1 byte as it only contains payloadId - uint8(2), // Payload ID 2 means it's a batch price attestation - uint16(attestations.length), // Number of attestations - uint16(encodedAttestations.length / attestations.length), // Size of a single price attestation. - encodedAttestations - ); - } - - function pricesToPriceAttestations( - bytes32[] memory priceIds, - PythStructs.Price[] memory prices - ) public returns (PriceAttestation[] memory attestations) { - assertEq(priceIds.length, prices.length); - attestations = new PriceAttestation[](prices.length); - - for (uint i = 0; i < prices.length; ++i) { - // Product ID, we use the same price Id. This field is not used. - attestations[i].productId = priceIds[i]; - attestations[i].priceId = priceIds[i]; - attestations[i].price = prices[i].price; - attestations[i].conf = prices[i].conf; - attestations[i].expo = prices[i].expo; - // Same price and conf is used for emaPrice and emaConf - attestations[i].emaPrice = prices[i].price; - attestations[i].emaConf = prices[i].conf; - attestations[i].status = PriceAttestationStatus.Trading; - attestations[i].numPublishers = 5; // This field is not used - attestations[i].maxNumPublishers = 10; // This field is not used - attestations[i].attestationTime = uint64(prices[i].publishTime); // This field is not used - attestations[i].publishTime = uint64(prices[i].publishTime); - // Fields below are not used when status is Trading. just setting them to - // the same value as the prices. - attestations[i].prevPublishTime = uint64(prices[i].publishTime); - attestations[i].prevPrice = prices[i].price; - attestations[i].prevConf = prices[i].conf; - } - } - function pricesToPriceFeedMessages( bytes32[] memory priceIds, PythStructs.Price[] memory prices From 46493d6d824580a984a6c596372dc01e032fc9ef Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Thu, 28 Mar 2024 14:26:11 +0000 Subject: [PATCH 09/10] chore: bump pyth-sdk-solidity version to 3.0.0 --- package-lock.json | 6 +++--- target_chains/ethereum/contracts/package.json | 2 +- target_chains/ethereum/sdk/solidity/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58e93f3e8c..b0c65d1ded 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58328,7 +58328,7 @@ "@openzeppelin/hardhat-upgrades": "^1.22.1", "@pythnetwork/entropy-sdk-solidity": "*", "@pythnetwork/pyth-multisig-wh-message-builder": "*", - "@pythnetwork/pyth-sdk-solidity": "^2.2.0", + "@pythnetwork/pyth-sdk-solidity": "^3.0.0", "contract_manager": "*", "dotenv": "^10.0.0", "elliptic": "^6.5.2", @@ -59447,7 +59447,7 @@ }, "target_chains/ethereum/sdk/solidity": { "name": "@pythnetwork/pyth-sdk-solidity", - "version": "2.4.1", + "version": "3.0.0", "license": "Apache-2.0", "devDependencies": { "abi_generator": "*", @@ -69717,7 +69717,7 @@ "@openzeppelin/truffle-upgrades": "^1.14.0", "@pythnetwork/entropy-sdk-solidity": "*", "@pythnetwork/pyth-multisig-wh-message-builder": "*", - "@pythnetwork/pyth-sdk-solidity": "^2.2.0", + "@pythnetwork/pyth-sdk-solidity": "^3.0.0", "@truffle/hdwallet-provider": "^2.1.5", "@types/chai": "^4.3.4", "chai": "^4.2.0", diff --git a/target_chains/ethereum/contracts/package.json b/target_chains/ethereum/contracts/package.json index 637600b62d..da32b32cc5 100644 --- a/target_chains/ethereum/contracts/package.json +++ b/target_chains/ethereum/contracts/package.json @@ -35,7 +35,7 @@ "@openzeppelin/contracts-upgradeable": "^4.5.2", "@openzeppelin/hardhat-upgrades": "^1.22.1", "@pythnetwork/pyth-multisig-wh-message-builder": "*", - "@pythnetwork/pyth-sdk-solidity": "^2.2.0", + "@pythnetwork/pyth-sdk-solidity": "^3.0.0", "@pythnetwork/entropy-sdk-solidity": "*", "contract_manager": "*", "dotenv": "^10.0.0", diff --git a/target_chains/ethereum/sdk/solidity/package.json b/target_chains/ethereum/sdk/solidity/package.json index 04db459e7b..2865a73ba8 100644 --- a/target_chains/ethereum/sdk/solidity/package.json +++ b/target_chains/ethereum/sdk/solidity/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-sdk-solidity", - "version": "2.4.1", + "version": "3.0.0", "description": "Read prices from the Pyth oracle", "repository": { "type": "git", From b83d557a2991cd58d0b1ea1784656b796e1aac50 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Thu, 28 Mar 2024 14:41:23 +0000 Subject: [PATCH 10/10] refactor(target_chains/ethereum): remove duplicated tests --- .../ethereum/contracts/forge-test/Pyth.t.sol | 221 ------------------ 1 file changed, 221 deletions(-) diff --git a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol index 41e995685c..2ad80b8f07 100644 --- a/target_chains/ethereum/contracts/forge-test/Pyth.t.sol +++ b/target_chains/ethereum/contracts/forge-test/Pyth.t.sol @@ -133,75 +133,6 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { } } - function testParsePriceFeedUpdatesWorksWithRandomDistinctUpdatesInput( - uint seed - ) public { - setRandSeed(seed); - uint numMessages = 1 + (getRandUint() % 30); - ( - bytes32[] memory priceIds, - PriceFeedMessage[] memory messages - ) = generateRandomPriceMessages(numMessages); - - ( - bytes[] memory updateData, - uint updateFee - ) = createBatchedUpdateDataFromMessages(messages); - - // Shuffle the messages - for (uint i = 1; i < numMessages; i++) { - uint swapWith = getRandUint() % (i + 1); - (messages[i], messages[swapWith]) = ( - messages[swapWith], - messages[i] - ); - (priceIds[i], priceIds[swapWith]) = ( - priceIds[swapWith], - priceIds[i] - ); - } - - // Select only first numSelectedMessages. numSelectedMessages will be in [0, numMessages] - uint numSelectedMessages = getRandUint() % (numMessages + 1); - - PriceFeedMessage[] memory selectedMessages = new PriceFeedMessage[]( - numSelectedMessages - ); - bytes32[] memory selectedPriceIds = new bytes32[](numSelectedMessages); - - for (uint i = 0; i < numSelectedMessages; i++) { - selectedMessages[i] = messages[i]; - selectedPriceIds[i] = priceIds[i]; - } - - // Only parse selected messages - PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{ - value: updateFee - }(updateData, selectedPriceIds, 0, MAX_UINT64); - - for (uint i = 0; i < numSelectedMessages; i++) { - assertEq(priceFeeds[i].id, selectedPriceIds[i]); - assertEq(priceFeeds[i].price.expo, selectedMessages[i].expo); - assertEq( - priceFeeds[i].emaPrice.price, - selectedMessages[i].emaPrice - ); - assertEq(priceFeeds[i].emaPrice.conf, selectedMessages[i].emaConf); - assertEq(priceFeeds[i].emaPrice.expo, selectedMessages[i].expo); - - assertEq(priceFeeds[i].price.price, selectedMessages[i].price); - assertEq(priceFeeds[i].price.conf, selectedMessages[i].conf); - assertEq( - priceFeeds[i].price.publishTime, - selectedMessages[i].publishTime - ); - assertEq( - priceFeeds[i].emaPrice.publishTime, - selectedMessages[i].publishTime - ); - } - } - function testParsePriceFeedUpdatesWorksWithOverlappingWithinTimeRangeUpdates() public { @@ -281,31 +212,6 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { assertEq(priceFeeds[0].price.publishTime, 20); } - function testParsePriceFeedUpdatesRevertsIfUpdateFeeIsNotPaid() public { - uint numMessages = 10; - ( - bytes32[] memory priceIds, - PriceFeedMessage[] memory messages - ) = generateRandomPriceMessages(numMessages); - - ( - bytes[] memory updateData, - uint updateFee - ) = createBatchedUpdateDataFromMessages(messages); - - // Since messages are not empty the fee should be at least 1 - assertGe(updateFee, 1); - - vm.expectRevert(PythErrors.InsufficientFee.selector); - - pyth.parsePriceFeedUpdates{value: updateFee - 1}( - updateData, - priceIds, - 0, - MAX_UINT64 - ); - } - function testParsePriceFeedUpdatesRevertsIfUpdateVAAIsInvalid( uint seed ) public { @@ -400,131 +306,4 @@ contract PythTest is Test, WormholeTestUtils, PythTestUtils { MAX_UINT64 ); } - - function testParsePriceFeedUpdatesRevertsIfPriceIdNotIncluded() public { - PriceFeedMessage[] memory messages = new PriceFeedMessage[](1); - - messages[0].priceId = bytes32(uint(1)); - messages[0].price = 1000; - messages[0].publishTime = 10; - - ( - bytes[] memory updateData, - uint updateFee - ) = createBatchedUpdateDataFromMessages(messages); - - bytes32[] memory priceIds = new bytes32[](1); - priceIds[0] = bytes32(uint(2)); - - vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); - pyth.parsePriceFeedUpdates{value: updateFee}( - updateData, - priceIds, - 0, - MAX_UINT64 - ); - } - - function testParsePriceFeedUpdateRevertsIfPricesOutOfTimeRange() public { - uint numMessages = 10; - ( - bytes32[] memory priceIds, - PriceFeedMessage[] memory messages - ) = generateRandomPriceMessages(numMessages); - - for (uint i = 0; i < numMessages; i++) { - messages[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200] - } - - ( - bytes[] memory updateData, - uint updateFee - ) = createBatchedUpdateDataFromMessages(messages); - - // Request for parse within the given time range should work - pyth.parsePriceFeedUpdates{value: updateFee}( - updateData, - priceIds, - 100, - 200 - ); - - // Request for parse after the time range should revert. - vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); - pyth.parsePriceFeedUpdates{value: updateFee}( - updateData, - priceIds, - 300, - MAX_UINT64 - ); - } - - function testParsePriceFeedUpdatesLatestPriceIfNecessary() public { - uint numMessages = 10; - ( - bytes32[] memory priceIds, - PriceFeedMessage[] memory messages - ) = generateRandomPriceMessages(numMessages); - - for (uint i = 0; i < numMessages; i++) { - messages[i].publishTime = uint64((getRandUint() % 101)); // All between [0, 100] - } - - ( - bytes[] memory updateData, - uint updateFee - ) = createBatchedUpdateDataFromMessages(messages); - - // Request for parse within the given time range should work and update the latest price - pyth.parsePriceFeedUpdates{value: updateFee}( - updateData, - priceIds, - 0, - 100 - ); - - // Check if the latest price is updated - for (uint i = 0; i < numMessages; i++) { - assertEq( - pyth.getPriceUnsafe(priceIds[i]).publishTime, - messages[i].publishTime - ); - } - - for (uint i = 0; i < numMessages; i++) { - messages[i].publishTime = uint64(100 + (getRandUint() % 101)); // All between [100, 200] - } - - (updateData, updateFee) = createBatchedUpdateDataFromMessages(messages); - - // Request for parse after the time range should revert. - vm.expectRevert(PythErrors.PriceFeedNotFoundWithinRange.selector); - pyth.parsePriceFeedUpdates{value: updateFee}( - updateData, - priceIds, - 300, - 400 - ); - - // parse function reverted so publishTimes should remain less than or equal to 100 - for (uint i = 0; i < numMessages; i++) { - assertGe(100, pyth.getPriceUnsafe(priceIds[i]).publishTime); - } - - // Time range is now fixed, so parse should work and update the latest price - pyth.parsePriceFeedUpdates{value: updateFee}( - updateData, - priceIds, - 100, - 200 - ); - - // Check if the latest price is updated - for (uint i = 0; i < numMessages; i++) { - assertEq( - pyth.getPriceUnsafe(priceIds[i]).publishTime, - messages[i].publishTime - ); - } - } }