Skip to content

Commit adfc2c4

Browse files
ro-texandrejrakic
andauthored
DS-899 Implement V12 support for Rust (#52)
Co-authored-by: andrejrakic <[email protected]>
1 parent 37c1126 commit adfc2c4

File tree

3 files changed

+269
-9
lines changed

3 files changed

+269
-9
lines changed

rust/crates/report/src/report.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod compress;
33
pub mod v1;
44
pub mod v10;
55
pub mod v11;
6+
pub mod v12;
67
pub mod v13;
78
pub mod v2;
89
pub mod v3;
@@ -126,9 +127,9 @@ pub fn decode_full_report(payload: &[u8]) -> Result<(Vec<[u8; 32]>, Vec<u8>), Re
126127
mod tests {
127128
use super::*;
128129
use crate::report::{
129-
v1::ReportDataV1, v10::ReportDataV10, v11::ReportDataV11, v13::ReportDataV13,
130-
v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v5::ReportDataV5, v6::ReportDataV6,
131-
v7::ReportDataV7, v8::ReportDataV8, v9::ReportDataV9,
130+
v1::ReportDataV1, v10::ReportDataV10, v11::ReportDataV11, v11::ReportDataV12,
131+
v13::ReportDataV13, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v5::ReportDataV5,
132+
v6::ReportDataV6, v7::ReportDataV7, v8::ReportDataV8, v9::ReportDataV9,
132133
};
133134
use num_bigint::BigInt;
134135

@@ -176,6 +177,10 @@ mod tests {
176177
00, 11, 251, 109, 19, 88, 151, 228, 170, 245, 101, 123, 255, 211, 176, 180, 143, 142, 42,
177178
81, 49, 33, 76, 158, 194, 214, 46, 172, 93, 83, 32, 103,
178179
]);
180+
const V12_FEED_ID: ID = ID([
181+
00, 12, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58,
182+
163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114,
183+
]);
179184
const V13_FEED_ID: ID = ID([
180185
00, 13, 19, 169, 185, 197, 227, 122, 9, 159, 55, 78, 146, 195, 121, 20, 175, 92, 38, 143,
181186
58, 138, 151, 33, 241, 114, 81, 53, 191, 180, 203, 184,
@@ -396,6 +401,27 @@ mod tests {
396401
report_data
397402
}
398403

404+
pub fn generate_mock_report_data_v12() -> ReportDataV12 {
405+
const MOCK_NAV_PER_SHARE: isize = 1;
406+
const MOCK_NEXT_NAV_PER_SHARE: isize = 2;
407+
const RIPCORD_NORMAL: u32 = 0;
408+
409+
let report_data = ReportDataV12 {
410+
feed_id: V12_FEED_ID,
411+
valid_from_timestamp: MOCK_TIMESTAMP,
412+
observations_timestamp: MOCK_TIMESTAMP,
413+
native_fee: BigInt::from(MOCK_FEE),
414+
link_fee: BigInt::from(MOCK_FEE),
415+
expires_at: MOCK_TIMESTAMP + 100,
416+
nav_per_share: BigInt::from(MOCK_NAV_PER_SHARE),
417+
next_nav_per_share: BigInt::from(MOCK_NEXT_NAV_PER_SHARE),
418+
nav_date: MOCK_TIMESTAMP as i64,
419+
ripcord: RIPCORD_NORMAL,
420+
};
421+
422+
report_data
423+
}
424+
399425
pub fn generate_mock_report_data_v13() -> ReportDataV13 {
400426
let multiplier: BigInt = "1000000000000000000".parse::<BigInt>().unwrap(); // 1.0 with 18 decimals
401427

@@ -808,6 +834,38 @@ mod tests {
808834
assert_eq!(decoded_report.feed_id, V11_FEED_ID);
809835
}
810836

837+
#[test]
838+
fn test_decode_report_v12() {
839+
let report_data = generate_mock_report_data_v12();
840+
let encoded_report_data = report_data.abi_encode().unwrap();
841+
842+
let report = generate_mock_report(&encoded_report_data);
843+
844+
let (_report_context, report_blob) = decode_full_report(&report).unwrap();
845+
846+
let expected_report_blob = vec![
847+
"000c6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
848+
"0000000000000000000000000000000000000000000000000000000066741d8c",
849+
"0000000000000000000000000000000000000000000000000000000066741d8c",
850+
"000000000000000000000000000000000000000000000000000000000000000a",
851+
"000000000000000000000000000000000000000000000000000000000000000a",
852+
"0000000000000000000000000000000000000000000000000000000066741df0",
853+
"0000000000000000000000000000000000000000000000000000000000000001", // NAV per share
854+
"0000000000000000000000000000000000000000000000000000000000000002", // Next NAV per share
855+
"0000000000000000000000000000000000000000000000000000000066741d8c", // NAV date
856+
"0000000000000000000000000000000000000000000000000000000000000000", // Ripcord: Normal
857+
];
858+
859+
assert_eq!(
860+
report_blob,
861+
bytes(&format!("0x{}", expected_report_blob.join("")))
862+
);
863+
864+
let decoded_report = ReportDataV12::decode(&report_blob).unwrap();
865+
866+
assert_eq!(decoded_report.feed_id, V12_FEED_ID);
867+
}
868+
811869
#[test]
812870
fn test_decode_report_v13() {
813871
let report_data = generate_mock_report_data_v13();

rust/crates/report/src/report/base.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ impl ReportBase {
6464
if offset + Self::WORD_SIZE > data.len() {
6565
return Err(ReportError::DataTooShort("uint32"));
6666
}
67-
let value_bytes = &data[offset + 28..offset + 32];
67+
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
6868
Ok(u32::from_be_bytes(
69-
value_bytes
69+
value_bytes[28..32]
7070
.try_into()
7171
.map_err(|_| ReportError::InvalidLength("uint32"))?,
7272
))
@@ -75,17 +75,23 @@ impl ReportBase {
7575
pub(crate) fn encode_uint32(value: u32) -> Result<[u8; 32], ReportError> {
7676
let mut buffer = [0u8; 32];
7777
let bytes_value = value.to_be_bytes();
78-
buffer[28..32].copy_from_slice(&bytes_value); // Place at the end of the 32 bytes word
78+
let len = bytes_value.len();
79+
80+
if len > 4 {
81+
return Err(ReportError::InvalidLength("uint32"));
82+
}
83+
84+
buffer[32 - len..32].copy_from_slice(&bytes_value);
7985
Ok(buffer)
8086
}
8187

8288
pub(crate) fn read_uint64(data: &[u8], offset: usize) -> Result<u64, ReportError> {
8389
if offset + Self::WORD_SIZE > data.len() {
8490
return Err(ReportError::DataTooShort("uint64"));
8591
}
86-
let value_bytes = &data[offset + 24..offset + 32];
92+
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
8793
Ok(u64::from_be_bytes(
88-
value_bytes
94+
value_bytes[24..32]
8995
.try_into()
9096
.map_err(|_| ReportError::InvalidLength("uint64"))?,
9197
))
@@ -94,7 +100,38 @@ impl ReportBase {
94100
pub(crate) fn encode_uint64(value: u64) -> Result<[u8; 32], ReportError> {
95101
let mut buffer = [0u8; 32];
96102
let bytes_value = value.to_be_bytes();
97-
buffer[24..32].copy_from_slice(&bytes_value); // Place at the end of the 32 bytes word
103+
let len = bytes_value.len();
104+
105+
if len > 8 {
106+
return Err(ReportError::InvalidLength("uint64"));
107+
}
108+
109+
buffer[32 - len..32].copy_from_slice(&bytes_value);
110+
Ok(buffer)
111+
}
112+
113+
pub(crate) fn read_int64(data: &[u8], offset: usize) -> Result<i64, ReportError> {
114+
if offset + Self::WORD_SIZE > data.len() {
115+
return Err(ReportError::DataTooShort("int64"));
116+
}
117+
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
118+
Ok(i64::from_be_bytes(
119+
value_bytes[24..32]
120+
.try_into()
121+
.map_err(|_| ReportError::InvalidLength("int64"))?,
122+
))
123+
}
124+
125+
pub(crate) fn encode_int64(value: i64) -> Result<[u8; 32], ReportError> {
126+
let mut buffer = [0u8; 32];
127+
let bytes_value = value.to_be_bytes();
128+
let len = bytes_value.len();
129+
130+
if len > 8 {
131+
return Err(ReportError::InvalidLength("int64"));
132+
}
133+
134+
buffer[32 - len..32].copy_from_slice(&bytes_value);
98135
Ok(buffer)
99136
}
100137
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use crate::feed_id::ID;
2+
use crate::report::base::{ReportBase, ReportError};
3+
use num_bigint::BigInt;
4+
5+
/// Represents a Report Data V12 Schema.
6+
///
7+
/// # Parameters
8+
/// - `feed_id`: Unique identifier for the Data Streams feed.
9+
/// - `valid_from_timestamp`: Earliest timestamp when the price is valid (seconds).
10+
/// - `observations_timestamp`: Latest timestamp when the price is valid (seconds).
11+
/// - `native_fee`: Verification cost in native blockchain tokens.
12+
/// - `link_fee`: Verification cost in LINK tokens.
13+
/// - `expires_at`: Expiration date of the report (seconds).
14+
/// - `nav_per_share`: DON consensus NAV Per Share value as reported by the Fund Manager.
15+
/// - `next_nav_per_share`: DON consensus next NAV Per Share value as reported by the Fund Manager.
16+
/// - `nav_date`: Timestamp for the publication date of the NAV Report (nanoseconds).
17+
/// - `ripcord`: Whether the provider paused NAV reporting.
18+
///
19+
/// # Ripcord Flag
20+
/// - `0` (false): Feed's data provider is OK. Fund's data provider and accuracy is as expected.
21+
/// - `1` (true): Feed's data provider is flagging a pause. Data provider detected outliers,
22+
/// deviated thresholds, or operational issues. **DO NOT consume NAV data when ripcord=1.**
23+
///
24+
/// # Solidity Equivalent
25+
/// ```solidity
26+
/// struct ReportDataV12 {
27+
/// bytes32 feedId;
28+
/// uint32 validFromTimestamp;
29+
/// uint32 observationsTimestamp;
30+
/// uint192 nativeFee;
31+
/// uint192 linkFee;
32+
/// uint32 expiresAt;
33+
/// int192 navPerShare;
34+
/// int192 nextNavPerShare;
35+
/// int64 navDate;
36+
/// uint32 ripcord;
37+
/// }
38+
/// ```
39+
#[derive(Debug)]
40+
pub struct ReportDataV12 {
41+
pub feed_id: ID,
42+
pub valid_from_timestamp: u32,
43+
pub observations_timestamp: u32,
44+
pub native_fee: BigInt,
45+
pub link_fee: BigInt,
46+
pub expires_at: u32,
47+
pub nav_per_share: BigInt,
48+
pub next_nav_per_share: BigInt,
49+
pub nav_date: i64,
50+
pub ripcord: u32,
51+
}
52+
53+
impl ReportDataV12 {
54+
/// Decodes an ABI-encoded `ReportDataV12` from bytes.
55+
///
56+
/// # Parameters
57+
///
58+
/// - `data`: The encoded report data.
59+
///
60+
/// # Returns
61+
///
62+
/// The decoded `ReportDataV12`.
63+
///
64+
/// # Errors
65+
///
66+
/// Returns a `ReportError` if the data is too short or if the data is invalid.
67+
pub fn decode(data: &[u8]) -> Result<Self, ReportError> {
68+
if data.len() < 10 * ReportBase::WORD_SIZE {
69+
return Err(ReportError::DataTooShort("ReportDataV12"));
70+
}
71+
72+
let feed_id = ID(data[..ReportBase::WORD_SIZE]
73+
.try_into()
74+
.map_err(|_| ReportError::InvalidLength("feed_id (bytes32)"))?);
75+
76+
let valid_from_timestamp = ReportBase::read_uint32(data, ReportBase::WORD_SIZE)?;
77+
let observations_timestamp = ReportBase::read_uint32(data, 2 * ReportBase::WORD_SIZE)?;
78+
let native_fee = ReportBase::read_uint192(data, 3 * ReportBase::WORD_SIZE)?;
79+
let link_fee = ReportBase::read_uint192(data, 4 * ReportBase::WORD_SIZE)?;
80+
let expires_at = ReportBase::read_uint32(data, 5 * ReportBase::WORD_SIZE)?;
81+
let nav_per_share = ReportBase::read_int192(data, 6 * ReportBase::WORD_SIZE)?;
82+
let next_nav_per_share = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?;
83+
let nav_date = ReportBase::read_int64(data, 8 * ReportBase::WORD_SIZE)?;
84+
let ripcord = ReportBase::read_uint32(data, 9 * ReportBase::WORD_SIZE)?;
85+
86+
Ok(Self {
87+
feed_id,
88+
valid_from_timestamp,
89+
observations_timestamp,
90+
native_fee,
91+
link_fee,
92+
expires_at,
93+
nav_per_share,
94+
next_nav_per_share,
95+
nav_date,
96+
ripcord,
97+
})
98+
}
99+
100+
/// Encodes the `ReportDataV12` into an ABI-encoded byte array.
101+
///
102+
/// # Returns
103+
///
104+
/// The ABI-encoded report data.
105+
///
106+
/// # Errors
107+
///
108+
/// Returns a `ReportError` if the data is invalid.
109+
pub fn abi_encode(&self) -> Result<Vec<u8>, ReportError> {
110+
let mut buffer = Vec::with_capacity(10 * ReportBase::WORD_SIZE);
111+
112+
buffer.extend_from_slice(&self.feed_id.0);
113+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.valid_from_timestamp)?);
114+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.observations_timestamp)?);
115+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.native_fee)?);
116+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.link_fee)?);
117+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.expires_at)?);
118+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.nav_per_share)?);
119+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.next_nav_per_share)?);
120+
buffer.extend_from_slice(&ReportBase::encode_int64(self.nav_date)?);
121+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.ripcord)?);
122+
123+
Ok(buffer)
124+
}
125+
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use super::*;
130+
use crate::report::tests::{
131+
generate_mock_report_data_v12, MOCK_FEE, MOCK_TIMESTAMP,
132+
};
133+
134+
const V12_FEED_ID_STR: &str =
135+
"0x000c6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472";
136+
137+
const MOCK_NAV_PER_SHARE: isize = 1;
138+
const MOCK_NEXT_NAV_PER_SHARE: isize = 2;
139+
const RIPCORD_NORMAL: u32 = 0;
140+
141+
#[test]
142+
fn test_decode_report_data_v12() {
143+
let report_data = generate_mock_report_data_v12();
144+
let encoded = report_data.abi_encode().unwrap();
145+
let decoded = ReportDataV12::decode(&encoded).unwrap();
146+
147+
let expected_feed_id = ID::from_hex_str(V12_FEED_ID_STR).unwrap();
148+
let expected_timestamp: u32 = MOCK_TIMESTAMP;
149+
let expected_fee = BigInt::from(MOCK_FEE);
150+
let expected_nav_per_share = BigInt::from(MOCK_NAV_PER_SHARE);
151+
let expected_next_nav_per_share = BigInt::from(MOCK_NEXT_NAV_PER_SHARE);
152+
let expected_ripcord = RIPCORD_NORMAL;
153+
154+
assert_eq!(decoded.feed_id, expected_feed_id);
155+
assert_eq!(decoded.valid_from_timestamp, expected_timestamp);
156+
assert_eq!(decoded.observations_timestamp, expected_timestamp);
157+
assert_eq!(decoded.native_fee, expected_fee);
158+
assert_eq!(decoded.link_fee, expected_fee);
159+
assert_eq!(decoded.expires_at, expected_timestamp + 100);
160+
assert_eq!(decoded.nav_per_share, expected_nav_per_share);
161+
assert_eq!(decoded.next_nav_per_share, expected_next_nav_per_share);
162+
assert_eq!(decoded.nav_date, expected_timestamp as i64);
163+
assert_eq!(decoded.ripcord, expected_ripcord);
164+
}
165+
}

0 commit comments

Comments
 (0)