Skip to content

Commit 46184e0

Browse files
feat(lazer/sui): better parsing and error handling (#3003)
* feat(lazer/sui): better parsing and error handling * nit: comments * remove invalid channel enum variant
1 parent 011c398 commit 46184e0

File tree

5 files changed

+291
-167
lines changed

5 files changed

+291
-167
lines changed

lazer/contracts/sui/sources/channel.move

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
module pyth_lazer::channel;
22

3+
// Error codes for channel parsing
4+
const EInvalidChannel: u64 = 1;
5+
36
public enum Channel has copy, drop {
4-
Invalid,
57
RealTime,
68
FixedRate50ms,
79
FixedRate200ms,
810
}
911

10-
/// Create a new Invalid channel
11-
public fun new_invalid(): Channel {
12-
Channel::Invalid
13-
}
14-
1512
/// Create a new RealTime channel
1613
public fun new_real_time(): Channel {
1714
Channel::RealTime
@@ -27,11 +24,16 @@ public fun new_fixed_rate_200ms(): Channel {
2724
Channel::FixedRate200ms
2825
}
2926

30-
/// Check if the channel is Invalid
31-
public fun is_invalid(channel: &Channel): bool {
32-
match (channel) {
33-
Channel::Invalid => true,
34-
_ => false,
27+
/// Parse channel from a channel value byte
28+
public fun from_u8(channel_value: u8): Channel {
29+
if (channel_value == 1) {
30+
new_real_time()
31+
} else if (channel_value == 2) {
32+
new_fixed_rate_50ms()
33+
} else if (channel_value == 3) {
34+
new_fixed_rate_200ms()
35+
} else {
36+
abort EInvalidChannel
3537
}
3638
}
3739

lazer/contracts/sui/sources/feed.move

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
module pyth_lazer::feed;
22

3-
use pyth_lazer::i16::I16;
4-
use pyth_lazer::i64::I64;
3+
use pyth_lazer::i16::{Self, I16};
4+
use pyth_lazer::i64::{Self, I64};
5+
use sui::bcs;
6+
7+
// Error codes for feed parsing
8+
const EInvalidProperty: u64 = 2;
59

610
/// The feed struct is based on the Lazer rust protocol definition defined here:
711
/// https://github.com/pyth-network/pyth-crosschain/blob/main/lazer/sdk/rust/protocol/src/payload.rs
@@ -56,7 +60,7 @@ public(package) fun new(
5660
confidence,
5761
funding_rate,
5862
funding_timestamp,
59-
funding_rate_interval
63+
funding_rate_interval,
6064
}
6165
}
6266

@@ -156,6 +160,115 @@ public(package) fun set_funding_timestamp(feed: &mut Feed, funding_timestamp: Op
156160
}
157161

158162
/// Set the funding rate interval
159-
public(package) fun set_funding_rate_interval(feed: &mut Feed, funding_rate_interval: Option<Option<u64>>) {
163+
public(package) fun set_funding_rate_interval(
164+
feed: &mut Feed,
165+
funding_rate_interval: Option<Option<u64>>,
166+
) {
160167
feed.funding_rate_interval = funding_rate_interval;
161168
}
169+
170+
/// Parse a feed from a BCS cursor
171+
public(package) fun parse_from_cursor(cursor: &mut bcs::BCS): Feed {
172+
let feed_id = cursor.peel_u32();
173+
let mut feed = new(
174+
feed_id,
175+
option::none(),
176+
option::none(),
177+
option::none(),
178+
option::none(),
179+
option::none(),
180+
option::none(),
181+
option::none(),
182+
option::none(),
183+
option::none(),
184+
);
185+
186+
let properties_count = cursor.peel_u8();
187+
let mut properties_i = 0;
188+
189+
while (properties_i < properties_count) {
190+
let property_id = cursor.peel_u8();
191+
192+
if (property_id == 0) {
193+
// Price property
194+
let price = cursor.peel_u64();
195+
if (price != 0) {
196+
feed.set_price(option::some(option::some(i64::from_u64(price))));
197+
} else {
198+
feed.set_price(option::some(option::none()));
199+
}
200+
} else if (property_id == 1) {
201+
// Best bid price property
202+
let best_bid_price = cursor.peel_u64();
203+
if (best_bid_price != 0) {
204+
feed.set_best_bid_price(
205+
option::some(option::some(i64::from_u64(best_bid_price))),
206+
);
207+
} else {
208+
feed.set_best_bid_price(option::some(option::none()));
209+
}
210+
} else if (property_id == 2) {
211+
// Best ask price property
212+
let best_ask_price = cursor.peel_u64();
213+
if (best_ask_price != 0) {
214+
feed.set_best_ask_price(
215+
option::some(option::some(i64::from_u64(best_ask_price))),
216+
);
217+
} else {
218+
feed.set_best_ask_price(option::some(option::none()));
219+
}
220+
} else if (property_id == 3) {
221+
// Publisher count property
222+
let publisher_count = cursor.peel_u16();
223+
feed.set_publisher_count(option::some(publisher_count));
224+
} else if (property_id == 4) {
225+
// Exponent property
226+
let exponent = cursor.peel_u16();
227+
feed.set_exponent(option::some(i16::from_u16(exponent)));
228+
} else if (property_id == 5) {
229+
// Confidence property
230+
let confidence = cursor.peel_u64();
231+
if (confidence != 0) {
232+
feed.set_confidence(option::some(option::some(i64::from_u64(confidence))));
233+
} else {
234+
feed.set_confidence(option::some(option::none()));
235+
}
236+
} else if (property_id == 6) {
237+
// Funding rate property
238+
let exists = cursor.peel_bool();
239+
if (exists) {
240+
let funding_rate = cursor.peel_u64();
241+
feed.set_funding_rate(option::some(option::some(i64::from_u64(funding_rate))));
242+
} else {
243+
feed.set_funding_rate(option::some(option::none()));
244+
}
245+
} else if (property_id == 7) {
246+
// Funding timestamp property
247+
let exists = cursor.peel_bool();
248+
if (exists) {
249+
let funding_timestamp = cursor.peel_u64();
250+
feed.set_funding_timestamp(option::some(option::some(funding_timestamp)));
251+
} else {
252+
feed.set_funding_timestamp(option::some(option::none()));
253+
}
254+
} else if (property_id == 8) {
255+
// Funding rate interval property
256+
let exists = cursor.peel_bool();
257+
if (exists) {
258+
let funding_rate_interval = cursor.peel_u64();
259+
feed.set_funding_rate_interval(
260+
option::some(option::some(funding_rate_interval)),
261+
);
262+
} else {
263+
feed.set_funding_rate_interval(option::some(option::none()));
264+
}
265+
} else {
266+
// Unknown property - we cannot safely skip it without knowing its length
267+
abort EInvalidProperty
268+
};
269+
270+
properties_i = properties_i + 1;
271+
};
272+
273+
feed
274+
}
Lines changed: 16 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
module pyth_lazer::pyth_lazer;
22

3-
use pyth_lazer::channel;
4-
use pyth_lazer::feed::{Self, Feed};
53
use pyth_lazer::state::{Self, State};
6-
use pyth_lazer::i64::{Self};
7-
use pyth_lazer::i16::{Self};
84
use pyth_lazer::update::{Self, Update};
95
use sui::bcs;
106
use sui::clock::Clock;
@@ -15,12 +11,10 @@ const UPDATE_MESSAGE_MAGIC: u32 = 1296547300;
1511
const PAYLOAD_MAGIC: u32 = 2479346549;
1612

1713
// Error codes
18-
const EInvalidUpdate: u64 = 1;
1914
const ESignerNotTrusted: u64 = 2;
2015
const ESignerExpired: u64 = 3;
21-
22-
// TODO:
23-
// error handling
16+
const EInvalidMagic: u64 = 4;
17+
const EInvalidPayloadLength: u64 = 6;
2418

2519
/// The `PYTH_LAZER` resource serves as the one-time witness.
2620
/// It has the `drop` ability, allowing it to be consumed immediately after use.
@@ -83,161 +77,39 @@ public(package) fun verify_le_ecdsa_message(
8377
/// * `update` - The LeEcdsa formatted Lazer update
8478
///
8579
/// # Errors
86-
/// * `EInvalidUpdate` - Failed to parse the update according to the protocol definition
80+
/// * `EInvalidMagic` - Invalid magic number in update or payload
81+
/// * `EInvalidPayloadLength` - Payload length doesn't match actual data
8782
/// * `ESignerNotTrusted` - The recovered public key is not in the trusted signers list
83+
/// * `ESignerExpired` - The signer's certificate has expired
8884
public fun parse_and_verify_le_ecdsa_update(s: &State, clock: &Clock, update: vector<u8>): Update {
8985
let mut cursor = bcs::new(update);
9086

91-
// TODO: introduce helper functions to check data len before peeling. allows us to return more
92-
// granular error messages.
93-
87+
// Parse and validate message magic
9488
let magic = cursor.peel_u32();
95-
assert!(magic == UPDATE_MESSAGE_MAGIC, 0);
89+
assert!(magic == UPDATE_MESSAGE_MAGIC, EInvalidMagic);
9690

91+
// Parse signature
9792
let mut signature = vector::empty<u8>();
98-
9993
let mut sig_i = 0;
10094
while (sig_i < SECP256K1_SIG_LEN) {
10195
signature.push_back(cursor.peel_u8());
10296
sig_i = sig_i + 1;
10397
};
10498

99+
// Parse expected payload length and get remaining bytes as payload
105100
let payload_len = cursor.peel_u16();
106-
107101
let payload = cursor.into_remainder_bytes();
108102

109-
assert!((payload_len as u64) == payload.length(), 0);
110-
111-
let mut cursor = bcs::new(payload);
112-
let payload_magic = cursor.peel_u32();
113-
assert!(payload_magic == PAYLOAD_MAGIC, 0);
103+
// Validate expectedpayload length
104+
assert!((payload_len as u64) == payload.length(), EInvalidPayloadLength);
114105

115-
let timestamp = cursor.peel_u64();
106+
// Parse payload
107+
let mut payload_cursor = bcs::new(payload);
108+
let payload_magic = payload_cursor.peel_u32();
109+
assert!(payload_magic == PAYLOAD_MAGIC, EInvalidMagic);
116110

117111
// Verify the signature against trusted signers
118112
verify_le_ecdsa_message(s, clock, &signature, &payload);
119113

120-
let channel_value = cursor.peel_u8();
121-
let channel = if (channel_value == 0) {
122-
channel::new_invalid()
123-
} else if (channel_value == 1) {
124-
channel::new_real_time()
125-
} else if (channel_value == 2) {
126-
channel::new_fixed_rate_50ms()
127-
} else if (channel_value == 3) {
128-
channel::new_fixed_rate_200ms()
129-
} else {
130-
channel::new_invalid() // Default to Invalid for unknown values
131-
};
132-
133-
let mut feeds = vector::empty<Feed>();
134-
let mut feed_i = 0;
135-
136-
let feed_count = cursor.peel_u8();
137-
138-
while (feed_i < feed_count) {
139-
let feed_id = cursor.peel_u32();
140-
let mut feed = feed::new(
141-
feed_id,
142-
option::none(),
143-
option::none(),
144-
option::none(),
145-
option::none(),
146-
option::none(),
147-
option::none(),
148-
option::none(),
149-
option::none(),
150-
option::none(),
151-
);
152-
153-
let properties_count = cursor.peel_u8();
154-
let mut properties_i = 0;
155-
156-
while (properties_i < properties_count) {
157-
let property_id = cursor.peel_u8();
158-
159-
if (property_id == 0) {
160-
let price = cursor.peel_u64();
161-
if (price != 0) {
162-
feed.set_price(option::some(option::some(i64::from_u64(price))));
163-
} else {
164-
feed.set_price(option::some(option::none()));
165-
}
166-
} else if (property_id == 1) {
167-
let best_bid_price = cursor.peel_u64();
168-
if (best_bid_price != 0) {
169-
feed.set_best_bid_price(
170-
option::some(option::some(i64::from_u64(best_bid_price))),
171-
);
172-
} else {
173-
feed.set_best_bid_price(option::some(option::none()));
174-
}
175-
} else if (property_id == 2) {
176-
let best_ask_price = cursor.peel_u64();
177-
if (best_ask_price != 0) {
178-
feed.set_best_ask_price(
179-
option::some(option::some(i64::from_u64(best_ask_price))),
180-
);
181-
} else {
182-
feed.set_best_ask_price(option::some(option::none()));
183-
}
184-
} else if (property_id == 3) {
185-
let publisher_count = cursor.peel_u16();
186-
feed.set_publisher_count(option::some(publisher_count));
187-
} else if (property_id == 4) {
188-
let exponent = cursor.peel_u16();
189-
feed.set_exponent(option::some(i16::from_u16(exponent)));
190-
} else if (property_id == 5) {
191-
let confidence = cursor.peel_u64();
192-
if (confidence != 0) {
193-
feed.set_confidence(option::some(option::some(i64::from_u64(confidence))));
194-
} else {
195-
feed.set_confidence(option::some(option::none()));
196-
}
197-
} else if (property_id == 6) {
198-
let exists = cursor.peel_u8();
199-
if (exists == 1) {
200-
let funding_rate = cursor.peel_u64();
201-
feed.set_funding_rate(option::some(option::some(i64::from_u64(funding_rate))));
202-
} else {
203-
feed.set_funding_rate(option::some(option::none()));
204-
}
205-
} else if (property_id == 7) {
206-
let exists = cursor.peel_u8();
207-
208-
if (exists == 1) {
209-
let funding_timestamp = cursor.peel_u64();
210-
feed.set_funding_timestamp(option::some(option::some(funding_timestamp)));
211-
} else {
212-
feed.set_funding_timestamp(option::some(option::none()));
213-
}
214-
} else if (property_id == 8) {
215-
let exists = cursor.peel_u8();
216-
217-
if (exists == 1) {
218-
let funding_rate_interval = cursor.peel_u64();
219-
feed.set_funding_rate_interval(
220-
option::some(option::some(funding_rate_interval)),
221-
);
222-
} else {
223-
feed.set_funding_rate_interval(option::some(option::none()));
224-
}
225-
} else {
226-
// When we have an unknown property, we do not know its length, and therefore
227-
// we cannot ignore it and parse the next properties.
228-
abort EInvalidUpdate // FIXME: return more granular error messages
229-
};
230-
231-
properties_i = properties_i + 1;
232-
};
233-
234-
vector::push_back(&mut feeds, feed);
235-
236-
feed_i = feed_i + 1;
237-
};
238-
239-
let remaining_bytes = cursor.into_remainder_bytes();
240-
assert!(remaining_bytes.length() == 0, 0);
241-
242-
update::new(timestamp, channel, feeds)
114+
update::parse_from_cursor(payload_cursor)
243115
}

0 commit comments

Comments
 (0)