forked from pyth-network/pyth-crosschain
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[eth] Add benchmark tests (pyth-network#368)
* Add remappings This helps vs code solidity LSP work * Remove unused wormhole contract * Format foundry config file * Fix install foundry script * Add benchmark tests and its utils
- Loading branch information
1 parent
a19cd93
commit 0df243b
Showing
9 changed files
with
469 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// SPDX-License-Identifier: Apache 2 | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; | ||
import "forge-std/Test.sol"; | ||
|
||
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; | ||
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; | ||
import "./utils/WormholeTestUtils.t.sol"; | ||
import "./utils/PythTestUtils.t.sol"; | ||
|
||
contract GasBenchmark 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; | ||
// 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 use 5 prices to form a batch of 5 prices, close to our mainnet transactions. | ||
uint8 constant NUM_PRICES = 5; | ||
|
||
uint constant BENCHMARK_ITERATIONS = 1000; | ||
|
||
IPyth public pyth; | ||
|
||
bytes32[] priceIds; | ||
PythStructs.Price[] prices; | ||
uint64 sequence; | ||
uint randSeed; | ||
|
||
function setUp() public { | ||
pyth = IPyth(setUpPyth(setUpWormhole(NUM_GUARDIANS))); | ||
|
||
priceIds = new bytes32[](NUM_PRICES); | ||
priceIds[0] = bytes32(0x1000000000000000000000000000000000000000000000000000000000000f00); | ||
for (uint i = 1; i < NUM_PRICES; ++i) { | ||
priceIds[i] = bytes32(uint256(priceIds[i-1])+1); | ||
} | ||
|
||
for (uint i = 0; i < NUM_PRICES; ++i) { | ||
prices.push(PythStructs.Price( | ||
int64(uint64(getRand() % 1000)), // Price | ||
uint64(getRand() % 100), // Confidence | ||
-5, // Expo | ||
getRand() % 10 // publishTime | ||
)); | ||
} | ||
} | ||
|
||
function getRand() internal returns (uint val) { | ||
++randSeed; | ||
val = uint(keccak256(abi.encode(randSeed))); | ||
} | ||
|
||
function advancePrices() internal { | ||
for (uint i = 0; i < NUM_PRICES; ++i) { | ||
prices[i].price = int64(uint64(getRand() % 1000)); | ||
prices[i].conf = uint64(getRand() % 100); | ||
prices[i].publishTime += getRand() % 10; | ||
} | ||
} | ||
|
||
function generateUpdateDataAndFee() internal returns (bytes[] memory updateData, uint updateFee) { | ||
bytes memory vaa = generatePriceFeedUpdateVAA( | ||
priceIds, | ||
prices, | ||
sequence, | ||
NUM_GUARDIAN_SIGNERS | ||
); | ||
|
||
++sequence; | ||
|
||
updateData = new bytes[](1); | ||
updateData[0] = vaa; | ||
|
||
updateFee = pyth.getUpdateFee(updateData); | ||
} | ||
|
||
function testBenchmarkUpdatePriceFeedsFresh() public { | ||
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) { | ||
advancePrices(); | ||
|
||
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee(); | ||
pyth.updatePriceFeeds{value: updateFee}(updateData); | ||
} | ||
} | ||
|
||
function testBenchmarkUpdatePriceFeedsNotFresh() public { | ||
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) { | ||
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee(); | ||
pyth.updatePriceFeeds{value: updateFee}(updateData); | ||
} | ||
} | ||
|
||
function testBenchmarkUpdatePriceFeedsIfNecessaryFresh() public { | ||
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) { | ||
advancePrices(); | ||
|
||
uint64[] memory publishTimes = new uint64[](NUM_PRICES); | ||
|
||
for (uint j = 0; j < NUM_PRICES; ++j) { | ||
publishTimes[j] = uint64(prices[j].publishTime); | ||
} | ||
|
||
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee(); | ||
|
||
// Since the prices have advanced, the publishTimes are newer than one in | ||
// the contract and hence, the call should succeed. | ||
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes); | ||
} | ||
} | ||
|
||
function testBenchmarkUpdatePriceFeedsIfNecessaryNotFresh() public { | ||
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) { | ||
uint64[] memory publishTimes = new uint64[](NUM_PRICES); | ||
|
||
for (uint j = 0; j < NUM_PRICES; ++j) { | ||
publishTimes[j] = uint64(prices[j].publishTime); | ||
} | ||
|
||
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee(); | ||
|
||
// Since the price is not advanced, the publishTimes are the same as the | ||
// ones in the contract except the first update. | ||
if (i > 0) { | ||
vm.expectRevert(bytes("no prices in the submitted batch have fresh prices, so this update will have no effect")); | ||
} | ||
|
||
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes); | ||
} | ||
} | ||
|
||
function testBenchmarkGetPrice() public { | ||
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee(); | ||
pyth.updatePriceFeeds{value: updateFee}(updateData); | ||
|
||
// Set the block timestamp to the publish time, so getPrice work as expected. | ||
vm.warp(prices[0].publishTime); | ||
|
||
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) { | ||
pyth.getPrice(priceIds[getRand() % NUM_PRICES]); | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
// SPDX-License-Identifier: Apache 2 | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import "../../contracts/pyth/PythUpgradable.sol"; | ||
import "../../contracts/pyth/PythInternalStructs.sol"; | ||
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; | ||
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; | ||
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; | ||
|
||
|
||
import "forge-std/Test.sol"; | ||
import "./WormholeTestUtils.t.sol"; | ||
|
||
abstract contract PythTestUtils is Test, WormholeTestUtils { | ||
uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1; | ||
bytes32 constant SOURCE_EMITTER_ADDRESS = 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b; | ||
|
||
uint16 constant GOVERNANCE_EMITTER_CHAIN_ID = 0x1; | ||
bytes32 constant GOVERNANCE_EMITTER_ADDRESS = 0x0000000000000000000000000000000000000000000000000000000000000011; | ||
|
||
function setUpPyth(address wormhole) public returns (address) { | ||
PythUpgradable implementation = new PythUpgradable(); | ||
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), new bytes(0)); | ||
PythUpgradable pyth = PythUpgradable(address(proxy)); | ||
pyth.initialize( | ||
wormhole, | ||
SOURCE_EMITTER_CHAIN_ID, | ||
SOURCE_EMITTER_ADDRESS | ||
); | ||
|
||
// TODO: All the logic below should be moved to the initializer | ||
pyth.addDataSource( | ||
SOURCE_EMITTER_CHAIN_ID, | ||
SOURCE_EMITTER_ADDRESS | ||
); | ||
|
||
pyth.updateSingleUpdateFeeInWei( | ||
1 | ||
); | ||
|
||
pyth.updateValidTimePeriodSeconds( | ||
60 | ||
); | ||
|
||
pyth.updateGovernanceDataSource( | ||
GOVERNANCE_EMITTER_CHAIN_ID, | ||
GOVERNANCE_EMITTER_ADDRESS, | ||
0 | ||
); | ||
|
||
return address(pyth); | ||
} | ||
|
||
// Generates byte-encoded payload for the given prices. It sets the emaPrice the same | ||
// as the given price. You can use this to mock wormhole call using `vm.mockCall` and | ||
// return a VM struct with this payload. | ||
// You can use generatePriceFeedUpdateVAA to generate a VAA for a price update. | ||
function generatePriceFeedUpdatePayload( | ||
bytes32[] memory priceIds, | ||
PythStructs.Price[] memory prices | ||
) public returns (bytes memory payload) { | ||
assertEq(priceIds.length, prices.length); | ||
|
||
bytes memory attestations = new bytes(0); | ||
|
||
for (uint i = 0; i < prices.length; ++i) { | ||
// encodePacked uses padding for arrays and we don't want it, so we manually concat them. | ||
attestations = abi.encodePacked( | ||
attestations, | ||
priceIds[i], // Product ID, we use the same price Id. This field is not used. | ||
priceIds[i], // Price ID, | ||
prices[i].price, // Price | ||
prices[i].conf, // Confidence | ||
prices[i].expo, // Exponent | ||
prices[i].price, // EMA price | ||
prices[i].conf // EMA confidence | ||
); | ||
|
||
// Breaking this in two encodePackes because of the limited EVM stack. | ||
attestations = abi.encodePacked( | ||
attestations, | ||
uint8(PythInternalStructs.PriceAttestationStatus.TRADING), | ||
uint32(5), // Number of publishers. This field is not used. | ||
uint32(10), // Maximum number of publishers. This field is not used. | ||
uint64(prices[i].publishTime), // Attestation time. This field is not used. | ||
uint64(prices[i].publishTime), // Publish time. | ||
// Previous values are unused as status is trading. We use the same value | ||
// to make sure the test is irrelevant of the logic of which price is chosen. | ||
uint64(prices[i].publishTime), // Previous publish time. | ||
prices[i].price, // Previous price | ||
prices[i].conf // Previous confidence | ||
); | ||
} | ||
|
||
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(prices.length), // Number of attestations | ||
uint16(attestations.length / prices.length), // Size of a single price attestation. | ||
attestations | ||
); | ||
} | ||
|
||
// Generates a VAA for the given prices. | ||
// This method calls generatePriceFeedUpdatePayload and then creates a VAA with it. | ||
// The VAAs generated from this method use block timestamp as their timestamp. | ||
function generatePriceFeedUpdateVAA( | ||
bytes32[] memory priceIds, | ||
PythStructs.Price[] memory prices, | ||
uint64 sequence, | ||
uint8 numSigners | ||
) public returns (bytes memory vaa) { | ||
bytes memory payload = generatePriceFeedUpdatePayload( | ||
priceIds, | ||
prices | ||
); | ||
|
||
vaa = generateVaa( | ||
uint32(block.timestamp), | ||
SOURCE_EMITTER_CHAIN_ID, | ||
SOURCE_EMITTER_ADDRESS, | ||
sequence, | ||
payload, | ||
numSigners | ||
); | ||
} | ||
} | ||
|
||
contract PythTestUtilsTest is Test, WormholeTestUtils, PythTestUtils { | ||
// TODO: It is better to have a PythEvents contract that be extendable. | ||
event PriceFeedUpdate(bytes32 indexed id, bool indexed fresh, uint16 chainId, uint64 sequenceNumber, uint lastPublishTime, uint publishTime, int64 price, uint64 conf); | ||
|
||
function testGeneratePriceFeedUpdateVAAWorks() public { | ||
IPyth pyth = IPyth(setUpPyth(setUpWormhole( | ||
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 = generatePriceFeedUpdateVAA( | ||
priceIds, | ||
prices, | ||
1, // Sequence | ||
1 // No. Signers | ||
); | ||
|
||
bytes[] memory updateData = new bytes[](1); | ||
updateData[0] = vaa; | ||
|
||
uint updateFee = pyth.getUpdateFee(updateData); | ||
|
||
vm.expectEmit(true, true, false, true); | ||
emit PriceFeedUpdate(priceIds[0], true, SOURCE_EMITTER_CHAIN_ID, 1, 0, 1, 100, 10); | ||
|
||
pyth.updatePriceFeeds{value: updateFee}(updateData); | ||
} | ||
} |
Oops, something went wrong.