// SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.0; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "forge-std/Test.sol"; import "../contracts/libraries/external/UnsafeBytesLib.sol"; import "@pythnetwork/pyth-sdk-solidity/AbstractPyth.sol"; import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol"; import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; import "../contracts/pyth/PythInternalStructs.sol"; import "./utils/WormholeTestUtils.t.sol"; 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 { // 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; // 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; // Cached prices are populated in the setUp PythStructs.Price[] cachedPrices; bytes[] cachedPricesUpdateData; uint cachedPricesUpdateFee; uint64[] cachedPricesPublishTimes; // Fresh prices are different prices that can be used // as a fresh price to update the prices PythStructs.Price[] freshPrices; bytes[] freshPricesUpdateData; uint freshPricesUpdateFee; uint64[] freshPricesPublishTimes; WormholeMerkleUpdate whMerkleUpdateDepth0; WormholeMerkleUpdate whMerkleUpdateDepth1; WormholeMerkleUpdate whMerkleUpdateDepth8; ThresholdMerkleUpdate thresholdMerkleUpdateDepth0; ThresholdMerkleUpdate thresholdMerkleUpdateDepth1; ThresholdMerkleUpdate thresholdMerkleUpdateDepth8; ThresholdUpdate thresholdUpdate; bytes nativeUpdate; uint64 sequence; function setUp() public { address payable wormhole = payable( setUpWormholeReceiver(NUM_GUARDIANS) ); // Deploy experimental contract PythExperimental implementation = new PythExperimental(); ERC1967Proxy proxy = new ERC1967Proxy( address(implementation), new bytes(0) ); pyth = PythExperimental(address(proxy)); uint16[] memory emitterChainIds = new uint16[](1); emitterChainIds[0] = PythTestUtils.SOURCE_EMITTER_CHAIN_ID; bytes32[] memory emitterAddresses = new bytes32[](1); emitterAddresses[0] = PythTestUtils.SOURCE_EMITTER_ADDRESS; pyth.initialize( wormhole, vm.addr(THRESHOLD_KEY), emitterChainIds, emitterAddresses, PythTestUtils.GOVERNANCE_EMITTER_CHAIN_ID, PythTestUtils.GOVERNANCE_EMITTER_ADDRESS, 0, // Initial governance sequence 60, // Valid time period in seconds 1 // single update fee in wei ); 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); } setRandSeed(12345); for (uint i = 0; i < NUM_PRICES; ++i) { uint64 publishTime = uint64(getRandUint() % 10) + 1; // to make sure prevPublishTime is >= 0 cachedPrices.push( PythStructs.Price( int64(uint64(getRandUint() % 1000)), // Price uint64(getRandUint() % 100), // Confidence -5, // Expo publishTime ) ); cachedPricesPublishTimes.push(publishTime); publishTime += uint64(getRandUint() % 10); freshPrices.push( PythStructs.Price( int64(uint64(getRandUint() % 1000)), // Price uint64(getRandUint() % 100), // Confidence -5, // Expo publishTime ) ); freshPricesPublishTimes.push(publishTime); } // Populate the contract with the initial prices ( cachedPricesUpdateData, cachedPricesUpdateFee ) = generateWormholeUpdateDataAndFee(cachedPrices); pyth.updatePriceFeeds{value: cachedPricesUpdateFee}( cachedPricesUpdateData ); ( freshPricesUpdateData, freshPricesUpdateFee ) = generateWormholeUpdateDataAndFee(freshPrices); // Generate the update payloads for the various verification systems whMerkleUpdateDepth0 = generateSingleWhMerkleUpdate( priceIds[0], freshPrices[0], 0 ); whMerkleUpdateDepth1 = generateSingleWhMerkleUpdate( priceIds[0], freshPrices[0], 1 ); whMerkleUpdateDepth8 = generateSingleWhMerkleUpdate( priceIds[0], freshPrices[0], 8 ); thresholdMerkleUpdateDepth0 = generateThresholdMerkleUpdate( priceIds[0], freshPrices[0], 0 ); thresholdMerkleUpdateDepth1 = generateThresholdMerkleUpdate( priceIds[0], freshPrices[0], 1 ); thresholdMerkleUpdateDepth8 = generateThresholdMerkleUpdate( priceIds[0], freshPrices[0], 8 ); thresholdUpdate = generateThresholdUpdate(priceIds[0], freshPrices[0]); nativeUpdate = generateMessagePayload(priceIds[0], freshPrices[0]); } // Get the payload for a wormhole batch price update function generateWormholeUpdateDataAndFee( PythStructs.Price[] memory prices ) internal returns (bytes[] memory updateData, uint updateFee) { bytes memory vaa = generateWhMerkleUpdate( pricesToPriceFeedMessages(priceIds, prices), MERKLE_TREE_DEPTH, NUM_GUARDIAN_SIGNERS ); ++sequence; updateData = new bytes[](1); updateData[0] = vaa; updateFee = pyth.getUpdateFee(updateData); } // Helper function to serialize a single price update to bytes. // Returns a serialized PriceFeedMessage. function generateMessagePayload( bytes32 priceId, PythStructs.Price memory price ) internal returns (bytes memory data) { bytes32[] memory messagePriceIds = new bytes32[](1); messagePriceIds[0] = priceId; PythStructs.Price[] memory prices = new PythStructs.Price[](1); prices[0] = price; data = encodePriceFeedMessages( pricesToPriceFeedMessages(messagePriceIds, prices) )[0]; return data; } // Helper function to generate a merkle proof of the given depth for the given price. // Note: this function assumes that the data is on the leftmost branch of the tree and // mocks the rest of the nodes (as the hash of the depth). function generateMerkleProof( bytes32 priceId, PythStructs.Price memory price, uint depth ) internal returns (bytes32 root, bytes memory data, bytes32[] memory proof) { data = generateMessagePayload(priceId, price); bytes32 curNodeHash = keccak256(data); proof = new bytes32[](depth); for (uint i = 0; i < depth; ) { // pretend the ith sibling is just i proof[i] = keccak256(abi.encode(i)); curNodeHash = keccak256(abi.encode(curNodeHash, proof[i])); unchecked { i++; } } root = curNodeHash; return (root, data, proof); } // Generate a wormhole-attested merkle proof with the given depth. function generateSingleWhMerkleUpdate( bytes32 priceId, PythStructs.Price memory price, uint depth ) internal returns (WormholeMerkleUpdate memory update) { ( bytes32 root, bytes memory data, bytes32[] memory proof ) = generateMerkleProof(priceId, price, depth); bytes memory rootVaa = generateVaa( uint32(block.timestamp), PythTestUtils.SOURCE_EMITTER_CHAIN_ID, PythTestUtils.SOURCE_EMITTER_ADDRESS, sequence, bytes.concat(root), NUM_GUARDIAN_SIGNERS ); ++sequence; return WormholeMerkleUpdate(rootVaa, data, proof); } // Generate a threshold-signed merkle proof with the given depth. function generateThresholdMerkleUpdate( bytes32 priceId, PythStructs.Price memory price, uint depth ) internal returns (ThresholdMerkleUpdate memory update) { ( bytes32 root, bytes memory data, bytes32[] memory proof ) = generateMerkleProof(priceId, price, depth); data = generateMessagePayload(priceId, price); bytes32 hash = keccak256(data); (uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, root); bytes memory signature = abi.encodePacked(r, s, v - 27); return ThresholdMerkleUpdate(signature, root, data, proof); } // Generate a threshold-signed price feed message. function generateThresholdUpdate( bytes32 priceId, PythStructs.Price memory price ) internal returns (ThresholdUpdate memory update) { bytes memory data = generateMessagePayload(priceId, price); bytes32 hash = keccak256(data); (uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, hash); bytes memory signature = abi.encodePacked(r, s, v - 27); return ThresholdUpdate(signature, data); } function testWhBatchUpdate() public { pyth.updatePriceFeeds{value: freshPricesUpdateFee}( freshPricesUpdateData ); } function testUpdateWhMerkleProofDepth0() public { pyth.updatePriceFeedsWhMerkle( whMerkleUpdateDepth0.rootVaa, whMerkleUpdateDepth0.data, whMerkleUpdateDepth0.proof ); } function testUpdateWhMerkleProofDepth1() public { pyth.updatePriceFeedsWhMerkle( whMerkleUpdateDepth1.rootVaa, whMerkleUpdateDepth1.data, whMerkleUpdateDepth1.proof ); } function testUpdateWhMerkleProofDepth8() public { pyth.updatePriceFeedsWhMerkle( whMerkleUpdateDepth8.rootVaa, whMerkleUpdateDepth8.data, whMerkleUpdateDepth8.proof ); } function testUpdateThresholdMerkleProofDepth0() public { pyth.updatePriceFeedsThresholdMerkle( thresholdMerkleUpdateDepth0.rootSignature, thresholdMerkleUpdateDepth0.rootHash, thresholdMerkleUpdateDepth0.data, thresholdMerkleUpdateDepth0.proof ); } function testUpdateThresholdMerkleProofDepth1() public { pyth.updatePriceFeedsThresholdMerkle( thresholdMerkleUpdateDepth1.rootSignature, thresholdMerkleUpdateDepth1.rootHash, thresholdMerkleUpdateDepth1.data, thresholdMerkleUpdateDepth1.proof ); } function testUpdateThresholdMerkleProofDepth8() public { pyth.updatePriceFeedsThresholdMerkle( thresholdMerkleUpdateDepth8.rootSignature, thresholdMerkleUpdateDepth8.rootHash, thresholdMerkleUpdateDepth8.data, thresholdMerkleUpdateDepth8.proof ); } function testUpdateThreshold() public { pyth.updatePriceFeedsThreshold( thresholdUpdate.signature, thresholdUpdate.data ); } function testUpdateNative() public { pyth.updatePriceFeedsNative(nativeUpdate); } } // Pyth contract extended with methods for other verification systems (merkle proofs / threshold signatures) contract PythExperimental is Pyth { address thresholdPublicKey; function initialize( address wormhole, address thresholdPublicKeyArg, uint16[] calldata dataSourceEmitterChainIds, bytes32[] calldata dataSourceEmitterAddresses, uint16 governanceEmitterChainId, bytes32 governanceEmitterAddress, uint64 governanceInitialSequence, uint validTimePeriodSeconds, uint singleUpdateFeeInWei ) public { thresholdPublicKey = thresholdPublicKeyArg; Pyth._initialize( wormhole, dataSourceEmitterChainIds, dataSourceEmitterAddresses, governanceEmitterChainId, governanceEmitterAddress, governanceInitialSequence, validTimePeriodSeconds, singleUpdateFeeInWei ); } // Update a single price feed via a wormhole-attested merkle proof. // 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 = parseAndVerifyBatchMessageVM(rootVaa); assert(vm.payload.length == 32); bytes32 expectedRoot = UnsafeBytesLib.toBytes32(vm.payload, 0); bool validProof = isValidMerkleProof(expectedRoot, data, proof); if (!validProof) revert PythErrors.InvalidArgument(); ( PythInternalStructs.PriceInfo memory info, bytes32 priceId, ) = parsePriceFeedMessageFromMemory(data, 0); updateLatestPriceIfNecessary(priceId, info); } function parseAndVerifyBatchMessageVM( 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 PriceFeedMessage function updatePriceFeedsThresholdMerkle( bytes memory rootSignature, bytes32 rootHash, bytes memory data, bytes32[] memory proof ) public payable { if (!verifySignature(rootHash, rootSignature, thresholdPublicKey)) revert PythErrors.InvalidArgument(); bool validProof = isValidMerkleProof(rootHash, data, proof); if (!validProof) revert PythErrors.InvalidArgument(); ( PythInternalStructs.PriceInfo memory info, 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 PriceFeedMessage. function updatePriceFeedsThreshold( bytes memory signature, bytes memory data ) public payable { bytes32 hash = keccak256(data); if (!verifySignature(hash, signature, thresholdPublicKey)) revert PythErrors.InvalidArgument(); ( PythInternalStructs.PriceInfo memory info, 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 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. // I'm assuming that check is not very expensive here. ( PythInternalStructs.PriceInfo memory info, bytes32 priceId, ) = parsePriceFeedMessageFromMemory(data, 0); updateLatestPriceIfNecessary(priceId, info); } function parsePriceFeedMessageFromMemory( bytes memory encodedPriceFeed, uint offset ) internal pure returns ( PythInternalStructs.PriceInfo memory priceInfo, bytes32 priceId, uint64 prevPublishTime ) { unchecked { priceId = UnsafeBytesLib.toBytes32(encodedPriceFeed, offset); offset += 32; priceInfo.price = int64( UnsafeBytesLib.toUint64(encodedPriceFeed, offset) ); offset += 8; priceInfo.conf = UnsafeBytesLib.toUint64(encodedPriceFeed, offset); offset += 8; priceInfo.expo = int32( UnsafeBytesLib.toUint32(encodedPriceFeed, offset) ); 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 ); offset += 8; // 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) ); offset += 8; priceInfo.emaConf = UnsafeBytesLib.toUint64( encodedPriceFeed, offset ); offset += 8; if (offset > encodedPriceFeed.length) revert PythErrors.InvalidUpdateData(); } } // Verify that signature is a valid ECDSA signature of messageHash by signer. function verifySignature( bytes32 messageHash, bytes memory signature, address signer ) public pure returns (bool) { bytes32 r; bytes32 s; uint8 v; assembly { r := mload(add(signature, 32)) s := mload(add(signature, 64)) v := byte(0, mload(add(signature, 96))) } if (v < 27) { v += 27; } if (v != 27 && v != 28) { return false; } address recoveredAddress = ecrecover(messageHash, v, r, s); return (recoveredAddress == signer); } // Check that proof is a valid merkle proof of data. This function assumes that // data is the leftmost node of the tree. // TODO: need to encode left/right structure for proof nodes function isValidMerkleProof( bytes32 expectedRoot, bytes memory data, bytes32[] memory proof ) public returns (bool) { bytes32 curNodeHash = keccak256(data); for (uint i = 0; i < proof.length; ) { curNodeHash = keccak256(abi.encode(curNodeHash, proof[i])); unchecked { i++; } } return (expectedRoot == curNodeHash); } } // A merkle tree price update delivered via wormhole. // The update is valid if the data above hashed with the proof nodes sequentially // equals the root hash in the wormhole VAA. 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 PriceFeedMessage bytes data; // The chain of proof nodes. bytes32[] proof; } // A merkle tree price update delivered via threshold signature. // The update is valid if the data above hashed with the proof nodes sequentially // equals the root hash, and the signature is valid for rootHash. struct ThresholdMerkleUpdate { bytes rootSignature; bytes32 rootHash; // The serialized bytes of a PriceFeedMessage bytes data; // The chain of proof nodes. bytes32[] proof; } // A merkle tree price update delivered via threshold signature. // The update is valid if the data above hashed with the proof nodes sequentially // equals the root hash, and the signature is valid for rootHash. struct ThresholdUpdate { // Signature of the hash of the data. bytes signature; // The serialized bytes of a PriceFeedMessage bytes data; }