Skip to content

Commit 7d593d8

Browse files
authored
Add deserialization to price messages (#372)
* Add deserialization to price messages * Update comments
1 parent 410f0bd commit 7d593d8

File tree

6 files changed

+180
-90
lines changed

6 files changed

+180
-90
lines changed

program/rust/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ bytemuck = "1.11.0"
1414
thiserror = "1.0"
1515
num-derive = "0.3"
1616
num-traits = "0.2"
17+
byteorder = "1.4.3"
1718

1819
[dev-dependencies]
19-
byteorder = "1.4.3"
2020
solana-program-test = "=1.13.3"
2121
solana-sdk = "=1.13.3"
2222
tokio = "1.14.1"

program/rust/src/accounts.rs

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub use {
4949
mapping::MappingAccount,
5050
permission::PermissionAccount,
5151
price::{
52+
Message,
5253
PriceAccount,
5354
PriceAccountV2,
5455
PriceComponent,

program/rust/src/accounts/price.rs

+153-3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,18 @@ use {
1717
Pod,
1818
Zeroable,
1919
},
20+
byteorder::{
21+
BigEndian,
22+
ReadBytesExt,
23+
},
2024
solana_program::pubkey::Pubkey,
21-
std::mem::size_of,
25+
std::{
26+
io::{
27+
Cursor,
28+
Read,
29+
},
30+
mem::size_of,
31+
},
2232
};
2333

2434
#[repr(C)]
@@ -213,6 +223,39 @@ impl PythAccount for PriceAccountV2 {
213223
/// Messages are forward-compatible. You may add new fields to messages after all previously
214224
/// defined fields. All code for parsing messages must ignore any extraneous bytes at the end of
215225
/// the message (which could be fields that the code does not yet understand).
226+
///
227+
/// The oracle is not using the Message enum due to the contract size limit and
228+
/// some of the methods for PriceFeedMessage and TwapMessage are not used by the oracle
229+
/// for the same reason. Rust compiler doesn't include the unused methods in the contract.
230+
/// Once we start using the unused structs and methods, the contract size will increase.
231+
232+
#[derive(Debug, Copy, Clone, PartialEq)]
233+
pub enum Message {
234+
PriceFeedMessage(PriceFeedMessage),
235+
TwapMessage(TwapMessage),
236+
}
237+
238+
#[allow(dead_code)]
239+
impl Message {
240+
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, OracleError> {
241+
match bytes[0] {
242+
PriceFeedMessage::DISCRIMINATOR => Ok(Self::PriceFeedMessage(
243+
PriceFeedMessage::try_from_bytes(bytes)?,
244+
)),
245+
TwapMessage::DISCRIMINATOR => {
246+
Ok(Self::TwapMessage(TwapMessage::try_from_bytes(bytes)?))
247+
}
248+
_ => Err(OracleError::DeserializationError),
249+
}
250+
}
251+
pub fn to_bytes(self) -> Vec<u8> {
252+
match self {
253+
Self::PriceFeedMessage(msg) => msg.to_bytes().to_vec(),
254+
Self::TwapMessage(msg) => msg.to_bytes().to_vec(),
255+
}
256+
}
257+
}
258+
216259
#[repr(C)]
217260
#[derive(Debug, Copy, Clone, PartialEq)]
218261
pub struct PriceFeedMessage {
@@ -273,7 +316,7 @@ impl PriceFeedMessage {
273316
/// Note that it would be more idiomatic to return a `Vec`, but that approach adds
274317
/// to the size of the compiled binary (which is already close to the size limit).
275318
#[allow(unused_assignments)]
276-
pub fn as_bytes(&self) -> [u8; Self::MESSAGE_SIZE] {
319+
pub fn to_bytes(self) -> [u8; Self::MESSAGE_SIZE] {
277320
let mut bytes = [0u8; Self::MESSAGE_SIZE];
278321

279322
let mut i: usize = 0;
@@ -307,6 +350,60 @@ impl PriceFeedMessage {
307350

308351
bytes
309352
}
353+
354+
/// Try to deserialize a message from an array of bytes (including the discriminator).
355+
/// This method is forward-compatible and allows the size to be larger than the
356+
/// size of the struct. As a side-effect, it will ignore newer fields that are
357+
/// not yet present in the struct.
358+
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, OracleError> {
359+
let mut cursor = Cursor::new(bytes);
360+
361+
let discriminator = cursor
362+
.read_u8()
363+
.map_err(|_| OracleError::DeserializationError)?;
364+
if discriminator != 0 {
365+
return Err(OracleError::DeserializationError);
366+
}
367+
368+
let mut id = [0u8; 32];
369+
cursor
370+
.read_exact(&mut id)
371+
.map_err(|_| OracleError::DeserializationError)?;
372+
373+
let price = cursor
374+
.read_i64::<BigEndian>()
375+
.map_err(|_| OracleError::DeserializationError)?;
376+
let conf = cursor
377+
.read_u64::<BigEndian>()
378+
.map_err(|_| OracleError::DeserializationError)?;
379+
let exponent = cursor
380+
.read_i32::<BigEndian>()
381+
.map_err(|_| OracleError::DeserializationError)?;
382+
let publish_time = cursor
383+
.read_i64::<BigEndian>()
384+
.map_err(|_| OracleError::DeserializationError)?;
385+
let prev_publish_time = cursor
386+
.read_i64::<BigEndian>()
387+
.map_err(|_| OracleError::DeserializationError)?;
388+
let ema_price = cursor
389+
.read_i64::<BigEndian>()
390+
.map_err(|_| OracleError::DeserializationError)?;
391+
let ema_conf = cursor
392+
.read_u64::<BigEndian>()
393+
.map_err(|_| OracleError::DeserializationError)?;
394+
395+
396+
Ok(Self {
397+
id,
398+
price,
399+
conf,
400+
exponent,
401+
publish_time,
402+
prev_publish_time,
403+
ema_price,
404+
ema_conf,
405+
})
406+
}
310407
}
311408

312409
/// Message format for sending Twap data via the accumulator program
@@ -353,7 +450,7 @@ impl TwapMessage {
353450
/// Note that it would be more idiomatic to return a `Vec`, but that approach adds
354451
/// to the size of the compiled binary (which is already close to the size limit).
355452
#[allow(unused_assignments)]
356-
pub fn as_bytes(&self) -> [u8; Self::MESSAGE_SIZE] {
453+
pub fn to_bytes(self) -> [u8; Self::MESSAGE_SIZE] {
357454
let mut bytes = [0u8; Self::MESSAGE_SIZE];
358455

359456
let mut i: usize = 0;
@@ -387,4 +484,57 @@ impl TwapMessage {
387484

388485
bytes
389486
}
487+
488+
/// Try to deserialize a message from an array of bytes (including the discriminator).
489+
/// This method is forward-compatible and allows the size to be larger than the
490+
/// size of the struct. As a side-effect, it will ignore newer fields that are
491+
/// not yet present in the struct.
492+
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, OracleError> {
493+
let mut cursor = Cursor::new(bytes);
494+
495+
let discriminator = cursor
496+
.read_u8()
497+
.map_err(|_| OracleError::DeserializationError)?;
498+
if discriminator != 1 {
499+
return Err(OracleError::DeserializationError);
500+
}
501+
502+
let mut id = [0u8; 32];
503+
cursor
504+
.read_exact(&mut id)
505+
.map_err(|_| OracleError::DeserializationError)?;
506+
507+
let cumulative_price = cursor
508+
.read_i128::<BigEndian>()
509+
.map_err(|_| OracleError::DeserializationError)?;
510+
let cumulative_conf = cursor
511+
.read_u128::<BigEndian>()
512+
.map_err(|_| OracleError::DeserializationError)?;
513+
let num_down_slots = cursor
514+
.read_u64::<BigEndian>()
515+
.map_err(|_| OracleError::DeserializationError)?;
516+
let exponent = cursor
517+
.read_i32::<BigEndian>()
518+
.map_err(|_| OracleError::DeserializationError)?;
519+
let publish_time = cursor
520+
.read_i64::<BigEndian>()
521+
.map_err(|_| OracleError::DeserializationError)?;
522+
let prev_publish_time = cursor
523+
.read_i64::<BigEndian>()
524+
.map_err(|_| OracleError::DeserializationError)?;
525+
let publish_slot = cursor
526+
.read_u64::<BigEndian>()
527+
.map_err(|_| OracleError::DeserializationError)?;
528+
529+
Ok(Self {
530+
id,
531+
cumulative_price,
532+
cumulative_conf,
533+
num_down_slots,
534+
exponent,
535+
publish_time,
536+
prev_publish_time,
537+
publish_slot,
538+
})
539+
}
390540
}

program/rust/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ mod log;
2727
pub use accounts::{
2828
AccountHeader,
2929
MappingAccount,
30+
Message,
3031
PermissionAccount,
3132
PriceAccount,
3233
PriceComponent,
3334
PriceEma,
35+
PriceFeedMessage,
3436
PriceInfo,
3537
ProductAccount,
3638
PythAccount,
39+
TwapMessage,
3740
};
3841
use {
3942
crate::error::OracleError,

program/rust/src/processor/upd_price.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ pub fn upd_price(
228228
let message =
229229
vec![
230230
PriceFeedMessage::from_price_account(price_account.key, &price_data)
231-
.as_bytes()
231+
.to_bytes()
232232
.to_vec(),
233233
];
234234

+21-85
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
use {
22
crate::accounts::{
3+
Message,
34
PriceFeedMessage,
45
TwapMessage,
56
},
6-
byteorder::{
7-
BigEndian,
8-
ReadBytesExt,
9-
},
107
quickcheck::Arbitrary,
118
quickcheck_macros::quickcheck,
12-
std::io::{
13-
Cursor,
14-
Read,
15-
},
169
};
1710

1811
impl Arbitrary for PriceFeedMessage {
@@ -37,55 +30,15 @@ impl Arbitrary for PriceFeedMessage {
3730
}
3831
}
3932

40-
/// TODO: move this parsing implementation to a separate data format library.
41-
impl PriceFeedMessage {
42-
fn from_bytes(bytes: &[u8]) -> Option<Self> {
43-
let mut cursor = Cursor::new(bytes);
44-
45-
let discriminator = cursor.read_u8().ok()?;
46-
if discriminator != 0 {
47-
return None;
48-
}
49-
50-
let mut id = [0u8; 32];
51-
cursor.read_exact(&mut id).ok()?;
52-
53-
let price = cursor.read_i64::<BigEndian>().ok()?;
54-
let conf = cursor.read_u64::<BigEndian>().ok()?;
55-
let exponent = cursor.read_i32::<BigEndian>().ok()?;
56-
let publish_time = cursor.read_i64::<BigEndian>().ok()?;
57-
let prev_publish_time = cursor.read_i64::<BigEndian>().ok()?;
58-
let ema_price = cursor.read_i64::<BigEndian>().ok()?;
59-
let ema_conf = cursor.read_u64::<BigEndian>().ok()?;
60-
61-
62-
if cursor.position() as usize != bytes.len() {
63-
// The message bytes are longer than expected
64-
None
65-
} else {
66-
Some(Self {
67-
id,
68-
price,
69-
conf,
70-
exponent,
71-
publish_time,
72-
prev_publish_time,
73-
ema_price,
74-
ema_conf,
75-
})
76-
}
77-
}
78-
}
79-
8033
#[quickcheck]
8134
fn test_price_feed_message_roundtrip(input: PriceFeedMessage) -> bool {
82-
let reconstructed = PriceFeedMessage::from_bytes(&input.as_bytes());
35+
let reconstructed = PriceFeedMessage::try_from_bytes(&input.to_bytes());
8336

8437
println!("Failed test case:");
8538
println!("{:?}", input);
8639
println!("{:?}", reconstructed);
8740

88-
reconstructed.is_some() && reconstructed.unwrap() == input
41+
reconstructed == Ok(input)
8942
}
9043

9144

@@ -111,51 +64,34 @@ impl Arbitrary for TwapMessage {
11164
}
11265
}
11366

114-
impl TwapMessage {
115-
fn from_bytes(bytes: &[u8]) -> Option<Self> {
116-
let mut cursor = Cursor::new(bytes);
67+
#[quickcheck]
68+
fn test_twap_message_roundtrip(input: TwapMessage) -> bool {
69+
let reconstructed = TwapMessage::try_from_bytes(&input.to_bytes());
70+
71+
println!("Failed test case:");
72+
println!("{:?}", input);
73+
println!("{:?}", reconstructed);
11774

118-
let discriminator = cursor.read_u8().ok()?;
119-
if discriminator != 1 {
120-
return None;
121-
}
75+
reconstructed == Ok(input)
76+
}
12277

123-
let mut id = [0u8; 32];
124-
cursor.read_exact(&mut id).ok()?;
125-
126-
let cumulative_price = cursor.read_i128::<BigEndian>().ok()?;
127-
let cumulative_conf = cursor.read_u128::<BigEndian>().ok()?;
128-
let num_down_slots = cursor.read_u64::<BigEndian>().ok()?;
129-
let exponent = cursor.read_i32::<BigEndian>().ok()?;
130-
let publish_time = cursor.read_i64::<BigEndian>().ok()?;
131-
let prev_publish_time = cursor.read_i64::<BigEndian>().ok()?;
132-
let publish_slot = cursor.read_u64::<BigEndian>().ok()?;
133-
134-
if cursor.position() as usize != bytes.len() {
135-
// The message bytes are longer than expected
136-
None
137-
} else {
138-
Some(Self {
139-
id,
140-
cumulative_price,
141-
cumulative_conf,
142-
num_down_slots,
143-
exponent,
144-
publish_time,
145-
prev_publish_time,
146-
publish_slot,
147-
})
78+
79+
impl Arbitrary for Message {
80+
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
81+
match u8::arbitrary(g) % 2 {
82+
0 => Message::PriceFeedMessage(Arbitrary::arbitrary(g)),
83+
_ => Message::TwapMessage(Arbitrary::arbitrary(g)),
14884
}
14985
}
15086
}
15187

15288
#[quickcheck]
153-
fn test_twap_message_roundtrip(input: TwapMessage) -> bool {
154-
let reconstructed = TwapMessage::from_bytes(&input.as_bytes());
89+
fn test_message_roundtrip(input: Message) -> bool {
90+
let reconstructed = Message::try_from_bytes(&input.to_bytes());
15591

15692
println!("Failed test case:");
15793
println!("{:?}", input);
15894
println!("{:?}", reconstructed);
15995

160-
reconstructed.is_some() && reconstructed.unwrap() == input
96+
reconstructed == Ok(input)
16197
}

0 commit comments

Comments
 (0)