Skip to content

Commit bc338cc

Browse files
authored
[eth]: add forward compatibility test for accumulator updateData (#884)
* test(ethereum): add forward compatibility test for accumulator data * test(ethereum): fix typo and rename
1 parent 3ffcf12 commit bc338cc

File tree

2 files changed

+200
-2
lines changed

2 files changed

+200
-2
lines changed

target_chains/ethereum/contracts/forge-test/Pyth.WormholeMerkleAccumulator.t.sol

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ contract PythWormholeMerkleAccumulatorTest is
8585
assertEq(emaPrice.publishTime, priceFeed.emaPrice.publishTime);
8686
}
8787

88+
function assertPriceFeedEqual(
89+
PythStructs.PriceFeed memory priceFeed1,
90+
PythStructs.PriceFeed memory priceFeed2
91+
) internal {
92+
assertEq(priceFeed1.id, priceFeed2.id);
93+
assertEq(priceFeed1.price.price, priceFeed2.price.price);
94+
assertEq(priceFeed1.price.conf, priceFeed2.price.conf);
95+
assertEq(priceFeed1.price.expo, priceFeed2.price.expo);
96+
assertEq(priceFeed1.price.publishTime, priceFeed2.price.publishTime);
97+
assertEq(priceFeed1.emaPrice.price, priceFeed2.emaPrice.price);
98+
assertEq(priceFeed1.emaPrice.conf, priceFeed2.emaPrice.conf);
99+
assertEq(priceFeed1.emaPrice.expo, priceFeed2.emaPrice.expo);
100+
assertEq(
101+
priceFeed1.emaPrice.publishTime,
102+
priceFeed2.emaPrice.publishTime
103+
);
104+
}
105+
88106
function generateRandomPriceFeedMessage(
89107
uint numPriceFeeds
90108
) internal returns (PriceFeedMessage[] memory priceFeedMessages) {
@@ -120,6 +138,36 @@ contract PythWormholeMerkleAccumulatorTest is
120138
updateFee = pyth.getUpdateFee(updateData);
121139
}
122140

141+
/// @notice This method creates a forward compatible wormhole update data by using a newer minor version,
142+
/// setting a trailing header size and generating additional trailing header data of size `trailingHeaderSize`
143+
function createFowardCompatibleWormholeMerkleUpdateData(
144+
PriceFeedMessage[] memory priceFeedMessages,
145+
uint8 minorVersion,
146+
uint8 trailingHeaderSize
147+
) internal returns (bytes[] memory updateData, uint updateFee) {
148+
updateData = new bytes[](1);
149+
150+
uint8 depth = 0;
151+
while ((1 << depth) < priceFeedMessages.length) {
152+
depth++;
153+
}
154+
155+
depth += getRandUint8() % 3;
156+
bytes memory trailingHeaderData = new bytes(uint8(0));
157+
for (uint i = 0; i < trailingHeaderSize; i++) {
158+
trailingHeaderData = abi.encodePacked(trailingHeaderData, uint8(i));
159+
}
160+
updateData[0] = generateForwardCompatibleWhMerkleUpdate(
161+
priceFeedMessages,
162+
depth,
163+
1,
164+
minorVersion,
165+
trailingHeaderData
166+
);
167+
168+
updateFee = pyth.getUpdateFee(updateData);
169+
}
170+
123171
/// Testing update price feeds method using wormhole merkle update type.
124172
function testUpdatePriceFeedWithWormholeMerkleWorks(uint seed) public {
125173
setRandSeed(seed);
@@ -947,6 +995,77 @@ contract PythWormholeMerkleAccumulatorTest is
947995
assertEq(updateFee, SINGLE_UPDATE_FEE_IN_WEI * numPriceFeeds);
948996
}
949997

950-
//TODO: add some tests of forward compatibility.
951-
// I.e., create a message where each part that can be expanded in size is expanded and make sure that parsing still works
998+
function testParsePriceFeedUpdatesWithWhMerkleUpdateWorksForForwardCompatibility()
999+
public
1000+
{
1001+
uint numPriceFeeds = (getRandUint() % 10) + 1;
1002+
PriceFeedMessage[]
1003+
memory priceFeedMessages = generateRandomPriceFeedMessage(
1004+
numPriceFeeds
1005+
);
1006+
(
1007+
bytes[] memory updateData,
1008+
uint updateFee
1009+
) = createWormholeMerkleUpdateData(priceFeedMessages);
1010+
1011+
bytes32[] memory priceIds = new bytes32[](numPriceFeeds);
1012+
for (uint i = 0; i < numPriceFeeds; i++) {
1013+
priceIds[i] = priceFeedMessages[i].priceId;
1014+
}
1015+
PythStructs.PriceFeed[] memory priceFeeds = pyth.parsePriceFeedUpdates{
1016+
value: updateFee
1017+
}(updateData, priceIds, 0, MAX_UINT64);
1018+
uint8 futureMinorVersion = uint8(2);
1019+
uint8 futureTrailingHeaderSize = uint8(20);
1020+
(
1021+
bytes[] memory updateDataFromFuture,
1022+
uint updateFeeFromFuture
1023+
) = createFowardCompatibleWormholeMerkleUpdateData(
1024+
priceFeedMessages,
1025+
futureMinorVersion,
1026+
futureTrailingHeaderSize
1027+
);
1028+
1029+
PythStructs.PriceFeed[] memory priceFeedsFromFutureUpdateData = pyth
1030+
.parsePriceFeedUpdates{value: updateFeeFromFuture}(
1031+
updateDataFromFuture,
1032+
priceIds,
1033+
0,
1034+
MAX_UINT64
1035+
);
1036+
assertEq(updateFee, updateFeeFromFuture);
1037+
1038+
for (uint i = 0; i < priceFeeds.length; i++) {
1039+
assertPriceFeedEqual(
1040+
priceFeeds[i],
1041+
priceFeedsFromFutureUpdateData[i]
1042+
);
1043+
}
1044+
}
1045+
1046+
function testUpdatePriceFeedUpdatesWithWhMerkleUpdateWorksForForwardCompatibility()
1047+
public
1048+
{
1049+
uint numPriceFeeds = (getRandUint() % 10) + 1;
1050+
PriceFeedMessage[]
1051+
memory priceFeedMessages = generateRandomPriceFeedMessage(
1052+
numPriceFeeds
1053+
);
1054+
uint8 futureMinorVersion = uint8(2);
1055+
uint8 futureTrailingHeaderSize = uint8(20);
1056+
(
1057+
bytes[] memory forwardCompatibleUpdateData,
1058+
uint updateFee
1059+
) = createFowardCompatibleWormholeMerkleUpdateData(
1060+
priceFeedMessages,
1061+
futureMinorVersion,
1062+
futureTrailingHeaderSize
1063+
);
1064+
1065+
pyth.updatePriceFeeds{value: updateFee}(forwardCompatibleUpdateData);
1066+
1067+
for (uint i = 0; i < priceFeedMessages.length; i++) {
1068+
assertPriceFeedMessageStored(priceFeedMessages[i]);
1069+
}
1070+
}
9521071
}

target_chains/ethereum/contracts/forge-test/utils/PythTestUtils.t.sol

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,85 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
164164
}
165165
}
166166

167+
function generateForwardCompatibleWhMerkleUpdate(
168+
PriceFeedMessage[] memory priceFeedMessages,
169+
uint8 depth,
170+
uint8 numSigners,
171+
uint8 minorVersion,
172+
bytes memory trailingHeaderData
173+
) internal returns (bytes memory whMerkleUpdateData) {
174+
bytes[] memory encodedPriceFeedMessages = encodePriceFeedMessages(
175+
priceFeedMessages
176+
);
177+
178+
(bytes20 rootDigest, bytes[] memory proofs) = MerkleTree
179+
.constructProofs(encodedPriceFeedMessages, depth);
180+
// refactoring some of these generateWormhole functions was necessary
181+
// to workaround the stack too deep limit.
182+
bytes
183+
memory wormholeMerkleVaa = generateForwardCompatibleWormholeMerkleVaa(
184+
rootDigest,
185+
trailingHeaderData,
186+
numSigners
187+
);
188+
{
189+
whMerkleUpdateData = abi.encodePacked(
190+
generateForwardCompatibleWormholeMerkleUpdateHeader(
191+
minorVersion,
192+
trailingHeaderData
193+
),
194+
uint16(wormholeMerkleVaa.length),
195+
wormholeMerkleVaa,
196+
uint8(priceFeedMessages.length)
197+
);
198+
}
199+
200+
for (uint i = 0; i < priceFeedMessages.length; i++) {
201+
whMerkleUpdateData = abi.encodePacked(
202+
whMerkleUpdateData,
203+
uint16(encodedPriceFeedMessages[i].length),
204+
encodedPriceFeedMessages[i],
205+
proofs[i]
206+
);
207+
}
208+
}
209+
210+
function generateForwardCompatibleWormholeMerkleUpdateHeader(
211+
uint8 minorVersion,
212+
bytes memory trailingHeaderData
213+
) private returns (bytes memory whMerkleUpdateHeader) {
214+
whMerkleUpdateHeader = abi.encodePacked(
215+
uint32(0x504e4155), // PythAccumulator.ACCUMULATOR_MAGIC
216+
uint8(1), // major version
217+
minorVersion,
218+
uint8(trailingHeaderData.length), // trailing header size
219+
trailingHeaderData,
220+
uint8(PythAccumulator.UpdateType.WormholeMerkle)
221+
);
222+
}
223+
224+
function generateForwardCompatibleWormholeMerkleVaa(
225+
bytes20 rootDigest,
226+
bytes memory futureData,
227+
uint8 numSigners
228+
) internal returns (bytes memory wormholeMerkleVaa) {
229+
wormholeMerkleVaa = generateVaa(
230+
0,
231+
SOURCE_EMITTER_CHAIN_ID,
232+
SOURCE_EMITTER_ADDRESS,
233+
0,
234+
abi.encodePacked(
235+
uint32(0x41555756), // PythAccumulator.ACCUMULATOR_WORMHOLE_MAGIC
236+
uint8(PythAccumulator.UpdateType.WormholeMerkle),
237+
uint64(0), // Slot, not used in target networks
238+
uint32(0), // Ring size, not used in target networks
239+
rootDigest, // this can have bytes past this for future versions
240+
futureData
241+
),
242+
numSigners
243+
);
244+
}
245+
167246
// Generates byte-encoded payload for the given price attestations. You can use this to mock wormhole
168247
// call using `vm.mockCall` and return a VM struct with this payload.
169248
// You can use generatePriceFeedUpdate to generate a VAA for a price update.

0 commit comments

Comments
 (0)