Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions rust/crates/report/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod compress;
pub mod v1;
pub mod v10;
pub mod v11;
pub mod v12;
pub mod v13;
pub mod v2;
pub mod v3;
Expand Down Expand Up @@ -126,9 +127,9 @@ pub fn decode_full_report(payload: &[u8]) -> Result<(Vec<[u8; 32]>, Vec<u8>), Re
mod tests {
use super::*;
use crate::report::{
v1::ReportDataV1, v10::ReportDataV10, v11::ReportDataV11, v13::ReportDataV13,
v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v5::ReportDataV5, v6::ReportDataV6,
v7::ReportDataV7, v8::ReportDataV8, v9::ReportDataV9,
v1::ReportDataV1, v10::ReportDataV10, v11::ReportDataV11, v11::ReportDataV12,
v13::ReportDataV13, v2::ReportDataV2, v3::ReportDataV3, v4::ReportDataV4, v5::ReportDataV5,
v6::ReportDataV6, v7::ReportDataV7, v8::ReportDataV8, v9::ReportDataV9,
};
use num_bigint::BigInt;

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

pub fn generate_mock_report_data_v12() -> ReportDataV12 {
const MOCK_NAV_PER_SHARE: isize = 1;
const MOCK_NEXT_NAV_PER_SHARE: isize = 2;
const RIPCORD_NORMAL: u32 = 0;

let report_data = ReportDataV12 {
feed_id: V12_FEED_ID,
valid_from_timestamp: MOCK_TIMESTAMP,
observations_timestamp: MOCK_TIMESTAMP,
native_fee: BigInt::from(MOCK_FEE),
link_fee: BigInt::from(MOCK_FEE),
expires_at: MOCK_TIMESTAMP + 100,
nav_per_share: BigInt::from(MOCK_NAV_PER_SHARE),
next_nav_per_share: BigInt::from(MOCK_NEXT_NAV_PER_SHARE),
nav_date: MOCK_TIMESTAMP as i64,
ripcord: RIPCORD_NORMAL,
};

report_data
}

pub fn generate_mock_report_data_v13() -> ReportDataV13 {
let multiplier: BigInt = "1000000000000000000".parse::<BigInt>().unwrap(); // 1.0 with 18 decimals

Expand Down Expand Up @@ -808,6 +834,38 @@ mod tests {
assert_eq!(decoded_report.feed_id, V11_FEED_ID);
}

#[test]
fn test_decode_report_v12() {
let report_data = generate_mock_report_data_v12();
let encoded_report_data = report_data.abi_encode().unwrap();

let report = generate_mock_report(&encoded_report_data);

let (_report_context, report_blob) = decode_full_report(&report).unwrap();

let expected_report_blob = vec![
"000c6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"0000000000000000000000000000000000000000000000000000000066741d8c",
"000000000000000000000000000000000000000000000000000000000000000a",
"000000000000000000000000000000000000000000000000000000000000000a",
"0000000000000000000000000000000000000000000000000000000066741df0",
"0000000000000000000000000000000000000000000000000000000000000001", // NAV per share
"0000000000000000000000000000000000000000000000000000000000000002", // Next NAV per share
"0000000000000000000000000000000000000000000000000000000066741d8c", // NAV date
"0000000000000000000000000000000000000000000000000000000000000000", // Ripcord: Normal
];

assert_eq!(
report_blob,
bytes(&format!("0x{}", expected_report_blob.join("")))
);

let decoded_report = ReportDataV12::decode(&report_blob).unwrap();

assert_eq!(decoded_report.feed_id, V12_FEED_ID);
}

#[test]
fn test_decode_report_v13() {
let report_data = generate_mock_report_data_v13();
Expand Down
49 changes: 43 additions & 6 deletions rust/crates/report/src/report/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ impl ReportBase {
if offset + Self::WORD_SIZE > data.len() {
return Err(ReportError::DataTooShort("uint32"));
}
let value_bytes = &data[offset + 28..offset + 32];
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
Ok(u32::from_be_bytes(
value_bytes
value_bytes[28..32]
.try_into()
.map_err(|_| ReportError::InvalidLength("uint32"))?,
))
Expand All @@ -75,17 +75,23 @@ impl ReportBase {
pub(crate) fn encode_uint32(value: u32) -> Result<[u8; 32], ReportError> {
let mut buffer = [0u8; 32];
let bytes_value = value.to_be_bytes();
buffer[28..32].copy_from_slice(&bytes_value); // Place at the end of the 32 bytes word
let len = bytes_value.len();

if len > 4 {
return Err(ReportError::InvalidLength("uint32"));
}

buffer[32 - len..32].copy_from_slice(&bytes_value);
Ok(buffer)
}

pub(crate) fn read_uint64(data: &[u8], offset: usize) -> Result<u64, ReportError> {
if offset + Self::WORD_SIZE > data.len() {
return Err(ReportError::DataTooShort("uint64"));
}
let value_bytes = &data[offset + 24..offset + 32];
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
Ok(u64::from_be_bytes(
value_bytes
value_bytes[24..32]
.try_into()
.map_err(|_| ReportError::InvalidLength("uint64"))?,
))
Expand All @@ -94,7 +100,38 @@ impl ReportBase {
pub(crate) fn encode_uint64(value: u64) -> Result<[u8; 32], ReportError> {
let mut buffer = [0u8; 32];
let bytes_value = value.to_be_bytes();
buffer[24..32].copy_from_slice(&bytes_value); // Place at the end of the 32 bytes word
let len = bytes_value.len();

if len > 8 {
return Err(ReportError::InvalidLength("uint64"));
}

buffer[32 - len..32].copy_from_slice(&bytes_value);
Ok(buffer)
}

pub(crate) fn read_int64(data: &[u8], offset: usize) -> Result<i64, ReportError> {
if offset + Self::WORD_SIZE > data.len() {
return Err(ReportError::DataTooShort("int64"));
}
let value_bytes = &data[offset..offset + Self::WORD_SIZE];
Ok(i64::from_be_bytes(
value_bytes[24..32]
.try_into()
.map_err(|_| ReportError::InvalidLength("int64"))?,
))
}

pub(crate) fn encode_int64(value: i64) -> Result<[u8; 32], ReportError> {
let mut buffer = [0u8; 32];
let bytes_value = value.to_be_bytes();
let len = bytes_value.len();

if len > 8 {
return Err(ReportError::InvalidLength("int64"));
}

buffer[32 - len..32].copy_from_slice(&bytes_value);
Ok(buffer)
}
}
165 changes: 165 additions & 0 deletions rust/crates/report/src/report/v12.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use crate::feed_id::ID;
use crate::report::base::{ReportBase, ReportError};
use num_bigint::BigInt;

/// Represents a Report Data V12 Schema.
///
/// # Parameters
/// - `feed_id`: Unique identifier for the Data Streams feed.
/// - `valid_from_timestamp`: Earliest timestamp when the price is valid (seconds).
/// - `observations_timestamp`: Latest timestamp when the price is valid (seconds).
/// - `native_fee`: Verification cost in native blockchain tokens.
/// - `link_fee`: Verification cost in LINK tokens.
/// - `expires_at`: Expiration date of the report (seconds).
/// - `nav_per_share`: DON consensus NAV Per Share value as reported by the Fund Manager.
/// - `next_nav_per_share`: DON consensus next NAV Per Share value as reported by the Fund Manager.
/// - `nav_date`: Timestamp for the publication date of the NAV Report (nanoseconds).
/// - `ripcord`: Whether the provider paused NAV reporting.
///
/// # Ripcord Flag
/// - `0` (false): Feed's data provider is OK. Fund's data provider and accuracy is as expected.
/// - `1` (true): Feed's data provider is flagging a pause. Data provider detected outliers,
/// deviated thresholds, or operational issues. **DO NOT consume NAV data when ripcord=1.**
///
/// # Solidity Equivalent
/// ```solidity
/// struct ReportDataV12 {
/// bytes32 feedId;
/// uint32 validFromTimestamp;
/// uint32 observationsTimestamp;
/// uint192 nativeFee;
/// uint192 linkFee;
/// uint32 expiresAt;
/// int192 navPerShare;
/// int192 nextNavPerShare;
/// int64 navDate;
/// uint32 ripcord;
/// }
/// ```
#[derive(Debug)]
pub struct ReportDataV12 {
pub feed_id: ID,
pub valid_from_timestamp: u32,
pub observations_timestamp: u32,
pub native_fee: BigInt,
pub link_fee: BigInt,
pub expires_at: u32,
pub nav_per_share: BigInt,
pub next_nav_per_share: BigInt,
pub nav_date: i64,
pub ripcord: u32,
}

impl ReportDataV12 {
/// Decodes an ABI-encoded `ReportDataV12` from bytes.
///
/// # Parameters
///
/// - `data`: The encoded report data.
///
/// # Returns
///
/// The decoded `ReportDataV12`.
///
/// # Errors
///
/// Returns a `ReportError` if the data is too short or if the data is invalid.
pub fn decode(data: &[u8]) -> Result<Self, ReportError> {
if data.len() < 10 * ReportBase::WORD_SIZE {
return Err(ReportError::DataTooShort("ReportDataV12"));
}

let feed_id = ID(data[..ReportBase::WORD_SIZE]
.try_into()
.map_err(|_| ReportError::InvalidLength("feed_id (bytes32)"))?);

let valid_from_timestamp = ReportBase::read_uint32(data, ReportBase::WORD_SIZE)?;
let observations_timestamp = ReportBase::read_uint32(data, 2 * ReportBase::WORD_SIZE)?;
let native_fee = ReportBase::read_uint192(data, 3 * ReportBase::WORD_SIZE)?;
let link_fee = ReportBase::read_uint192(data, 4 * ReportBase::WORD_SIZE)?;
let expires_at = ReportBase::read_uint32(data, 5 * ReportBase::WORD_SIZE)?;
let nav_per_share = ReportBase::read_int192(data, 6 * ReportBase::WORD_SIZE)?;
let next_nav_per_share = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?;
let nav_date = ReportBase::read_int64(data, 8 * ReportBase::WORD_SIZE)?;
let ripcord = ReportBase::read_uint32(data, 9 * ReportBase::WORD_SIZE)?;

Ok(Self {
feed_id,
valid_from_timestamp,
observations_timestamp,
native_fee,
link_fee,
expires_at,
nav_per_share,
next_nav_per_share,
nav_date,
ripcord,
})
}

/// Encodes the `ReportDataV12` into an ABI-encoded byte array.
///
/// # Returns
///
/// The ABI-encoded report data.
///
/// # Errors
///
/// Returns a `ReportError` if the data is invalid.
pub fn abi_encode(&self) -> Result<Vec<u8>, ReportError> {
let mut buffer = Vec::with_capacity(10 * ReportBase::WORD_SIZE);

buffer.extend_from_slice(&self.feed_id.0);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.valid_from_timestamp)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.observations_timestamp)?);
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.native_fee)?);
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.link_fee)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.expires_at)?);
buffer.extend_from_slice(&ReportBase::encode_int192(&self.nav_per_share)?);
buffer.extend_from_slice(&ReportBase::encode_int192(&self.next_nav_per_share)?);
buffer.extend_from_slice(&ReportBase::encode_int64(self.nav_date)?);
buffer.extend_from_slice(&ReportBase::encode_uint32(self.ripcord)?);

Ok(buffer)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::report::tests::{
generate_mock_report_data_v12, MOCK_FEE, MOCK_TIMESTAMP,
};

const V12_FEED_ID_STR: &str =
"0x000c6b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b8472";

const MOCK_NAV_PER_SHARE: isize = 1;
const MOCK_NEXT_NAV_PER_SHARE: isize = 2;
const RIPCORD_NORMAL: u32 = 0;

#[test]
fn test_decode_report_data_v12() {
let report_data = generate_mock_report_data_v12();
let encoded = report_data.abi_encode().unwrap();
let decoded = ReportDataV12::decode(&encoded).unwrap();

let expected_feed_id = ID::from_hex_str(V12_FEED_ID_STR).unwrap();
let expected_timestamp: u32 = MOCK_TIMESTAMP;
let expected_fee = BigInt::from(MOCK_FEE);
let expected_nav_per_share = BigInt::from(MOCK_NAV_PER_SHARE);
let expected_next_nav_per_share = BigInt::from(MOCK_NEXT_NAV_PER_SHARE);
let expected_ripcord = RIPCORD_NORMAL;

assert_eq!(decoded.feed_id, expected_feed_id);
assert_eq!(decoded.valid_from_timestamp, expected_timestamp);
assert_eq!(decoded.observations_timestamp, expected_timestamp);
assert_eq!(decoded.native_fee, expected_fee);
assert_eq!(decoded.link_fee, expected_fee);
assert_eq!(decoded.expires_at, expected_timestamp + 100);
assert_eq!(decoded.nav_per_share, expected_nav_per_share);
assert_eq!(decoded.next_nav_per_share, expected_next_nav_per_share);
assert_eq!(decoded.nav_date, expected_timestamp as i64);
assert_eq!(decoded.ripcord, expected_ripcord);
}
}
Loading