Skip to content

Commit 0df243b

Browse files
authored
[eth] Add benchmark tests (#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
1 parent a19cd93 commit 0df243b

File tree

9 files changed

+469
-40
lines changed

9 files changed

+469
-40
lines changed

ethereum/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,42 @@ npm run install-forge-deps
3434

3535
After installing the dependencies. Run `forge build` to build the contracts and `forge test` to
3636
test the contracts using tests in `forge-test` directory.
37+
38+
### Gas Benchmark
39+
40+
You can use foundry to run benchmark tests written in [`forge-test/GasBenchmark.t.sol`](./forge-test/GasBenchmark.t.sol). To run the tests with gas report
41+
you can run `forge test --gas-report --match-contract GasBenchmark`. However, as there are multiple benchmarks, this might not be useful. You can run a
42+
specific benchmark test by passing the test name using `--match-test`. A full command to run `testBenchmarkUpdatePriceFeedsFresh` benchmark test is like this:
43+
44+
```
45+
forge test --gas-report --match-contract GasBenchmark --match-test testBenchmarkUpdatePriceFeedsFresh
46+
```
47+
48+
A gas report should have a couple of tables like this:
49+
50+
```
51+
╭───────────────────────────────────────────────────────────────────────────────────────────┬─────────────────┬────────┬────────┬─────────┬─────────╮
52+
│ node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy contract ┆ ┆ ┆ ┆ ┆ │
53+
╞═══════════════════════════════════════════════════════════════════════════════════════════╪═════════════════╪════════╪════════╪═════════╪═════════╡
54+
│ Deployment Cost ┆ Deployment Size ┆ ┆ ┆ ┆ │
55+
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
56+
│ 164236 ┆ 2050 ┆ ┆ ┆ ┆ │
57+
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
58+
│ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │
59+
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
60+
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ .. │
61+
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
62+
│ parseAndVerifyVM ┆ 90292 ┆ 91262 ┆ 90292 ┆ 138792 ┆ 50 │
63+
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
64+
│ updatePriceFeeds ┆ 187385 ┆ 206005 ┆ 187385 ┆ 1118385 ┆ 50 │
65+
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
66+
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ ... │
67+
╰───────────────────────────────────────────────────────────────────────────────────────────┴─────────────────┴────────┴────────┴─────────┴─────────╯
68+
```
69+
70+
For most of the methods, the median gas usage is an indication of our desired gas usage. Because the calls that store something in the storage
71+
for the first time use significantly more gas.
72+
73+
If you like to optimize the contract and measure the gas optimization you can get gas snapshots using `forge snapshot` and evaluate your
74+
optimization with it. For more information, please refer to [Gas Snapshots documentation](https://book.getfoundry.sh/forge/gas-snapshots).
75+
Once you optimized the code, please share the snapshot difference (generated using `forge snapshot --diff <old-snapshot>`) in the PR too.

ethereum/contracts/wormhole/mock/MockImplementation.sol

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// SPDX-License-Identifier: Apache 2
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
6+
import "forge-std/Test.sol";
7+
8+
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
9+
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
10+
import "./utils/WormholeTestUtils.t.sol";
11+
import "./utils/PythTestUtils.t.sol";
12+
13+
contract GasBenchmark is Test, WormholeTestUtils, PythTestUtils {
14+
// 19, current mainnet number of guardians, is used to have gas estimates
15+
// close to our mainnet transactions.
16+
uint8 constant NUM_GUARDIANS = 19;
17+
// 2/3 of the guardians should sign a message for a VAA which is 13 out of 19 guardians.
18+
// It is possible to have more signers but the median seems to be 13.
19+
uint8 constant NUM_GUARDIAN_SIGNERS = 13;
20+
21+
// We use 5 prices to form a batch of 5 prices, close to our mainnet transactions.
22+
uint8 constant NUM_PRICES = 5;
23+
24+
uint constant BENCHMARK_ITERATIONS = 1000;
25+
26+
IPyth public pyth;
27+
28+
bytes32[] priceIds;
29+
PythStructs.Price[] prices;
30+
uint64 sequence;
31+
uint randSeed;
32+
33+
function setUp() public {
34+
pyth = IPyth(setUpPyth(setUpWormhole(NUM_GUARDIANS)));
35+
36+
priceIds = new bytes32[](NUM_PRICES);
37+
priceIds[0] = bytes32(0x1000000000000000000000000000000000000000000000000000000000000f00);
38+
for (uint i = 1; i < NUM_PRICES; ++i) {
39+
priceIds[i] = bytes32(uint256(priceIds[i-1])+1);
40+
}
41+
42+
for (uint i = 0; i < NUM_PRICES; ++i) {
43+
prices.push(PythStructs.Price(
44+
int64(uint64(getRand() % 1000)), // Price
45+
uint64(getRand() % 100), // Confidence
46+
-5, // Expo
47+
getRand() % 10 // publishTime
48+
));
49+
}
50+
}
51+
52+
function getRand() internal returns (uint val) {
53+
++randSeed;
54+
val = uint(keccak256(abi.encode(randSeed)));
55+
}
56+
57+
function advancePrices() internal {
58+
for (uint i = 0; i < NUM_PRICES; ++i) {
59+
prices[i].price = int64(uint64(getRand() % 1000));
60+
prices[i].conf = uint64(getRand() % 100);
61+
prices[i].publishTime += getRand() % 10;
62+
}
63+
}
64+
65+
function generateUpdateDataAndFee() internal returns (bytes[] memory updateData, uint updateFee) {
66+
bytes memory vaa = generatePriceFeedUpdateVAA(
67+
priceIds,
68+
prices,
69+
sequence,
70+
NUM_GUARDIAN_SIGNERS
71+
);
72+
73+
++sequence;
74+
75+
updateData = new bytes[](1);
76+
updateData[0] = vaa;
77+
78+
updateFee = pyth.getUpdateFee(updateData);
79+
}
80+
81+
function testBenchmarkUpdatePriceFeedsFresh() public {
82+
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
83+
advancePrices();
84+
85+
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
86+
pyth.updatePriceFeeds{value: updateFee}(updateData);
87+
}
88+
}
89+
90+
function testBenchmarkUpdatePriceFeedsNotFresh() public {
91+
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
92+
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
93+
pyth.updatePriceFeeds{value: updateFee}(updateData);
94+
}
95+
}
96+
97+
function testBenchmarkUpdatePriceFeedsIfNecessaryFresh() public {
98+
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
99+
advancePrices();
100+
101+
uint64[] memory publishTimes = new uint64[](NUM_PRICES);
102+
103+
for (uint j = 0; j < NUM_PRICES; ++j) {
104+
publishTimes[j] = uint64(prices[j].publishTime);
105+
}
106+
107+
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
108+
109+
// Since the prices have advanced, the publishTimes are newer than one in
110+
// the contract and hence, the call should succeed.
111+
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes);
112+
}
113+
}
114+
115+
function testBenchmarkUpdatePriceFeedsIfNecessaryNotFresh() public {
116+
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
117+
uint64[] memory publishTimes = new uint64[](NUM_PRICES);
118+
119+
for (uint j = 0; j < NUM_PRICES; ++j) {
120+
publishTimes[j] = uint64(prices[j].publishTime);
121+
}
122+
123+
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
124+
125+
// Since the price is not advanced, the publishTimes are the same as the
126+
// ones in the contract except the first update.
127+
if (i > 0) {
128+
vm.expectRevert(bytes("no prices in the submitted batch have fresh prices, so this update will have no effect"));
129+
}
130+
131+
pyth.updatePriceFeedsIfNecessary{value: updateFee}(updateData, priceIds, publishTimes);
132+
}
133+
}
134+
135+
function testBenchmarkGetPrice() public {
136+
(bytes[] memory updateData, uint updateFee) = generateUpdateDataAndFee();
137+
pyth.updatePriceFeeds{value: updateFee}(updateData);
138+
139+
// Set the block timestamp to the publish time, so getPrice work as expected.
140+
vm.warp(prices[0].publishTime);
141+
142+
for (uint i = 0; i < BENCHMARK_ITERATIONS; ++i) {
143+
pyth.getPrice(priceIds[getRand() % NUM_PRICES]);
144+
}
145+
}
146+
}

ethereum/forge-test/PythUpgradable.t.sol

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// SPDX-License-Identifier: Apache 2
2+
3+
pragma solidity ^0.8.0;
4+
5+
import "../../contracts/pyth/PythUpgradable.sol";
6+
import "../../contracts/pyth/PythInternalStructs.sol";
7+
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
8+
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
9+
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
10+
11+
12+
import "forge-std/Test.sol";
13+
import "./WormholeTestUtils.t.sol";
14+
15+
abstract contract PythTestUtils is Test, WormholeTestUtils {
16+
uint16 constant SOURCE_EMITTER_CHAIN_ID = 0x1;
17+
bytes32 constant SOURCE_EMITTER_ADDRESS = 0x71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b;
18+
19+
uint16 constant GOVERNANCE_EMITTER_CHAIN_ID = 0x1;
20+
bytes32 constant GOVERNANCE_EMITTER_ADDRESS = 0x0000000000000000000000000000000000000000000000000000000000000011;
21+
22+
function setUpPyth(address wormhole) public returns (address) {
23+
PythUpgradable implementation = new PythUpgradable();
24+
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), new bytes(0));
25+
PythUpgradable pyth = PythUpgradable(address(proxy));
26+
pyth.initialize(
27+
wormhole,
28+
SOURCE_EMITTER_CHAIN_ID,
29+
SOURCE_EMITTER_ADDRESS
30+
);
31+
32+
// TODO: All the logic below should be moved to the initializer
33+
pyth.addDataSource(
34+
SOURCE_EMITTER_CHAIN_ID,
35+
SOURCE_EMITTER_ADDRESS
36+
);
37+
38+
pyth.updateSingleUpdateFeeInWei(
39+
1
40+
);
41+
42+
pyth.updateValidTimePeriodSeconds(
43+
60
44+
);
45+
46+
pyth.updateGovernanceDataSource(
47+
GOVERNANCE_EMITTER_CHAIN_ID,
48+
GOVERNANCE_EMITTER_ADDRESS,
49+
0
50+
);
51+
52+
return address(pyth);
53+
}
54+
55+
// Generates byte-encoded payload for the given prices. It sets the emaPrice the same
56+
// as the given price. You can use this to mock wormhole call using `vm.mockCall` and
57+
// return a VM struct with this payload.
58+
// You can use generatePriceFeedUpdateVAA to generate a VAA for a price update.
59+
function generatePriceFeedUpdatePayload(
60+
bytes32[] memory priceIds,
61+
PythStructs.Price[] memory prices
62+
) public returns (bytes memory payload) {
63+
assertEq(priceIds.length, prices.length);
64+
65+
bytes memory attestations = new bytes(0);
66+
67+
for (uint i = 0; i < prices.length; ++i) {
68+
// encodePacked uses padding for arrays and we don't want it, so we manually concat them.
69+
attestations = abi.encodePacked(
70+
attestations,
71+
priceIds[i], // Product ID, we use the same price Id. This field is not used.
72+
priceIds[i], // Price ID,
73+
prices[i].price, // Price
74+
prices[i].conf, // Confidence
75+
prices[i].expo, // Exponent
76+
prices[i].price, // EMA price
77+
prices[i].conf // EMA confidence
78+
);
79+
80+
// Breaking this in two encodePackes because of the limited EVM stack.
81+
attestations = abi.encodePacked(
82+
attestations,
83+
uint8(PythInternalStructs.PriceAttestationStatus.TRADING),
84+
uint32(5), // Number of publishers. This field is not used.
85+
uint32(10), // Maximum number of publishers. This field is not used.
86+
uint64(prices[i].publishTime), // Attestation time. This field is not used.
87+
uint64(prices[i].publishTime), // Publish time.
88+
// Previous values are unused as status is trading. We use the same value
89+
// to make sure the test is irrelevant of the logic of which price is chosen.
90+
uint64(prices[i].publishTime), // Previous publish time.
91+
prices[i].price, // Previous price
92+
prices[i].conf // Previous confidence
93+
);
94+
}
95+
96+
payload = abi.encodePacked(
97+
uint32(0x50325748), // Magic
98+
uint16(3), // Major version
99+
uint16(0), // Minor version
100+
uint16(1), // Header size of 1 byte as it only contains payloadId
101+
uint8(2), // Payload ID 2 means it's a batch price attestation
102+
uint16(prices.length), // Number of attestations
103+
uint16(attestations.length / prices.length), // Size of a single price attestation.
104+
attestations
105+
);
106+
}
107+
108+
// Generates a VAA for the given prices.
109+
// This method calls generatePriceFeedUpdatePayload and then creates a VAA with it.
110+
// The VAAs generated from this method use block timestamp as their timestamp.
111+
function generatePriceFeedUpdateVAA(
112+
bytes32[] memory priceIds,
113+
PythStructs.Price[] memory prices,
114+
uint64 sequence,
115+
uint8 numSigners
116+
) public returns (bytes memory vaa) {
117+
bytes memory payload = generatePriceFeedUpdatePayload(
118+
priceIds,
119+
prices
120+
);
121+
122+
vaa = generateVaa(
123+
uint32(block.timestamp),
124+
SOURCE_EMITTER_CHAIN_ID,
125+
SOURCE_EMITTER_ADDRESS,
126+
sequence,
127+
payload,
128+
numSigners
129+
);
130+
}
131+
}
132+
133+
contract PythTestUtilsTest is Test, WormholeTestUtils, PythTestUtils {
134+
// TODO: It is better to have a PythEvents contract that be extendable.
135+
event PriceFeedUpdate(bytes32 indexed id, bool indexed fresh, uint16 chainId, uint64 sequenceNumber, uint lastPublishTime, uint publishTime, int64 price, uint64 conf);
136+
137+
function testGeneratePriceFeedUpdateVAAWorks() public {
138+
IPyth pyth = IPyth(setUpPyth(setUpWormhole(
139+
1 // Number of guardians
140+
)));
141+
142+
bytes32[] memory priceIds = new bytes32[](1);
143+
priceIds[0] = 0x0000000000000000000000000000000000000000000000000000000000000222;
144+
145+
PythStructs.Price[] memory prices = new PythStructs.Price[](1);
146+
prices[0] = PythStructs.Price(
147+
100, // Price
148+
10, // Confidence
149+
-5, // Exponent
150+
1 // Publish time
151+
);
152+
153+
bytes memory vaa = generatePriceFeedUpdateVAA(
154+
priceIds,
155+
prices,
156+
1, // Sequence
157+
1 // No. Signers
158+
);
159+
160+
bytes[] memory updateData = new bytes[](1);
161+
updateData[0] = vaa;
162+
163+
uint updateFee = pyth.getUpdateFee(updateData);
164+
165+
vm.expectEmit(true, true, false, true);
166+
emit PriceFeedUpdate(priceIds[0], true, SOURCE_EMITTER_CHAIN_ID, 1, 0, 1, 100, 10);
167+
168+
pyth.updatePriceFeeds{value: updateFee}(updateData);
169+
}
170+
}

0 commit comments

Comments
 (0)