[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
This commit is contained in:
Ali Behjati 2022-11-02 10:59:39 +01:00 committed by GitHub
parent a19cd93cd3
commit 0df243ba9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 469 additions and 40 deletions

View File

@ -34,3 +34,42 @@ npm run install-forge-deps
After installing the dependencies. Run `forge build` to build the contracts and `forge test` to
test the contracts using tests in `forge-test` directory.
### Gas Benchmark
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
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
specific benchmark test by passing the test name using `--match-test`. A full command to run `testBenchmarkUpdatePriceFeedsFresh` benchmark test is like this:
```
forge test --gas-report --match-contract GasBenchmark --match-test testBenchmarkUpdatePriceFeedsFresh
```
A gas report should have a couple of tables like this:
```
╭───────────────────────────────────────────────────────────────────────────────────────────┬─────────────────┬────────┬────────┬─────────┬─────────╮
│ node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy contract ┆ ┆ ┆ ┆ ┆ │
╞═══════════════════════════════════════════════════════════════════════════════════════════╪═════════════════╪════════╪════════╪═════════╪═════════╡
│ Deployment Cost ┆ Deployment Size ┆ ┆ ┆ ┆ │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 164236 ┆ 2050 ┆ ┆ ┆ ┆ │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ Function Name ┆ min ┆ avg ┆ median ┆ max ┆ # calls │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ .. │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ parseAndVerifyVM ┆ 90292 ┆ 91262 ┆ 90292 ┆ 138792 ┆ 50 │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ updatePriceFeeds ┆ 187385 ┆ 206005 ┆ 187385 ┆ 1118385 ┆ 50 │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ ............. ┆ ..... ┆ ..... ┆ ..... ┆ ..... ┆ ... │
╰───────────────────────────────────────────────────────────────────────────────────────────┴─────────────────┴────────┴────────┴─────────┴─────────╯
```
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
for the first time use significantly more gas.
If you like to optimize the contract and measure the gas optimization you can get gas snapshots using `forge snapshot` and evaluate your
optimization with it. For more information, please refer to [Gas Snapshots documentation](https://book.getfoundry.sh/forge/gas-snapshots).
Once you optimized the code, please share the snapshot difference (generated using `forge snapshot --diff <old-snapshot>`) in the PR too.

View File

@ -1,16 +0,0 @@
// contracts/Implementation.sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "../Implementation.sol";
contract MockImplementation is Implementation {
function initialize() initializer public {
// this function needs to be exposed for an upgrade to pass
}
function testNewImplementationActive() external pure returns (bool) {
return true;
}
}

View File

@ -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]);
}
}
}

View File

@ -1,20 +0,0 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "../contracts/pyth/PythUpgradable.sol";
import "forge-std/Test.sol";
contract TestPythUpgradable is Test {
PythUpgradable public pyth;
function setUp() public {
pyth = new PythUpgradable();
// The values below are just dummy values and this test does nothing.
pyth.initialize(
address(0x0000000000000000000000000000000000000000000000000000000000000000),
0,
0x0000000000000000000000000000000000000000000000000000000000000000
);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "../../contracts/wormhole/Implementation.sol";
import "../../contracts/wormhole/Setup.sol";
import "../../contracts/wormhole/Wormhole.sol";
import "../../contracts/wormhole/interfaces/IWormhole.sol";
import "forge-std/Test.sol";
abstract contract WormholeTestUtils is Test {
function setUpWormhole(uint8 numGuardians) public returns (address) {
Implementation wormholeImpl = new Implementation();
Setup wormholeSetup = new Setup();
Wormhole wormhole = new Wormhole(address(wormholeSetup), new bytes(0));
address[] memory initSigners = new address[](numGuardians);
for (uint256 i = 0; i < numGuardians; ++i) {
initSigners[i] = vm.addr(i + 1); // i+1 is the private key for the i-th signer.
}
// These values are the default values used in our tilt test environment
// and are not important.
Setup(address(wormhole)).setup(
address(wormholeImpl),
initSigners,
2, // Ethereum chain ID
1, // Governance source chain ID (1 = solana)
0x0000000000000000000000000000000000000000000000000000000000000004 // Governance source address
);
return address(wormhole);
}
function generateVaa(
uint32 timestamp,
uint16 emitterChainId,
bytes32 emitterAddress,
uint64 sequence,
bytes memory payload,
uint8 numSigners
) public returns (bytes memory vaa) {
bytes memory body = abi.encodePacked(
timestamp,
uint32(0), // Nonce. It is zero for single VAAs.
emitterChainId,
emitterAddress,
sequence,
uint8(0), // Consistency level (sometimes no. confirmation block). Not important here.
payload
);
bytes32 hash = keccak256(abi.encodePacked(keccak256(body)));
bytes memory signatures = new bytes(0);
for (uint256 i = 0; i < numSigners; ++i) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(i + 1, hash);
// encodePacked uses padding for arrays and we don't want it, so we manually concat them.
signatures = abi.encodePacked(
signatures,
uint8(i), // Guardian index of the signature
r,
s,
v - 27 // v is either 27 or 28. 27 is added to v in Eth (following BTC) but Wormhole doesn't use it.
);
}
vaa = abi.encodePacked(
uint8(1), // Version
uint32(0), // Guardian set index. it is initialized by 0
numSigners,
signatures,
body
);
}
}
contract WormholeTestUtilsTest is Test, WormholeTestUtils {
function testGenerateVaaWorks() public {
IWormhole wormhole = IWormhole(setUpWormhole(5));
bytes memory vaa = generateVaa(
112,
7,
0x0000000000000000000000000000000000000000000000000000000000000bad,
10,
hex"deadbeaf",
4
);
(Structs.VM memory vm, bool valid, ) = wormhole.parseAndVerifyVM(vaa);
assertTrue(valid);
assertEq(vm.timestamp, 112);
assertEq(vm.emitterChainId, 7);
assertEq(vm.emitterAddress, 0x0000000000000000000000000000000000000000000000000000000000000bad);
assertEq(vm.payload, hex"deadbeaf");
assertEq(vm.signatures.length, 4);
}
}

View File

@ -1,11 +1,11 @@
[profile.default]
solc_version = "0.8.4"
solc_version = '0.8.4'
optimizer = true
optimizer_runs = 200
src="contracts"
src = 'contracts'
# We put the tests into the forge-test directory (instead of test) so that
# truffle doesn't try to build them
test="forge-test"
test = 'forge-test'
libs = [
'lib',

6
ethereum/remappings.txt Normal file
View File

@ -0,0 +1,6 @@
@ensdomains/=node_modules/@ensdomains/
@openzeppelin/=node_modules/@openzeppelin/
@pythnetwork/=node_modules/@pythnetwork/
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
truffle/=node_modules/truffle/

View File

@ -13,7 +13,7 @@ if [ ! -f foundry.toml ]; then
fi
# Read compiler version from foundry.toml
SOLC_VERSION=$(grep solc_version foundry.toml | cut -d'=' -f2 | tr -d '" ') || true
SOLC_VERSION=$(grep solc_version foundry.toml | cut -d'=' -f2 | tr -d "' ") || true
if [ -z "$SOLC_VERSION" ]; then
echo "solc_version not found in foundry.toml." >& 2