VerificationExperiments.t.sol 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. // SPDX-License-Identifier: Apache 2
  2. pragma solidity ^0.8.0;
  3. import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
  4. import "forge-std/Test.sol";
  5. import "../contracts/libraries/external/UnsafeBytesLib.sol";
  6. import "@pythnetwork/pyth-sdk-solidity/AbstractPyth.sol";
  7. import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
  8. import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
  9. import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
  10. import "../contracts/pyth/PythInternalStructs.sol";
  11. import "./utils/WormholeTestUtils.t.sol";
  12. import "./utils/PythTestUtils.t.sol";
  13. import "./utils/RandTestUtils.t.sol";
  14. // Experiments to measure the gas usage of different ways of verifying prices in the EVM contract.
  15. contract VerificationExperiments is Test, WormholeTestUtils, PythTestUtils {
  16. // 19, current mainnet number of guardians, is used to have gas estimates
  17. // close to our mainnet transactions.
  18. uint8 constant NUM_GUARDIANS = 19;
  19. // 2/3 of the guardians should sign a message for a VAA which is 13 out of 19 guardians.
  20. // It is possible to have more signers but the median seems to be 13.
  21. uint8 constant NUM_GUARDIAN_SIGNERS = 13;
  22. // We use 5 prices to form a batch of 5 prices, close to our mainnet transactions.
  23. uint8 constant NUM_PRICES = 5;
  24. // Private key for the threshold signature
  25. uint256 THRESHOLD_KEY = 1234;
  26. // We will have less than 512 price for a foreseeable future.
  27. uint8 constant MERKLE_TREE_DEPTH = 9;
  28. PythExperimental public pyth;
  29. bytes32[] priceIds;
  30. // Cached prices are populated in the setUp
  31. PythStructs.Price[] cachedPrices;
  32. bytes[] cachedPricesUpdateData;
  33. uint cachedPricesUpdateFee;
  34. uint64[] cachedPricesPublishTimes;
  35. // Fresh prices are different prices that can be used
  36. // as a fresh price to update the prices
  37. PythStructs.Price[] freshPrices;
  38. bytes[] freshPricesUpdateData;
  39. uint freshPricesUpdateFee;
  40. uint64[] freshPricesPublishTimes;
  41. WormholeMerkleUpdate whMerkleUpdateDepth0;
  42. WormholeMerkleUpdate whMerkleUpdateDepth1;
  43. WormholeMerkleUpdate whMerkleUpdateDepth8;
  44. ThresholdMerkleUpdate thresholdMerkleUpdateDepth0;
  45. ThresholdMerkleUpdate thresholdMerkleUpdateDepth1;
  46. ThresholdMerkleUpdate thresholdMerkleUpdateDepth8;
  47. ThresholdUpdate thresholdUpdate;
  48. bytes nativeUpdate;
  49. uint64 sequence;
  50. function setUp() public {
  51. address payable wormhole = payable(
  52. setUpWormholeReceiver(NUM_GUARDIANS)
  53. );
  54. // Deploy experimental contract
  55. PythExperimental implementation = new PythExperimental();
  56. ERC1967Proxy proxy = new ERC1967Proxy(
  57. address(implementation),
  58. new bytes(0)
  59. );
  60. pyth = PythExperimental(address(proxy));
  61. uint16[] memory emitterChainIds = new uint16[](1);
  62. emitterChainIds[0] = PythTestUtils.SOURCE_EMITTER_CHAIN_ID;
  63. bytes32[] memory emitterAddresses = new bytes32[](1);
  64. emitterAddresses[0] = PythTestUtils.SOURCE_EMITTER_ADDRESS;
  65. pyth.initialize(
  66. wormhole,
  67. vm.addr(THRESHOLD_KEY),
  68. emitterChainIds,
  69. emitterAddresses,
  70. PythTestUtils.GOVERNANCE_EMITTER_CHAIN_ID,
  71. PythTestUtils.GOVERNANCE_EMITTER_ADDRESS,
  72. 0, // Initial governance sequence
  73. 60, // Valid time period in seconds
  74. 1 // single update fee in wei
  75. );
  76. priceIds = new bytes32[](NUM_PRICES);
  77. priceIds[0] = bytes32(
  78. 0x1000000000000000000000000000000000000000000000000000000000000f00
  79. );
  80. for (uint i = 1; i < NUM_PRICES; ++i) {
  81. priceIds[i] = bytes32(uint256(priceIds[i - 1]) + 1);
  82. }
  83. setRandSeed(12345);
  84. for (uint i = 0; i < NUM_PRICES; ++i) {
  85. uint64 publishTime = uint64(getRandUint() % 10) + 1; // to make sure prevPublishTime is >= 0
  86. cachedPrices.push(
  87. PythStructs.Price(
  88. int64(uint64(getRandUint() % 1000)), // Price
  89. uint64(getRandUint() % 100), // Confidence
  90. -5, // Expo
  91. publishTime
  92. )
  93. );
  94. cachedPricesPublishTimes.push(publishTime);
  95. publishTime += uint64(getRandUint() % 10);
  96. freshPrices.push(
  97. PythStructs.Price(
  98. int64(uint64(getRandUint() % 1000)), // Price
  99. uint64(getRandUint() % 100), // Confidence
  100. -5, // Expo
  101. publishTime
  102. )
  103. );
  104. freshPricesPublishTimes.push(publishTime);
  105. }
  106. // Populate the contract with the initial prices
  107. (
  108. cachedPricesUpdateData,
  109. cachedPricesUpdateFee
  110. ) = generateWormholeUpdateDataAndFee(cachedPrices);
  111. pyth.updatePriceFeeds{value: cachedPricesUpdateFee}(
  112. cachedPricesUpdateData
  113. );
  114. (
  115. freshPricesUpdateData,
  116. freshPricesUpdateFee
  117. ) = generateWormholeUpdateDataAndFee(freshPrices);
  118. // Generate the update payloads for the various verification systems
  119. whMerkleUpdateDepth0 = generateSingleWhMerkleUpdate(
  120. priceIds[0],
  121. freshPrices[0],
  122. 0
  123. );
  124. whMerkleUpdateDepth1 = generateSingleWhMerkleUpdate(
  125. priceIds[0],
  126. freshPrices[0],
  127. 1
  128. );
  129. whMerkleUpdateDepth8 = generateSingleWhMerkleUpdate(
  130. priceIds[0],
  131. freshPrices[0],
  132. 8
  133. );
  134. thresholdMerkleUpdateDepth0 = generateThresholdMerkleUpdate(
  135. priceIds[0],
  136. freshPrices[0],
  137. 0
  138. );
  139. thresholdMerkleUpdateDepth1 = generateThresholdMerkleUpdate(
  140. priceIds[0],
  141. freshPrices[0],
  142. 1
  143. );
  144. thresholdMerkleUpdateDepth8 = generateThresholdMerkleUpdate(
  145. priceIds[0],
  146. freshPrices[0],
  147. 8
  148. );
  149. thresholdUpdate = generateThresholdUpdate(priceIds[0], freshPrices[0]);
  150. nativeUpdate = generateMessagePayload(priceIds[0], freshPrices[0]);
  151. }
  152. // Get the payload for a wormhole batch price update
  153. function generateWormholeUpdateDataAndFee(
  154. PythStructs.Price[] memory prices
  155. ) internal returns (bytes[] memory updateData, uint updateFee) {
  156. bytes memory vaa = generateWhMerkleUpdate(
  157. pricesToPriceFeedMessages(priceIds, prices),
  158. MERKLE_TREE_DEPTH,
  159. NUM_GUARDIAN_SIGNERS
  160. );
  161. ++sequence;
  162. updateData = new bytes[](1);
  163. updateData[0] = vaa;
  164. updateFee = pyth.getUpdateFee(updateData);
  165. }
  166. // Helper function to serialize a single price update to bytes.
  167. // Returns a serialized PriceFeedMessage.
  168. function generateMessagePayload(
  169. bytes32 priceId,
  170. PythStructs.Price memory price
  171. ) internal returns (bytes memory data) {
  172. bytes32[] memory messagePriceIds = new bytes32[](1);
  173. messagePriceIds[0] = priceId;
  174. PythStructs.Price[] memory prices = new PythStructs.Price[](1);
  175. prices[0] = price;
  176. data = encodePriceFeedMessages(
  177. pricesToPriceFeedMessages(messagePriceIds, prices)
  178. )[0];
  179. return data;
  180. }
  181. // Helper function to generate a merkle proof of the given depth for the given price.
  182. // Note: this function assumes that the data is on the leftmost branch of the tree and
  183. // mocks the rest of the nodes (as the hash of the depth).
  184. function generateMerkleProof(
  185. bytes32 priceId,
  186. PythStructs.Price memory price,
  187. uint depth
  188. )
  189. internal
  190. returns (bytes32 root, bytes memory data, bytes32[] memory proof)
  191. {
  192. data = generateMessagePayload(priceId, price);
  193. bytes32 curNodeHash = keccak256(data);
  194. proof = new bytes32[](depth);
  195. for (uint i = 0; i < depth; ) {
  196. // pretend the ith sibling is just i
  197. proof[i] = keccak256(abi.encode(i));
  198. curNodeHash = keccak256(abi.encode(curNodeHash, proof[i]));
  199. unchecked {
  200. i++;
  201. }
  202. }
  203. root = curNodeHash;
  204. return (root, data, proof);
  205. }
  206. // Generate a wormhole-attested merkle proof with the given depth.
  207. function generateSingleWhMerkleUpdate(
  208. bytes32 priceId,
  209. PythStructs.Price memory price,
  210. uint depth
  211. ) internal returns (WormholeMerkleUpdate memory update) {
  212. (
  213. bytes32 root,
  214. bytes memory data,
  215. bytes32[] memory proof
  216. ) = generateMerkleProof(priceId, price, depth);
  217. bytes memory rootVaa = generateVaa(
  218. uint32(block.timestamp),
  219. PythTestUtils.SOURCE_EMITTER_CHAIN_ID,
  220. PythTestUtils.SOURCE_EMITTER_ADDRESS,
  221. sequence,
  222. bytes.concat(root),
  223. NUM_GUARDIAN_SIGNERS
  224. );
  225. ++sequence;
  226. return WormholeMerkleUpdate(rootVaa, data, proof);
  227. }
  228. // Generate a threshold-signed merkle proof with the given depth.
  229. function generateThresholdMerkleUpdate(
  230. bytes32 priceId,
  231. PythStructs.Price memory price,
  232. uint depth
  233. ) internal returns (ThresholdMerkleUpdate memory update) {
  234. (
  235. bytes32 root,
  236. bytes memory data,
  237. bytes32[] memory proof
  238. ) = generateMerkleProof(priceId, price, depth);
  239. data = generateMessagePayload(priceId, price);
  240. (uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, root);
  241. bytes memory signature = abi.encodePacked(r, s, v - 27);
  242. return ThresholdMerkleUpdate(signature, root, data, proof);
  243. }
  244. // Generate a threshold-signed price feed message.
  245. function generateThresholdUpdate(
  246. bytes32 priceId,
  247. PythStructs.Price memory price
  248. ) internal returns (ThresholdUpdate memory update) {
  249. bytes memory data = generateMessagePayload(priceId, price);
  250. bytes32 hash = keccak256(data);
  251. (uint8 v, bytes32 r, bytes32 s) = vm.sign(THRESHOLD_KEY, hash);
  252. bytes memory signature = abi.encodePacked(r, s, v - 27);
  253. return ThresholdUpdate(signature, data);
  254. }
  255. function testWhBatchUpdate() public {
  256. pyth.updatePriceFeeds{value: freshPricesUpdateFee}(
  257. freshPricesUpdateData
  258. );
  259. }
  260. function testUpdateWhMerkleProofDepth0() public {
  261. pyth.updatePriceFeedsWhMerkle(
  262. whMerkleUpdateDepth0.rootVaa,
  263. whMerkleUpdateDepth0.data,
  264. whMerkleUpdateDepth0.proof
  265. );
  266. }
  267. function testUpdateWhMerkleProofDepth1() public {
  268. pyth.updatePriceFeedsWhMerkle(
  269. whMerkleUpdateDepth1.rootVaa,
  270. whMerkleUpdateDepth1.data,
  271. whMerkleUpdateDepth1.proof
  272. );
  273. }
  274. function testUpdateWhMerkleProofDepth8() public {
  275. pyth.updatePriceFeedsWhMerkle(
  276. whMerkleUpdateDepth8.rootVaa,
  277. whMerkleUpdateDepth8.data,
  278. whMerkleUpdateDepth8.proof
  279. );
  280. }
  281. function testUpdateThresholdMerkleProofDepth0() public {
  282. pyth.updatePriceFeedsThresholdMerkle(
  283. thresholdMerkleUpdateDepth0.rootSignature,
  284. thresholdMerkleUpdateDepth0.rootHash,
  285. thresholdMerkleUpdateDepth0.data,
  286. thresholdMerkleUpdateDepth0.proof
  287. );
  288. }
  289. function testUpdateThresholdMerkleProofDepth1() public {
  290. pyth.updatePriceFeedsThresholdMerkle(
  291. thresholdMerkleUpdateDepth1.rootSignature,
  292. thresholdMerkleUpdateDepth1.rootHash,
  293. thresholdMerkleUpdateDepth1.data,
  294. thresholdMerkleUpdateDepth1.proof
  295. );
  296. }
  297. function testUpdateThresholdMerkleProofDepth8() public {
  298. pyth.updatePriceFeedsThresholdMerkle(
  299. thresholdMerkleUpdateDepth8.rootSignature,
  300. thresholdMerkleUpdateDepth8.rootHash,
  301. thresholdMerkleUpdateDepth8.data,
  302. thresholdMerkleUpdateDepth8.proof
  303. );
  304. }
  305. function testUpdateThreshold() public {
  306. pyth.updatePriceFeedsThreshold(
  307. thresholdUpdate.signature,
  308. thresholdUpdate.data
  309. );
  310. }
  311. function testUpdateNative() public {
  312. pyth.updatePriceFeedsNative(nativeUpdate);
  313. }
  314. }
  315. // Pyth contract extended with methods for other verification systems (merkle proofs / threshold signatures)
  316. contract PythExperimental is Pyth {
  317. address thresholdPublicKey;
  318. function initialize(
  319. address wormhole,
  320. address thresholdPublicKeyArg,
  321. uint16[] calldata dataSourceEmitterChainIds,
  322. bytes32[] calldata dataSourceEmitterAddresses,
  323. uint16 governanceEmitterChainId,
  324. bytes32 governanceEmitterAddress,
  325. uint64 governanceInitialSequence,
  326. uint validTimePeriodSeconds,
  327. uint singleUpdateFeeInWei
  328. ) public {
  329. thresholdPublicKey = thresholdPublicKeyArg;
  330. Pyth._initialize(
  331. wormhole,
  332. dataSourceEmitterChainIds,
  333. dataSourceEmitterAddresses,
  334. governanceEmitterChainId,
  335. governanceEmitterAddress,
  336. governanceInitialSequence,
  337. validTimePeriodSeconds,
  338. singleUpdateFeeInWei
  339. );
  340. }
  341. // Update a single price feed via a wormhole-attested merkle proof.
  342. // data is expected to be a serialized PriceFeedMessage
  343. function updatePriceFeedsWhMerkle(
  344. bytes calldata rootVaa,
  345. bytes memory data,
  346. bytes32[] memory proof
  347. ) public payable {
  348. IWormhole.VM memory vm = parseAndVerifyBatchMessageVM(rootVaa);
  349. assert(vm.payload.length == 32);
  350. bytes32 expectedRoot = UnsafeBytesLib.toBytes32(vm.payload, 0);
  351. bool validProof = isValidMerkleProof(expectedRoot, data, proof);
  352. if (!validProof) revert PythErrors.InvalidArgument();
  353. (
  354. PythInternalStructs.PriceInfo memory info,
  355. bytes32 priceId,
  356. ) = parsePriceFeedMessageFromMemory(data, 0);
  357. updateLatestPriceIfNecessary(priceId, info);
  358. }
  359. function parseAndVerifyBatchMessageVM(
  360. bytes calldata encodedVm
  361. ) internal view returns (IWormhole.VM memory vm) {
  362. {
  363. bool valid;
  364. (vm, valid, ) = wormhole().parseAndVerifyVM(encodedVm);
  365. if (!valid) revert PythErrors.InvalidWormholeVaa();
  366. }
  367. if (!verifyPythVM(vm)) revert PythErrors.InvalidUpdateDataSource();
  368. }
  369. function verifyPythVM(
  370. IWormhole.VM memory vm
  371. ) private view returns (bool valid) {
  372. return isValidDataSource(vm.emitterChainId, vm.emitterAddress);
  373. }
  374. // Update a single price feed via a threshold-signed merkle proof.
  375. // data is expected to be a serialized PriceFeedMessage
  376. function updatePriceFeedsThresholdMerkle(
  377. bytes memory rootSignature,
  378. bytes32 rootHash,
  379. bytes memory data,
  380. bytes32[] memory proof
  381. ) public payable {
  382. if (!verifySignature(rootHash, rootSignature, thresholdPublicKey))
  383. revert PythErrors.InvalidArgument();
  384. bool validProof = isValidMerkleProof(rootHash, data, proof);
  385. if (!validProof) revert PythErrors.InvalidArgument();
  386. (
  387. PythInternalStructs.PriceInfo memory info,
  388. bytes32 priceId,
  389. ) = parsePriceFeedMessageFromMemory(data, 0);
  390. updateLatestPriceIfNecessary(priceId, info);
  391. }
  392. // Update a single price feed via a threshold-signed price update.
  393. // data is expected to be a serialized PriceFeedMessage.
  394. function updatePriceFeedsThreshold(
  395. bytes memory signature,
  396. bytes memory data
  397. ) public payable {
  398. bytes32 hash = keccak256(data);
  399. if (!verifySignature(hash, signature, thresholdPublicKey))
  400. revert PythErrors.InvalidArgument();
  401. (
  402. PythInternalStructs.PriceInfo memory info,
  403. bytes32 priceId,
  404. ) = parsePriceFeedMessageFromMemory(data, 0);
  405. updateLatestPriceIfNecessary(priceId, info);
  406. }
  407. // Update a single price feed via a "native" price update (i.e., using the default ethereum tx signature for authentication).
  408. // data is expected to be a serialized PriceFeedMessage.
  409. // This function represents the lower bound on how much gas we can use.
  410. function updatePriceFeedsNative(bytes memory data) public payable {
  411. // TODO: this function should have a check on the sender.
  412. // I'm assuming that check is not very expensive here.
  413. (
  414. PythInternalStructs.PriceInfo memory info,
  415. bytes32 priceId,
  416. ) = parsePriceFeedMessageFromMemory(data, 0);
  417. updateLatestPriceIfNecessary(priceId, info);
  418. }
  419. function parsePriceFeedMessageFromMemory(
  420. bytes memory encodedPriceFeed,
  421. uint offset
  422. )
  423. internal
  424. pure
  425. returns (
  426. PythInternalStructs.PriceInfo memory priceInfo,
  427. bytes32 priceId,
  428. uint64 prevPublishTime
  429. )
  430. {
  431. unchecked {
  432. priceId = UnsafeBytesLib.toBytes32(encodedPriceFeed, offset);
  433. offset += 32;
  434. priceInfo.price = int64(
  435. UnsafeBytesLib.toUint64(encodedPriceFeed, offset)
  436. );
  437. offset += 8;
  438. priceInfo.conf = UnsafeBytesLib.toUint64(encodedPriceFeed, offset);
  439. offset += 8;
  440. priceInfo.expo = int32(
  441. UnsafeBytesLib.toUint32(encodedPriceFeed, offset)
  442. );
  443. offset += 4;
  444. // Publish time is i64 in some environments due to the standard in that
  445. // environment. This would not cause any problem because since the signed
  446. // integer is represented in two's complement, the value would be the same
  447. // in both cases (for a million year at least)
  448. priceInfo.publishTime = UnsafeBytesLib.toUint64(
  449. encodedPriceFeed,
  450. offset
  451. );
  452. offset += 8;
  453. // We do not store this field because it is not used on the latest feed queries.
  454. prevPublishTime = UnsafeBytesLib.toUint64(encodedPriceFeed, offset);
  455. offset += 8;
  456. priceInfo.emaPrice = int64(
  457. UnsafeBytesLib.toUint64(encodedPriceFeed, offset)
  458. );
  459. offset += 8;
  460. priceInfo.emaConf = UnsafeBytesLib.toUint64(
  461. encodedPriceFeed,
  462. offset
  463. );
  464. offset += 8;
  465. if (offset > encodedPriceFeed.length)
  466. revert PythErrors.InvalidUpdateData();
  467. }
  468. }
  469. // Verify that signature is a valid ECDSA signature of messageHash by signer.
  470. function verifySignature(
  471. bytes32 messageHash,
  472. bytes memory signature,
  473. address signer
  474. ) public pure returns (bool) {
  475. bytes32 r;
  476. bytes32 s;
  477. uint8 v;
  478. assembly {
  479. r := mload(add(signature, 32))
  480. s := mload(add(signature, 64))
  481. v := byte(0, mload(add(signature, 96)))
  482. }
  483. if (v < 27) {
  484. v += 27;
  485. }
  486. if (v != 27 && v != 28) {
  487. return false;
  488. }
  489. address recoveredAddress = ecrecover(messageHash, v, r, s);
  490. return (recoveredAddress == signer);
  491. }
  492. // Check that proof is a valid merkle proof of data. This function assumes that
  493. // data is the leftmost node of the tree.
  494. // TODO: need to encode left/right structure for proof nodes
  495. function isValidMerkleProof(
  496. bytes32 expectedRoot,
  497. bytes memory data,
  498. bytes32[] memory proof
  499. ) public pure returns (bool) {
  500. bytes32 curNodeHash = keccak256(data);
  501. for (uint i = 0; i < proof.length; ) {
  502. curNodeHash = keccak256(abi.encode(curNodeHash, proof[i]));
  503. unchecked {
  504. i++;
  505. }
  506. }
  507. return (expectedRoot == curNodeHash);
  508. }
  509. }
  510. // A merkle tree price update delivered via wormhole.
  511. // The update is valid if the data above hashed with the proof nodes sequentially
  512. // equals the root hash in the wormhole VAA.
  513. struct WormholeMerkleUpdate {
  514. // The serialized bytes of a wormhole VAA
  515. // The payload of this VAA is a single 32-byte root hash for the merkle tree.
  516. bytes rootVaa;
  517. // The serialized bytes of a PriceFeedMessage
  518. bytes data;
  519. // The chain of proof nodes.
  520. bytes32[] proof;
  521. }
  522. // A merkle tree price update delivered via threshold signature.
  523. // The update is valid if the data above hashed with the proof nodes sequentially
  524. // equals the root hash, and the signature is valid for rootHash.
  525. struct ThresholdMerkleUpdate {
  526. bytes rootSignature;
  527. bytes32 rootHash;
  528. // The serialized bytes of a PriceFeedMessage
  529. bytes data;
  530. // The chain of proof nodes.
  531. bytes32[] proof;
  532. }
  533. // A merkle tree price update delivered via threshold signature.
  534. // The update is valid if the data above hashed with the proof nodes sequentially
  535. // equals the root hash, and the signature is valid for rootHash.
  536. struct ThresholdUpdate {
  537. // Signature of the hash of the data.
  538. bytes signature;
  539. // The serialized bytes of a PriceFeedMessage
  540. bytes data;
  541. }