Skip to content

Commit b232581

Browse files
authored
Remove middle structs (#375)
* Remove middle structs * Parse and process attestation in place It helps a lot because we won't expand memory anymore * Update comments * Remove unusued PriceAttestation struct
1 parent 61651a1 commit b232581

File tree

4 files changed

+150
-221
lines changed

4 files changed

+150
-221
lines changed

ethereum/contracts/pyth/Pyth.sol

Lines changed: 118 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,13 @@ abstract contract Pyth is PythGetters, PythSetters, AbstractPyth {
2424
setPyth2WormholeEmitter(pyth2WormholeEmitter);
2525
}
2626

27-
function updatePriceBatchFromVm(bytes calldata encodedVm) private returns (PythInternalStructs.BatchPriceAttestation memory bpa) {
27+
function updatePriceBatchFromVm(bytes calldata encodedVm) private {
2828
(IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
2929

3030
require(valid, reason);
3131
require(verifyPythVM(vm), "invalid data source chain/emitter ID");
3232

33-
PythInternalStructs.BatchPriceAttestation memory batch = parseBatchPriceAttestation(vm.payload);
34-
35-
uint freshPrices = 0;
36-
37-
for (uint i = 0; i < batch.attestations.length; i++) {
38-
PythInternalStructs.PriceAttestation memory attestation = batch.attestations[i];
39-
40-
PythInternalStructs.PriceInfo memory newPriceInfo = createNewPriceInfo(attestation);
41-
PythInternalStructs.PriceInfo memory latestPrice = latestPriceInfo(attestation.priceId);
42-
43-
bool fresh = false;
44-
if(newPriceInfo.price.publishTime > latestPrice.price.publishTime) {
45-
freshPrices += 1;
46-
fresh = true;
47-
setLatestPriceInfo(attestation.priceId, newPriceInfo);
48-
}
49-
50-
emit PriceFeedUpdate(attestation.priceId, fresh, vm.emitterChainId, vm.sequence, latestPrice.price.publishTime,
51-
newPriceInfo.price.publishTime, newPriceInfo.price.price, newPriceInfo.price.conf);
52-
}
53-
54-
emit BatchPriceFeedUpdate(vm.emitterChainId, vm.sequence, batch.attestations.length, freshPrices);
55-
56-
return batch;
33+
parseAndProcessBatchPriceAttestation(vm);
5734
}
5835

5936
function updatePriceFeeds(bytes[] calldata updateData) public override payable {
@@ -76,146 +53,163 @@ abstract contract Pyth is PythGetters, PythSetters, AbstractPyth {
7653
return singleUpdateFeeInWei() * updateData.length;
7754
}
7855

79-
function createNewPriceInfo(PythInternalStructs.PriceAttestation memory pa) private pure returns (PythInternalStructs.PriceInfo memory info) {
80-
PythInternalStructs.PriceAttestationStatus status = PythInternalStructs.PriceAttestationStatus(pa.status);
81-
if (status == PythInternalStructs.PriceAttestationStatus.TRADING) {
82-
info.price.price = pa.price;
83-
info.price.conf = pa.conf;
84-
info.price.publishTime = pa.publishTime;
85-
info.emaPrice.publishTime = pa.publishTime;
86-
} else {
87-
info.price.price = pa.prevPrice;
88-
info.price.conf = pa.prevConf;
89-
info.price.publishTime = pa.prevPublishTime;
90-
91-
// The EMA is last updated when the aggregate had trading status,
92-
// so, we use prev_publish_time (the time when the aggregate last had trading status).
93-
info.emaPrice.publishTime = pa.prevPublishTime;
94-
}
95-
96-
info.price.expo = pa.expo;
97-
info.emaPrice.price = pa.emaPrice;
98-
info.emaPrice.conf = pa.emaConf;
99-
info.emaPrice.expo = pa.expo;
100-
101-
return info;
102-
}
103-
10456
function verifyPythVM(IWormhole.VM memory vm) private view returns (bool valid) {
10557
return isValidDataSource(vm.emitterChainId, vm.emitterAddress);
10658
}
10759

108-
109-
function parseBatchPriceAttestation(bytes memory encoded) public pure returns (PythInternalStructs.BatchPriceAttestation memory bpa) {
60+
function parseAndProcessBatchPriceAttestation(IWormhole.VM memory vm) internal {
61+
bytes memory encoded = vm.payload;
11062
uint index = 0;
11163

11264
// Check header
113-
bpa.header.magic = encoded.toUint32(index);
114-
index += 4;
115-
require(bpa.header.magic == 0x50325748, "invalid magic value");
116-
117-
bpa.header.versionMajor = encoded.toUint16(index);
118-
index += 2;
119-
require(bpa.header.versionMajor == 3, "invalid version major, expected 3");
120-
121-
bpa.header.versionMinor = encoded.toUint16(index);
122-
index += 2;
123-
require(bpa.header.versionMinor >= 0, "invalid version minor, expected 0 or more");
124-
125-
bpa.header.hdrSize = encoded.toUint16(index);
126-
index += 2;
127-
128-
// NOTE(2022-04-19): Currently, only payloadId comes after
129-
// hdrSize. Future extra header fields must be read using a
130-
// separate offset to respect hdrSize, i.e.:
131-
//
132-
// uint hdrIndex = 0;
133-
// bpa.header.payloadId = encoded.toUint8(index + hdrIndex);
134-
// hdrIndex += 1;
135-
//
136-
// bpa.header.someNewField = encoded.toUint32(index + hdrIndex);
137-
// hdrIndex += 4;
138-
//
139-
// // Skip remaining unknown header bytes
140-
// index += bpa.header.hdrSize;
141-
142-
bpa.header.payloadId = encoded.toUint8(index);
143-
144-
// Skip remaining unknown header bytes
145-
index += bpa.header.hdrSize;
146-
147-
// Payload ID of 2 required for batch headerBa
148-
require(bpa.header.payloadId == 2, "invalid payload ID, expected 2 for BatchPriceAttestation");
65+
{
66+
uint32 magic = encoded.toUint32(index);
67+
index += 4;
68+
require(magic == 0x50325748, "invalid magic value");
69+
70+
uint16 versionMajor = encoded.toUint16(index);
71+
index += 2;
72+
require(versionMajor == 3, "invalid version major, expected 3");
73+
74+
uint16 versionMinor = encoded.toUint16(index);
75+
index += 2;
76+
require(versionMinor >= 0, "invalid version minor, expected 0 or more");
77+
78+
uint16 hdrSize = encoded.toUint16(index);
79+
index += 2;
80+
81+
// NOTE(2022-04-19): Currently, only payloadId comes after
82+
// hdrSize. Future extra header fields must be read using a
83+
// separate offset to respect hdrSize, i.e.:
84+
//
85+
// uint hdrIndex = 0;
86+
// bpa.header.payloadId = encoded.toUint8(index + hdrIndex);
87+
// hdrIndex += 1;
88+
//
89+
// bpa.header.someNewField = encoded.toUint32(index + hdrIndex);
90+
// hdrIndex += 4;
91+
//
92+
// // Skip remaining unknown header bytes
93+
// index += bpa.header.hdrSize;
94+
95+
uint8 payloadId = encoded.toUint8(index);
96+
97+
// Skip remaining unknown header bytes
98+
index += hdrSize;
99+
100+
// Payload ID of 2 required for batch headerBa
101+
require(payloadId == 2, "invalid payload ID, expected 2 for BatchPriceAttestation");
102+
}
149103

150104
// Parse the number of attestations
151-
bpa.nAttestations = encoded.toUint16(index);
105+
uint16 nAttestations = encoded.toUint16(index);
152106
index += 2;
153107

154108
// Parse the attestation size
155-
bpa.attestationSize = encoded.toUint16(index);
109+
uint16 attestationSize = encoded.toUint16(index);
156110
index += 2;
157-
require(encoded.length == (index + (bpa.attestationSize * bpa.nAttestations)), "invalid BatchPriceAttestation size");
158-
159-
bpa.attestations = new PythInternalStructs.PriceAttestation[](bpa.nAttestations);
111+
require(encoded.length == (index + (attestationSize * nAttestations)), "invalid BatchPriceAttestation size");
160112

113+
PythInternalStructs.PriceInfo memory info;
114+
bytes32 priceId;
115+
uint freshPrices = 0;
116+
161117
// Deserialize each attestation
162-
for (uint j=0; j < bpa.nAttestations; j++) {
118+
for (uint j=0; j < nAttestations; j++) {
163119
// NOTE: We don't advance the global index immediately.
164120
// attestationIndex is an attestation-local offset used
165121
// for readability and easier debugging.
166122
uint attestationIndex = 0;
167123

168-
// Attestation
169-
bpa.attestations[j].productId = encoded.toBytes32(index + attestationIndex);
124+
// Unused bytes32 product id
170125
attestationIndex += 32;
171126

172-
bpa.attestations[j].priceId = encoded.toBytes32(index + attestationIndex);
127+
priceId = encoded.toBytes32(index + attestationIndex);
173128
attestationIndex += 32;
174129

175-
bpa.attestations[j].price = int64(encoded.toUint64(index + attestationIndex));
130+
info.price.price = int64(encoded.toUint64(index + attestationIndex));
176131
attestationIndex += 8;
177132

178-
bpa.attestations[j].conf = encoded.toUint64(index + attestationIndex);
133+
info.price.conf = encoded.toUint64(index + attestationIndex);
179134
attestationIndex += 8;
180135

181-
bpa.attestations[j].expo = int32(encoded.toUint32(index + attestationIndex));
136+
info.price.expo = int32(encoded.toUint32(index + attestationIndex));
137+
info.emaPrice.expo = info.price.expo;
182138
attestationIndex += 4;
183139

184-
bpa.attestations[j].emaPrice = int64(encoded.toUint64(index + attestationIndex));
140+
info.emaPrice.price = int64(encoded.toUint64(index + attestationIndex));
185141
attestationIndex += 8;
186142

187-
bpa.attestations[j].emaConf = encoded.toUint64(index + attestationIndex);
143+
info.emaPrice.conf = encoded.toUint64(index + attestationIndex);
188144
attestationIndex += 8;
189145

190-
bpa.attestations[j].status = encoded.toUint8(index + attestationIndex);
191-
attestationIndex += 1;
192-
193-
bpa.attestations[j].numPublishers = encoded.toUint32(index + attestationIndex);
194-
attestationIndex += 4;
195-
196-
bpa.attestations[j].maxNumPublishers = encoded.toUint32(index + attestationIndex);
197-
attestationIndex += 4;
146+
{
147+
// Status is an enum (encoded as uint8) with the following values:
148+
// 0 = UNKNOWN: The price feed is not currently updating for an unknown reason.
149+
// 1 = TRADING: The price feed is updating as expected.
150+
// 2 = HALTED: The price feed is not currently updating because trading in the product has been halted.
151+
// 3 = AUCTION: The price feed is not currently updating because an auction is setting the price.
152+
uint8 status = encoded.toUint8(index + attestationIndex);
153+
attestationIndex += 1;
154+
155+
// Unused uint32 numPublishers
156+
attestationIndex += 4;
157+
158+
// Unused uint32 numPublishers
159+
attestationIndex += 4;
160+
161+
// Unused uint64 attestationTime
162+
attestationIndex += 8;
163+
164+
info.price.publishTime = encoded.toUint64(index + attestationIndex);
165+
info.emaPrice.publishTime = info.price.publishTime;
166+
attestationIndex += 8;
167+
168+
if (status == 1) { // status == TRADING
169+
attestationIndex += 24;
170+
} else {
171+
// If status is not trading then the latest available price is
172+
// the previous price info that are passed here.
173+
174+
// Previous publish time
175+
info.price.publishTime = encoded.toUint64(index + attestationIndex);
176+
attestationIndex += 8;
177+
178+
// Previous price
179+
info.price.price = int64(encoded.toUint64(index + attestationIndex));
180+
attestationIndex += 8;
181+
182+
// Previous confidence
183+
info.price.conf = encoded.toUint64(index + attestationIndex);
184+
attestationIndex += 8;
185+
186+
// The EMA is last updated when the aggregate had trading status,
187+
// so, we use previous publish time here too.
188+
info.emaPrice.publishTime = info.price.publishTime;
189+
}
190+
}
198191

199-
bpa.attestations[j].attestationTime = encoded.toUint64(index + attestationIndex);
200-
attestationIndex += 8;
192+
require(attestationIndex <= attestationSize, "INTERNAL: Consumed more than `attestationSize` bytes");
201193

202-
bpa.attestations[j].publishTime = encoded.toUint64(index + attestationIndex);
203-
attestationIndex += 8;
194+
// Respect specified attestation size for forward-compat
195+
index += attestationSize;
204196

205-
bpa.attestations[j].prevPublishTime = encoded.toUint64(index + attestationIndex);
206-
attestationIndex += 8;
197+
// Store the attestation
198+
PythInternalStructs.PriceInfo memory latestPrice = latestPriceInfo(priceId);
207199

208-
bpa.attestations[j].prevPrice = int64(encoded.toUint64(index + attestationIndex));
209-
attestationIndex += 8;
200+
bool fresh = false;
201+
if(info.price.publishTime > latestPrice.price.publishTime) {
202+
freshPrices += 1;
203+
fresh = true;
204+
setLatestPriceInfo(priceId, info);
205+
}
210206

211-
bpa.attestations[j].prevConf = encoded.toUint64(index + attestationIndex);
212-
attestationIndex += 8;
207+
emit PriceFeedUpdate(priceId, fresh, vm.emitterChainId, vm.sequence, latestPrice.price.publishTime,
208+
info.price.publishTime, info.price.price, info.price.conf);
209+
}
213210

214-
require(attestationIndex <= bpa.attestationSize, "INTERNAL: Consumed more than `attestationSize` bytes");
215211

216-
// Respect specified attestation size for forward-compat
217-
index += bpa.attestationSize;
218-
}
212+
emit BatchPriceFeedUpdate(vm.emitterChainId, vm.sequence, nAttestations, freshPrices);
219213
}
220214

221215
function queryPriceFeed(bytes32 id) public view override returns (PythStructs.PriceFeed memory priceFeed){

ethereum/contracts/pyth/PythInternalStructs.sol

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,6 @@ import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
99
contract PythInternalStructs {
1010
using BytesLib for bytes;
1111

12-
struct BatchPriceAttestation {
13-
Header header;
14-
15-
uint16 nAttestations;
16-
uint16 attestationSize;
17-
PriceAttestation[] attestations;
18-
}
19-
20-
struct Header {
21-
uint32 magic;
22-
uint16 versionMajor;
23-
uint16 versionMinor;
24-
uint16 hdrSize;
25-
uint8 payloadId;
26-
}
27-
28-
struct PriceAttestation {
29-
bytes32 productId;
30-
bytes32 priceId;
31-
int64 price;
32-
uint64 conf;
33-
int32 expo;
34-
int64 emaPrice;
35-
uint64 emaConf;
36-
uint8 status;
37-
uint32 numPublishers;
38-
uint32 maxNumPublishers;
39-
uint64 attestationTime;
40-
uint64 publishTime;
41-
uint64 prevPublishTime;
42-
int64 prevPrice;
43-
uint64 prevConf;
44-
}
45-
4612
struct InternalPrice {
4713
int64 price;
4814
uint64 conf;
@@ -62,17 +28,4 @@ contract PythInternalStructs {
6228
uint16 chainId;
6329
bytes32 emitterAddress;
6430
}
65-
66-
/* PriceAttestationStatus represents the availability status of a price feed passed down in attestation.
67-
UNKNOWN: The price feed is not currently updating for an unknown reason.
68-
TRADING: The price feed is updating as expected.
69-
HALTED: The price feed is not currently updating because trading in the product has been halted.
70-
AUCTION: The price feed is not currently updating because an auction is setting the price.
71-
*/
72-
enum PriceAttestationStatus {
73-
UNKNOWN,
74-
TRADING,
75-
HALTED,
76-
AUCTION
77-
}
7831
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ abstract contract PythTestUtils is Test, WormholeTestUtils {
8080
// Breaking this in two encodePackes because of the limited EVM stack.
8181
attestations = abi.encodePacked(
8282
attestations,
83-
uint8(PythInternalStructs.PriceAttestationStatus.TRADING),
83+
uint8(1), // status = 1 = Trading
8484
uint32(5), // Number of publishers. This field is not used.
8585
uint32(10), // Maximum number of publishers. This field is not used.
8686
uint64(prices[i].publishTime), // Attestation time. This field is not used.

0 commit comments

Comments
 (0)