diff --git a/.gitignore b/.gitignore index 1897cb58..9f253318 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,12 @@ target cmake-build-* /venv +# CMake files +features.h + # IntelliJ / CLion configuration .idea *.iml -# CMake files -features.h \ No newline at end of file +# Clang format +.clang-format \ No newline at end of file diff --git a/program/c/src/oracle/oracle.h b/program/c/src/oracle/oracle.h index 05c62338..61fbc7b5 100644 --- a/program/c/src/oracle/oracle.h +++ b/program/c/src/oracle/oracle.h @@ -23,7 +23,7 @@ extern "C" { #define PC_MAP_TABLE_SIZE 640 // Total price component slots available -#define PC_NUM_COMP_PYTHNET 128 +#define PC_NUM_COMP_PYTHNET 127 // PC_NUM_COMP - number of price components in use // Not whole PC_NUM_COMP_PYTHNET because of stack issues appearing in upd_aggregate() diff --git a/program/c/src/oracle/upd_aggregate.h b/program/c/src/oracle/upd_aggregate.h index 21ae8e45..94179940 100644 --- a/program/c/src/oracle/upd_aggregate.h +++ b/program/c/src/oracle/upd_aggregate.h @@ -134,14 +134,6 @@ static inline void upd_twap( // update aggregate price static inline bool upd_aggregate( pc_price_t *ptr, uint64_t slot, int64_t timestamp ) { - // Update the value of the previous price, if it had TRADING status. - if ( ptr->agg_.status_ == PC_STATUS_TRADING ) { - ptr->prev_slot_ = ptr->agg_.pub_slot_; - ptr->prev_price_ = ptr->agg_.price_; - ptr->prev_conf_ = ptr->agg_.conf_; - ptr->prev_timestamp_ = ptr->timestamp_; - } - // update aggregate details ready for next slot ptr->valid_slot_ = ptr->agg_.pub_slot_;// valid slot-time of agg. price ptr->agg_.pub_slot_ = slot; // publish slot-time of agg. price diff --git a/program/rust/src/accounts/price.rs b/program/rust/src/accounts/price.rs index 421cc827..bcc76a59 100644 --- a/program/rust/src/accounts/price.rs +++ b/program/rust/src/accounts/price.rs @@ -31,62 +31,113 @@ mod price_pythnet { }, error::OracleError, }, + std::ops::{ + Deref, + DerefMut, + Index, + IndexMut, + }, }; + #[repr(C)] + #[derive(Copy, Clone)] + pub struct PriceComponentArrayWrapper([PriceComponent; PC_NUM_COMP_PYTHNET as usize]); + + // Implementing Index and IndexMut allows PriceComponentArrayWrapper to use array indexing directly, + // such as price_account.comp_[i], making it behave more like a native array or slice. + impl Index for PriceComponentArrayWrapper { + type Output = PriceComponent; + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } + } + impl IndexMut for PriceComponentArrayWrapper { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.0[index] + } + } + + // Implementing Deref and DerefMut allows PriceComponentArrayWrapper to use slice methods directly, + // such as len(), making it behave more like a native array or slice. + impl Deref for PriceComponentArrayWrapper { + type Target = [PriceComponent]; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for PriceComponentArrayWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + unsafe impl Pod for PriceComponentArrayWrapper { + } + unsafe impl Zeroable for PriceComponentArrayWrapper { + } + /// Pythnet-only extended price account format. This extension is /// an append-only change that adds extra publisher slots and /// PriceCumulative for TWAP processing. #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] pub struct PriceAccountPythnet { - pub header: AccountHeader, + pub header: AccountHeader, /// Type of the price account - pub price_type: u32, + pub price_type: u32, /// Exponent for the published prices - pub exponent: i32, + pub exponent: i32, /// Current number of authorized publishers - pub num_: u32, + pub num_: u32, /// Number of valid quotes for the last aggregation - pub num_qt_: u32, + pub num_qt_: u32, /// Last slot with a succesful aggregation (status : TRADING) - pub last_slot_: u64, + pub last_slot_: u64, /// Second to last slot where aggregation was attempted - pub valid_slot_: u64, + pub valid_slot_: u64, /// Ema for price - pub twap_: PriceEma, + pub twap_: PriceEma, /// Ema for confidence - pub twac_: PriceEma, + pub twac_: PriceEma, /// Last time aggregation was attempted - pub timestamp_: i64, + pub timestamp_: i64, /// Minimum valid publisher quotes for a succesful aggregation - pub min_pub_: u8, - pub message_sent_: u8, + pub min_pub_: u8, + pub message_sent_: u8, /// Configurable max latency in slots between send and receive - pub max_latency_: u8, + pub max_latency_: u8, /// Unused placeholder for alignment - pub unused_2_: i8, - pub unused_3_: i32, + pub unused_2_: i8, + pub unused_3_: i32, /// Corresponding product account - pub product_account: Pubkey, + pub product_account: Pubkey, /// Next price account in the list - pub next_price_account: Pubkey, + pub next_price_account: Pubkey, /// Second to last slot where aggregation was succesful (i.e. status : TRADING) - pub prev_slot_: u64, + pub prev_slot_: u64, /// Aggregate price at prev_slot_ - pub prev_price_: i64, + pub prev_price_: i64, /// Confidence interval at prev_slot_ - pub prev_conf_: u64, + pub prev_conf_: u64, /// Timestamp of prev_slot_ - pub prev_timestamp_: i64, + pub prev_timestamp_: i64, /// Last attempted aggregate results - pub agg_: PriceInfo, + pub agg_: PriceInfo, /// Publishers' price components. NOTE(2023-10-06): On Pythnet, not all /// PC_NUM_COMP_PYTHNET slots are used due to stack size /// issues in the C code. For iterating over price components, /// PC_NUM_COMP must be used. - pub comp_: [PriceComponent; PC_NUM_COMP_PYTHNET as usize], + pub comp_: PriceComponentArrayWrapper, + /// Previous EMA for price and confidence + pub prev_twap_: PriceEma, + pub prev_twac_: PriceEma, + /// Previous TWAP cumulative values + pub prev_price_cumulative: PriceCumulative, /// Cumulative sums of aggregative price and confidence used to compute arithmetic moving averages - pub price_cumulative: PriceCumulative, + pub price_cumulative: PriceCumulative, } impl PriceAccountPythnet { diff --git a/program/rust/src/processor/upd_price.rs b/program/rust/src/processor/upd_price.rs index 5e62f241..e1440ad6 100644 --- a/program/rust/src/processor/upd_price.rs +++ b/program/rust/src/processor/upd_price.rs @@ -2,10 +2,10 @@ use { crate::{ accounts::{ PriceAccount, - PriceInfo, PythOracleSerialize, UPD_PRICE_WRITE_SEED, }, + c_oracle_header::PC_STATUS_TRADING, deserialize::{ load, load_checked, @@ -128,7 +128,8 @@ pub fn upd_price( let clock = Clock::from_account_info(clock_account)?; let mut publisher_index: usize = 0; - let latest_aggregate_price: PriceInfo; + let slots_since_last_successful_aggregate: u64; + let noninitial_price_update_after_program_upgrade: bool; // The price_data borrow happens in a scope because it must be // dropped before we borrow again as raw data pointer for the C @@ -149,7 +150,16 @@ pub fn upd_price( OracleError::PermissionViolation.into(), )?; - latest_aggregate_price = price_data.agg_; + // We use last_slot_ to calculate slots_since_last_successful_aggregate. This is because last_slot_ is updated after the aggregate price is updated successfully. + slots_since_last_successful_aggregate = clock.slot - price_data.last_slot_; + + // Check if the program upgrade has happened in the current slot and aggregate price has been updated, if so, use the old logic to update twap/twac/price_cumulative. + // This is to ensure that twap/twac/price_cumulative are calculated correctly during the migration. + // We check if prev_twap_.denom_ is == 0 because when the program upgrade has happened, denom_ is initialized to 0 and it can only stay the same or increase while numer_ can be negative if prices are negative. + // And we check if slots_since_last_successful_aggregate == 0 to check if the aggregate price has been updated in the current slot. + noninitial_price_update_after_program_upgrade = + price_data.prev_twap_.denom_ == 0 && slots_since_last_successful_aggregate == 0; + let latest_publisher_price = price_data.comp_[publisher_index].latest_; // Check that publisher is publishing a more recent price @@ -161,10 +171,38 @@ pub fn upd_price( )?; } - // Try to update the aggregate - #[allow(unused_variables)] - if clock.slot > latest_aggregate_price.pub_slot_ { - let updated = unsafe { + // Extend the scope of the mutable borrow of price_data + { + let mut price_data = load_checked::(price_account, cmd_args.header.version)?; + + // Update the publisher's price + if is_component_update(cmd_args)? { + let status: u32 = get_status_for_conf_price_ratio( + cmd_args.price, + cmd_args.confidence, + cmd_args.status, + )?; + let publisher_price = &mut price_data.comp_[publisher_index].latest_; + publisher_price.price_ = cmd_args.price; + publisher_price.conf_ = cmd_args.confidence; + publisher_price.status_ = status; + publisher_price.pub_slot_ = cmd_args.publishing_slot; + } + + // If the price update is the first in the slot and the aggregate is trading, update the previous slot, price, conf, and timestamp. + if slots_since_last_successful_aggregate > 0 && price_data.agg_.status_ == PC_STATUS_TRADING + { + price_data.prev_slot_ = price_data.agg_.pub_slot_; + price_data.prev_price_ = price_data.agg_.price_; + price_data.prev_conf_ = price_data.agg_.conf_; + price_data.prev_timestamp_ = price_data.timestamp_; + } + } + + let updated = unsafe { + if noninitial_price_update_after_program_upgrade { + false + } else { // NOTE: c_upd_aggregate must use a raw pointer to price // data. Solana's `.borrow_*` methods require exclusive // access, i.e. no other borrow can exist for the account. @@ -173,25 +211,41 @@ pub fn upd_price( clock.slot, clock.unix_timestamp, ) - }; - - // If the aggregate was successfully updated, calculate the difference and update TWAP. - if updated { - let agg_diff = (clock.slot as i64) - - load_checked::(price_account, cmd_args.header.version)?.prev_slot_ - as i64; - // Encapsulate TWAP update logic in a function to minimize unsafe block scope. - unsafe { - c_upd_twap(price_account.try_borrow_mut_data()?.as_mut_ptr(), agg_diff); - } + } + }; + + // If the aggregate was successfully updated, calculate the difference and update TWAP. + if updated { + { let mut price_data = load_checked::(price_account, cmd_args.header.version)?; + + // Multiple price updates may occur within the same slot. Updates within the same slot will + // use the previously calculated values (prev_twap, prev_twac, and prev_price_cumulative) + // from the last successful aggregated price update as their basis for recalculation. This + // ensures that each update within a slot builds upon the last and not the twap/twac/price_cumulative + // that is calculated right after the publishers' individual price updates. + if slots_since_last_successful_aggregate > 0 { + price_data.prev_twap_ = price_data.twap_; + price_data.prev_twac_ = price_data.twac_; + price_data.prev_price_cumulative = price_data.price_cumulative; + } + price_data.twap_ = price_data.prev_twap_; + price_data.twac_ = price_data.prev_twac_; + price_data.price_cumulative = price_data.prev_price_cumulative; + + price_data.update_price_cumulative()?; // We want to send a message every time the aggregate price updates. However, during the migration, // not every publisher will necessarily provide the accumulator accounts. The message_sent_ flag // ensures that after every aggregate update, the next publisher who provides the accumulator accounts // will send the message. price_data.message_sent_ = 0; - price_data.update_price_cumulative()?; + } + let agg_diff = (clock.slot as i64) + - load_checked::(price_account, cmd_args.header.version)?.prev_slot_ + as i64; + unsafe { + c_upd_twap(price_account.try_borrow_mut_data()?.as_mut_ptr(), agg_diff); } } @@ -261,23 +315,6 @@ pub fn upd_price( } } - // Try to update the publisher's price - if is_component_update(cmd_args)? { - // IMPORTANT: If the publisher does not meet the price/conf - // ratio condition, its price will not count for the next - // aggregate. - let status: u32 = - get_status_for_conf_price_ratio(cmd_args.price, cmd_args.confidence, cmd_args.status)?; - - { - let publisher_price = &mut price_data.comp_[publisher_index].latest_; - publisher_price.price_ = cmd_args.price; - publisher_price.conf_ = cmd_args.confidence; - publisher_price.status_ = status; - publisher_price.pub_slot_ = cmd_args.publishing_slot; - } - } - Ok(()) } diff --git a/program/rust/src/tests/mod.rs b/program/rust/src/tests/mod.rs index 8208d153..fdfa8e70 100644 --- a/program/rust/src/tests/mod.rs +++ b/program/rust/src/tests/mod.rs @@ -19,13 +19,10 @@ mod test_publish_batch; mod test_set_max_latency; mod test_set_min_pub; mod test_sizes; +mod test_twap; mod test_upd_aggregate; mod test_upd_permissions; mod test_upd_price; mod test_upd_price_no_fail_on_error; mod test_upd_product; mod test_utils; - - -mod test_twap; -mod test_upd_price_v2; diff --git a/program/rust/src/tests/test_ema.rs b/program/rust/src/tests/test_ema.rs index 1a1cae10..4973ec59 100644 --- a/program/rust/src/tests/test_ema.rs +++ b/program/rust/src/tests/test_ema.rs @@ -1,12 +1,31 @@ extern crate test_generator; use { + super::test_utils::update_clock_slot, crate::{ - accounts::PriceAccount, + accounts::{ + PriceAccount, + PythAccount, + }, + c_oracle_header::{ + PC_STATUS_TRADING, + PC_STATUS_UNKNOWN, + PC_VERSION, + }, + deserialize::{ + load_checked, + load_mut, + }, + instruction::{ + OracleCommand, + UpdPriceArgs, + }, processor::{ c_upd_aggregate, c_upd_twap, + process_instruction, }, + tests::test_utils::AccountSetup, }, bytemuck::Zeroable, csv::ReaderBuilder, @@ -14,10 +33,161 @@ use { Deserialize, Serialize, }, - std::fs::File, + solana_program::pubkey::Pubkey, + solana_sdk::account_info::AccountInfo, + std::{ + fs::File, + mem::size_of, + }, test_generator::test_resources, }; +#[test] +fn test_ema_multiple_publishers_same_slot() -> Result<(), Box> { + let mut instruction_data = [0u8; size_of::()]; + + let program_id = Pubkey::new_unique(); + + let mut funding_setup = AccountSetup::new_funding(); + let funding_account = funding_setup.as_account_info(); + + let mut price_setup = AccountSetup::new::(&program_id); + let mut price_account = price_setup.as_account_info(); + price_account.is_signer = false; + PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); + + add_publisher(&mut price_account, funding_account.key); + + let mut clock_setup = AccountSetup::new_clock(); + let mut clock_account = clock_setup.as_account_info(); + clock_account.is_signer = false; + clock_account.is_writable = false; + + update_clock_slot(&mut clock_account, 1); + + populate_instruction(&mut instruction_data, 10, 1, 1); + process_instruction( + &program_id, + &[ + funding_account.clone(), + price_account.clone(), + clock_account.clone(), + ], + &instruction_data, + )?; + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.prev_twap_.val_, 0); + assert_eq!(price_data.prev_twac_.val_, 0); + assert_eq!(price_data.twap_.val_, 10); + assert_eq!(price_data.twac_.val_, 1); + } + + // add new test for multiple publishers and ensure that ema is updated correctly + let mut funding_setup_two = AccountSetup::new_funding(); + let funding_account_two = funding_setup_two.as_account_info(); + + add_publisher(&mut price_account, funding_account_two.key); + + update_clock_slot(&mut clock_account, 2); + + populate_instruction(&mut instruction_data, 20, 1, 2); + process_instruction( + &program_id, + &[ + funding_account.clone(), + price_account.clone(), + clock_account.clone(), + ], + &instruction_data, + )?; + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.prev_twap_.val_, 10); + assert_eq!(price_data.prev_twac_.val_, 1); + assert_eq!(price_data.twap_.val_, 15); + assert_eq!(price_data.twac_.val_, 1); + } + + populate_instruction(&mut instruction_data, 30, 1, 2); + process_instruction( + &program_id, + &[ + funding_account_two.clone(), + price_account.clone(), + clock_account.clone(), + ], + &instruction_data, + )?; + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.prev_twap_.val_, 10); + assert_eq!(price_data.prev_twac_.val_, 1); + // The EMA value decreases to 12 despite an increase in the aggregate price due to the higher confidence associated with the new price. + // The EMA calculation considers the weight of 1/confidence for the new price, leading to a lower weighted average when the confidence is high. + assert_eq!(price_data.twap_.val_, 12); + assert_eq!(price_data.twac_.val_, 1); + } + + // add test for multiple publishers where the first publisher causes the aggregate to be unknown and second publisher causes it to be trading + { + let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + price_data.min_pub_ = 2; + price_data.num_ = 1; + } + + update_clock_slot(&mut clock_account, 3); + + populate_instruction(&mut instruction_data, 20, 1, 3); + process_instruction( + &program_id, + &[ + funding_account.clone(), + price_account.clone(), + clock_account.clone(), + ], + &instruction_data, + )?; + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.prev_twap_.val_, 10); + assert_eq!(price_data.prev_twac_.val_, 1); + assert_eq!(price_data.twap_.val_, 12); + assert_eq!(price_data.twac_.val_, 1); + } + + { + let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + price_data.min_pub_ = 2; + price_data.num_ = 2; + } + + populate_instruction(&mut instruction_data, 40, 1, 3); + process_instruction( + &program_id, + &[ + funding_account_two.clone(), + price_account.clone(), + clock_account.clone(), + ], + &instruction_data, + )?; + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_twap_.val_, 12); + assert_eq!(price_data.prev_twac_.val_, 1); + assert_eq!(price_data.twap_.val_, 13); + assert_eq!(price_data.twac_.val_, 2); + } + Ok(()) +} #[test_resources("program/rust/test_data/ema/*.csv")] fn test_ema(input_path_raw: &str) { @@ -110,7 +280,6 @@ fn run_ema_test(inputs: &[InputRecord], expected_outputs: &[OutputRecord]) { } } - // TODO: put these functions somewhere more accessible pub fn upd_aggregate( price_account: &mut PriceAccount, @@ -130,7 +299,6 @@ pub fn upd_twap(price_account: &mut PriceAccount, nslots: i64) { unsafe { c_upd_twap((price_account as *mut PriceAccount) as *mut u8, nslots) } } - #[derive(Serialize, Deserialize, Debug)] struct InputRecord { price: i64, @@ -148,3 +316,20 @@ struct OutputRecord { twap: i64, twac: i64, } + +fn populate_instruction(instruction_data: &mut [u8], price: i64, conf: u64, pub_slot: u64) { + let mut cmd = load_mut::(instruction_data).unwrap(); + cmd.header = OracleCommand::UpdPrice.into(); + cmd.status = PC_STATUS_TRADING; + cmd.price = price; + cmd.confidence = conf; + cmd.publishing_slot = pub_slot; + cmd.unused_ = 0; +} + +fn add_publisher(price_account: &mut AccountInfo, publisher_key: &Pubkey) { + let mut price_data = load_checked::(price_account, PC_VERSION).unwrap(); + let index = price_data.num_ as usize; + price_data.comp_[index].pub_ = *publisher_key; + price_data.num_ += 1; +} diff --git a/program/rust/src/tests/test_full_publisher_set.rs b/program/rust/src/tests/test_full_publisher_set.rs index bc4cfd13..87f48b71 100644 --- a/program/rust/src/tests/test_full_publisher_set.rs +++ b/program/rust/src/tests/test_full_publisher_set.rs @@ -36,7 +36,6 @@ async fn test_full_publisher_set() -> Result<(), Box> { .await; let price = price_accounts["LTC"]; - let n_pubs = pub_keypairs.len(); // Divide publishers into two even parts (assuming the max PC_NUM_COMP size is even) @@ -61,18 +60,6 @@ async fn test_full_publisher_set() -> Result<(), Box> { sim.upd_price(second_kp, price, second_quote).await?; } - // Advance slot once from 1 to 2 - sim.warp_to_slot(2).await?; - - // Final price update to trigger aggregation - let first_kp = pub_keypairs.first().unwrap(); - let first_quote = Quote { - price: 100, - confidence: 30, - status: PC_STATUS_TRADING, - }; - sim.upd_price(first_kp, price, first_quote).await?; - { let price_data = sim .get_account_data_as::(price) diff --git a/program/rust/src/tests/test_publish.rs b/program/rust/src/tests/test_publish.rs index 208642a4..f10489d8 100644 --- a/program/rust/src/tests/test_publish.rs +++ b/program/rust/src/tests/test_publish.rs @@ -73,10 +73,9 @@ async fn test_publish() { assert_eq!(price_data.comp_[0].latest_.price_, 150); assert_eq!(price_data.comp_[0].latest_.conf_, 7); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - - assert_eq!(price_data.comp_[0].agg_.price_, 0); - assert_eq!(price_data.comp_[0].agg_.conf_, 0); - assert_eq!(price_data.comp_[0].agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.comp_[0].agg_.price_, 150); + assert_eq!(price_data.comp_[0].agg_.conf_, 7); + assert_eq!(price_data.comp_[0].agg_.status_, PC_STATUS_TRADING); } sim.warp_to_slot(2).await.unwrap(); @@ -102,8 +101,8 @@ async fn test_publish() { assert_eq!(price_data.comp_[0].latest_.conf_, 0); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_UNKNOWN); - assert_eq!(price_data.comp_[0].agg_.price_, 150); - assert_eq!(price_data.comp_[0].agg_.conf_, 7); - assert_eq!(price_data.comp_[0].agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.comp_[0].agg_.price_, 0); + assert_eq!(price_data.comp_[0].agg_.conf_, 0); + assert_eq!(price_data.comp_[0].agg_.status_, PC_STATUS_UNKNOWN); } } diff --git a/program/rust/src/tests/test_publish_batch.rs b/program/rust/src/tests/test_publish_batch.rs index f1c5424b..f40f97b5 100644 --- a/program/rust/src/tests/test_publish_batch.rs +++ b/program/rust/src/tests/test_publish_batch.rs @@ -84,9 +84,12 @@ async fn test_publish_batch() { price_data.comp_[0].latest_.status_, get_status_for_conf_price_ratio(quote.price, quote.confidence, quote.status).unwrap() ); - assert_eq!(price_data.comp_[0].agg_.price_, 0); - assert_eq!(price_data.comp_[0].agg_.conf_, 0); - assert_eq!(price_data.comp_[0].agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.comp_[0].agg_.price_, quote.price); + assert_eq!(price_data.comp_[0].agg_.conf_, quote.confidence); + assert_eq!( + price_data.comp_[0].agg_.status_, + get_status_for_conf_price_ratio(quote.price, quote.confidence, quote.status).unwrap() + ); } sim.warp_to_slot(2).await.unwrap(); @@ -111,7 +114,6 @@ async fn test_publish_batch() { .get_account_data_as::(*price) .await .unwrap(); - let quote = quotes.get(key).unwrap(); let new_quote = new_quotes.get(key).unwrap(); assert_eq!(price_data.comp_[0].latest_.price_, new_quote.price); @@ -125,11 +127,16 @@ async fn test_publish_batch() { ) .unwrap() ); - assert_eq!(price_data.comp_[0].agg_.price_, quote.price); - assert_eq!(price_data.comp_[0].agg_.conf_, quote.confidence); + assert_eq!(price_data.comp_[0].agg_.price_, new_quote.price); + assert_eq!(price_data.comp_[0].agg_.conf_, new_quote.confidence); assert_eq!( price_data.comp_[0].agg_.status_, - get_status_for_conf_price_ratio(quote.price, quote.confidence, quote.status).unwrap() + get_status_for_conf_price_ratio( + new_quote.price, + new_quote.confidence, + new_quote.status + ) + .unwrap() ); } } diff --git a/program/rust/src/tests/test_sizes.rs b/program/rust/src/tests/test_sizes.rs index 9ec1e37b..50e68341 100644 --- a/program/rust/src/tests/test_sizes.rs +++ b/program/rust/src/tests/test_sizes.rs @@ -6,6 +6,7 @@ use { PermissionAccount, PriceAccount, PriceComponent, + PriceCumulative, PriceEma, PriceInfo, ProductAccount, @@ -14,6 +15,7 @@ use { c_oracle_header::{ PC_MAP_TABLE_SIZE, PC_NUM_COMP, + PC_NUM_COMP_PYTHNET, PC_VERSION, ZSTD_UPPER_BOUND, }, @@ -52,30 +54,22 @@ fn test_sizes() { size_of::(), size_of::() + 2 * size_of::() ); - - { - use crate::{ - accounts::PriceCumulative, - c_oracle_header::PC_NUM_COMP_PYTHNET, - }; - - // Sanity-check the Pythnet PC_NUM_COMP - assert_eq!(PC_NUM_COMP, 64); - - assert_eq!( - size_of::(), - 48 + u64::BITS as usize - + 3 * size_of::() - + size_of::() - + (PC_NUM_COMP_PYTHNET as usize) * size_of::() - + size_of::() - ); - assert_eq!(size_of::(), 12576); - assert!(size_of::() == try_convert::<_, usize>(ZSTD_UPPER_BOUND).unwrap()); - - assert_eq!(size_of::(), 48); - } - + // Sanity-check the Pythnet PC_NUM_COMP + assert_eq!(PC_NUM_COMP, 64); + assert_eq!( + size_of::(), + 48 + u64::BITS as usize + + 3 * size_of::() + + size_of::() + + (PC_NUM_COMP_PYTHNET as usize) * size_of::() + + size_of::() + + size_of::() + + size_of::() + + size_of::() + ); + assert_eq!(size_of::(), 12576); + assert!(size_of::() == try_convert::<_, usize>(ZSTD_UPPER_BOUND).unwrap()); + assert_eq!(size_of::(), 48); assert_eq!(size_of::(), 8); assert_eq!(size_of::(), 16); assert_eq!(size_of::(), 16); diff --git a/program/rust/src/tests/test_upd_aggregate.rs b/program/rust/src/tests/test_upd_aggregate.rs index c9e962cf..657c6cd6 100644 --- a/program/rust/src/tests/test_upd_aggregate.rs +++ b/program/rust/src/tests/test_upd_aggregate.rs @@ -67,7 +67,6 @@ fn test_upd_aggregate() { corp_act_status_: 0, }; - let mut instruction_data = [0u8; size_of::()]; populate_instruction(&mut instruction_data, 42, 2, 1); @@ -78,11 +77,42 @@ fn test_upd_aggregate() { price_account.is_signer = false; PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); + // test same slot aggregation, aggregate price and conf should be updated + { + let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + price_data.num_ = 1; + price_data.last_slot_ = 1000; + price_data.agg_.pub_slot_ = 1000; + price_data.comp_[0].latest_ = p1; + } + unsafe { + assert!(c_upd_aggregate( + price_account.try_borrow_mut_data().unwrap().as_mut_ptr(), + 1000, + 1, + )); + } + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + + assert_eq!(price_data.agg_.price_, 100); + assert_eq!(price_data.agg_.conf_, 10); + assert_eq!(price_data.num_qt_, 1); + assert_eq!(price_data.timestamp_, 1); + assert_eq!(price_data.prev_slot_, 0); + assert_eq!(price_data.prev_price_, 0); + assert_eq!(price_data.prev_conf_, 0); + assert_eq!(price_data.prev_timestamp_, 0); + } + // single publisher { let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); price_data.num_ = 1; + price_data.num_qt_ = 0; price_data.last_slot_ = 1000; + price_data.timestamp_ = 0; price_data.agg_.pub_slot_ = 1000; price_data.comp_[0].latest_ = p1; } @@ -134,10 +164,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.agg_.conf_, 55); assert_eq!(price_data.num_qt_, 2); assert_eq!(price_data.timestamp_, 2); - assert_eq!(price_data.prev_slot_, 1000); - assert_eq!(price_data.prev_price_, 100); - assert_eq!(price_data.prev_conf_, 10); - assert_eq!(price_data.prev_timestamp_, 1); } // three publishers @@ -167,10 +193,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.agg_.conf_, 90); assert_eq!(price_data.num_qt_, 3); assert_eq!(price_data.timestamp_, 3); - assert_eq!(price_data.prev_slot_, 1000); - assert_eq!(price_data.prev_price_, 145); - assert_eq!(price_data.prev_conf_, 55); - assert_eq!(price_data.prev_timestamp_, 2); } // four publishers @@ -202,10 +224,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.num_qt_, 4); assert_eq!(price_data.timestamp_, 4); assert_eq!(price_data.last_slot_, 1001); - assert_eq!(price_data.prev_slot_, 1000); - assert_eq!(price_data.prev_price_, 200); - assert_eq!(price_data.prev_conf_, 90); - assert_eq!(price_data.prev_timestamp_, 3); } unsafe { @@ -223,10 +241,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.last_slot_, 1025); assert_eq!(price_data.num_qt_, 4); assert_eq!(price_data.timestamp_, 5); - assert_eq!(price_data.prev_slot_, 1001); - assert_eq!(price_data.prev_price_, 245); - assert_eq!(price_data.prev_conf_, 85); - assert_eq!(price_data.prev_timestamp_, 4); } // check what happens when nothing publishes for a while @@ -245,10 +259,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.last_slot_, 1025); assert_eq!(price_data.num_qt_, 0); assert_eq!(price_data.timestamp_, 10); - assert_eq!(price_data.prev_slot_, 1025); - assert_eq!(price_data.prev_price_, 245); - assert_eq!(price_data.prev_conf_, 85); - assert_eq!(price_data.prev_timestamp_, 5); } unsafe { @@ -266,10 +276,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.last_slot_, 1025); assert_eq!(price_data.num_qt_, 0); assert_eq!(price_data.timestamp_, 12); - assert_eq!(price_data.prev_slot_, 1025); - assert_eq!(price_data.prev_price_, 245); - assert_eq!(price_data.prev_conf_, 85); - assert_eq!(price_data.prev_timestamp_, 5); } // ensure the update occurs within the PC_MAX_SEND_LATENCY limit of 25 slots, allowing the aggregated price to reflect both p4 and p5 contributions @@ -298,10 +304,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.agg_.conf_, 55); assert_eq!(price_data.num_qt_, 2); assert_eq!(price_data.timestamp_, 13); - assert_eq!(price_data.prev_slot_, 1025); - assert_eq!(price_data.prev_price_, 245); - assert_eq!(price_data.prev_conf_, 85); - assert_eq!(price_data.prev_timestamp_, 5); } // verify behavior when publishing halts for 1 slot, causing the slot difference from p5 to exceed the PC_MAX_SEND_LATENCY threshold of 25. @@ -321,10 +323,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.agg_.conf_, 50); assert_eq!(price_data.num_qt_, 1); assert_eq!(price_data.timestamp_, 14); - assert_eq!(price_data.prev_slot_, 1025); - assert_eq!(price_data.prev_price_, 445); - assert_eq!(price_data.prev_conf_, 55); - assert_eq!(price_data.prev_timestamp_, 13); } // verify behavior when max_latency_ is set to 5, and all components pub_slot_ gap is more than 5, this should result in PC_STATUS_UNKNOWN status @@ -356,10 +354,6 @@ fn test_upd_aggregate() { assert_eq!(price_data.agg_.conf_, 50); assert_eq!(price_data.num_qt_, 0); assert_eq!(price_data.timestamp_, 15); - assert_eq!(price_data.prev_slot_, 1000); - assert_eq!(price_data.prev_price_, 500); - assert_eq!(price_data.prev_conf_, 50); - assert_eq!(price_data.prev_timestamp_, 14); } } diff --git a/program/rust/src/tests/test_upd_price.rs b/program/rust/src/tests/test_upd_price.rs index 9726cda7..d370030f 100644 --- a/program/rust/src/tests/test_upd_price.rs +++ b/program/rust/src/tests/test_upd_price.rs @@ -21,6 +21,7 @@ use { processor::process_instruction, tests::test_utils::{ update_clock_slot, + update_clock_timestamp, AccountSetup, }, }, @@ -28,6 +29,7 @@ use { program_error::ProgramError, pubkey::Pubkey, }, + solana_sdk::account_info::AccountInfo, std::mem::size_of, }; @@ -58,6 +60,7 @@ fn test_upd_price() { clock_account.is_writable = false; update_clock_slot(&mut clock_account, 1); + update_clock_timestamp(&mut clock_account, 1); assert!(process_instruction( &program_id, @@ -77,9 +80,17 @@ fn test_upd_price() { assert_eq!(price_data.comp_[0].latest_.pub_slot_, 1); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 0); + assert_eq!(price_data.last_slot_, 1); assert_eq!(price_data.agg_.pub_slot_, 1); - assert_eq!(price_data.agg_.price_, 0); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.agg_.price_, 42); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 0); + assert_eq!(price_data.prev_price_, 0); + assert_eq!(price_data.prev_conf_, 0); + assert_eq!(price_data.prev_timestamp_, 0); + assert_eq!(price_data.price_cumulative.price, 42); + assert_eq!(price_data.price_cumulative.conf, 2); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } // add some prices for current slot - get rejected @@ -105,12 +116,20 @@ fn test_upd_price() { assert_eq!(price_data.comp_[0].latest_.pub_slot_, 1); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 0); + assert_eq!(price_data.last_slot_, 1); assert_eq!(price_data.agg_.pub_slot_, 1); - assert_eq!(price_data.agg_.price_, 0); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.agg_.price_, 42); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 0); + assert_eq!(price_data.prev_price_, 0); + assert_eq!(price_data.prev_conf_, 0); + assert_eq!(price_data.prev_timestamp_, 0); + assert_eq!(price_data.price_cumulative.price, 42); + assert_eq!(price_data.price_cumulative.conf, 2); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } - // add next price in new slot triggering snapshot and aggregate calc + // update new price in new slot, aggregate should be updated and prev values should be updated populate_instruction(&mut instruction_data, 81, 2, 2); update_clock_slot(&mut clock_account, 3); @@ -132,14 +151,23 @@ fn test_upd_price() { assert_eq!(price_data.comp_[0].latest_.pub_slot_, 2); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 1); + assert_eq!(price_data.last_slot_, 3); assert_eq!(price_data.agg_.pub_slot_, 3); - assert_eq!(price_data.agg_.price_, 42); + assert_eq!(price_data.agg_.price_, 81); assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 1); + assert_eq!(price_data.prev_price_, 42); + assert_eq!(price_data.prev_conf_, 2); + assert_eq!(price_data.prev_timestamp_, 1); + assert_eq!(price_data.price_cumulative.price, 42 + 2 * 81); + assert_eq!(price_data.price_cumulative.conf, 3 * 2); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } - // next price doesn't change but slot does + // next price doesn't change but slot and timestamp does populate_instruction(&mut instruction_data, 81, 2, 3); update_clock_slot(&mut clock_account, 4); + update_clock_timestamp(&mut clock_account, 4); assert!(process_instruction( &program_id, &[ @@ -158,9 +186,18 @@ fn test_upd_price() { assert_eq!(price_data.comp_[0].latest_.pub_slot_, 3); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 3); + assert_eq!(price_data.last_slot_, 4); assert_eq!(price_data.agg_.pub_slot_, 4); assert_eq!(price_data.agg_.price_, 81); assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 3); + assert_eq!(price_data.prev_price_, 81); + assert_eq!(price_data.prev_conf_, 2); + assert_eq!(price_data.timestamp_, 4); // We only check for timestamp_ here because test_upd_price doesn't directly update timestamp_, this is updated through c_upd_aggregate which is tested in test_upd_aggregate, but we assert here to show that in subsequent asserts for prev_timestamp_ the value should be updated to this value + assert_eq!(price_data.prev_timestamp_, 1); + assert_eq!(price_data.price_cumulative.price, 42 + 3 * 81); + assert_eq!(price_data.price_cumulative.conf, 4 * 2); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } // next price doesn't change and neither does aggregate but slot does @@ -184,9 +221,17 @@ fn test_upd_price() { assert_eq!(price_data.comp_[0].latest_.pub_slot_, 4); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 4); + assert_eq!(price_data.last_slot_, 5); assert_eq!(price_data.agg_.pub_slot_, 5); assert_eq!(price_data.agg_.price_, 81); assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 4); + assert_eq!(price_data.prev_price_, 81); + assert_eq!(price_data.prev_conf_, 2); + assert_eq!(price_data.prev_timestamp_, 4); + assert_eq!(price_data.price_cumulative.price, 42 + 81 * 4); + assert_eq!(price_data.price_cumulative.conf, 5 * 2); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } // try to publish back-in-time @@ -212,9 +257,17 @@ fn test_upd_price() { assert_eq!(price_data.comp_[0].latest_.pub_slot_, 4); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 4); + assert_eq!(price_data.last_slot_, 5); assert_eq!(price_data.agg_.pub_slot_, 5); assert_eq!(price_data.agg_.price_, 81); assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 4); + assert_eq!(price_data.prev_price_, 81); + assert_eq!(price_data.prev_conf_, 2); + assert_eq!(price_data.prev_timestamp_, 4); + assert_eq!(price_data.price_cumulative.price, 42 + 81 * 4); + assert_eq!(price_data.price_cumulative.conf, 5 * 2); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } populate_instruction(&mut instruction_data, 50, 20, 5); @@ -246,14 +299,22 @@ fn test_upd_price() { assert_eq!(price_data.comp_[0].latest_.pub_slot_, 5); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_IGNORED); assert_eq!(price_data.valid_slot_, 5); + assert_eq!(price_data.last_slot_, 5); assert_eq!(price_data.agg_.pub_slot_, 6); assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.prev_slot_, 5); + assert_eq!(price_data.prev_price_, 81); + assert_eq!(price_data.prev_conf_, 2); + assert_eq!(price_data.prev_timestamp_, 4); + assert_eq!(price_data.price_cumulative.price, 42 + 81 * 4); + assert_eq!(price_data.price_cumulative.conf, 2 * 5); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } - // Crank one more time and aggregate should be unknown - populate_instruction(&mut instruction_data, 50, 20, 6); - update_clock_slot(&mut clock_account, 7); + // Negative prices are accepted + populate_instruction(&mut instruction_data, -100, 1, 7); + update_clock_slot(&mut clock_account, 8); assert!(process_instruction( &program_id, @@ -265,22 +326,34 @@ fn test_upd_price() { &instruction_data ) .is_ok()); - { let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 50); - assert_eq!(price_data.comp_[0].latest_.conf_, 20); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 6); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_IGNORED); + assert_eq!(price_data.comp_[0].latest_.price_, -100); + assert_eq!(price_data.comp_[0].latest_.conf_, 1); + assert_eq!(price_data.comp_[0].latest_.pub_slot_, 7); + assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 6); - assert_eq!(price_data.agg_.pub_slot_, 7); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.last_slot_, 8); + assert_eq!(price_data.agg_.pub_slot_, 8); + assert_eq!(price_data.agg_.price_, -100); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 5); + assert_eq!(price_data.prev_price_, 81); + assert_eq!(price_data.prev_conf_, 2); + assert_eq!(price_data.prev_timestamp_, 4); + assert_eq!(price_data.price_cumulative.price, 42 + 81 * 4 - 100 * 3); + assert_eq!(price_data.price_cumulative.conf, 2 * 5 + 3); // 2 * 5 + 1 * 3 + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } - // Negative prices are accepted - populate_instruction(&mut instruction_data, -100, 1, 7); - update_clock_slot(&mut clock_account, 8); + // add new test for multiple publishers and ensure that agg price is not updated multiple times when program upgrade happens in the same slot after the first update + let mut funding_setup_two = AccountSetup::new_funding(); + let funding_account_two = funding_setup_two.as_account_info(); + + add_publisher(&mut price_account, funding_account_two.key); + + populate_instruction(&mut instruction_data, 10, 1, 10); + update_clock_slot(&mut clock_account, 10); assert!(process_instruction( &program_id, @@ -295,19 +368,77 @@ fn test_upd_price() { { let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, -100); + assert_eq!(price_data.comp_[0].latest_.price_, 10); assert_eq!(price_data.comp_[0].latest_.conf_, 1); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 7); + assert_eq!(price_data.comp_[0].latest_.pub_slot_, 10); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 7); - assert_eq!(price_data.agg_.pub_slot_, 8); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.valid_slot_, 8); + assert_eq!(price_data.last_slot_, 10); + assert_eq!(price_data.agg_.pub_slot_, 10); + assert_eq!(price_data.agg_.price_, 10); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 8); + assert_eq!(price_data.prev_price_, -100); + assert_eq!(price_data.prev_conf_, 1); + assert_eq!(price_data.prev_timestamp_, 4); + assert_eq!(price_data.twap_.numer_, 1677311098); + assert_eq!(price_data.twap_.denom_, 1279419481); + assert_eq!( + price_data.price_cumulative.price, + 42 + 81 * 4 - 100 * 3 + 10 * 2 + ); + assert_eq!(price_data.price_cumulative.conf, 2 * 5 + 5); // 2 * 5 + 1 * 5 + assert_eq!(price_data.price_cumulative.num_down_slots, 0); } - // Crank again for aggregate - populate_instruction(&mut instruction_data, -100, 1, 8); - update_clock_slot(&mut clock_account, 9); + // reset twap_.denom_ to 0 to simulate program upgrade in the same slot and make sure agg_.price_ is not updated again + { + let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + price_data.prev_twap_.denom_ = 0; + } + populate_instruction(&mut instruction_data, 20, 1, 10); + assert!(process_instruction( + &program_id, + &[ + funding_account_two.clone(), + price_account.clone(), + clock_account.clone() + ], + &instruction_data + ) + .is_ok()); + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.comp_[1].latest_.price_, 20); + assert_eq!(price_data.comp_[1].latest_.conf_, 1); + assert_eq!(price_data.comp_[1].latest_.pub_slot_, 10); + assert_eq!(price_data.comp_[1].latest_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.valid_slot_, 8); + assert_eq!(price_data.last_slot_, 10); + assert_eq!(price_data.agg_.pub_slot_, 10); + assert_eq!(price_data.agg_.price_, 10); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 8); + assert_eq!(price_data.prev_price_, -100); + assert_eq!(price_data.prev_conf_, 1); + assert_eq!(price_data.prev_timestamp_, 4); + assert_eq!(price_data.twap_.numer_, 1677311098); // twap_.numer_ should not be updated + assert_eq!(price_data.twap_.denom_, 1279419481); // twap_.denom_ should not be updated + + // price_cumulative should not be updated + assert_eq!( + price_data.price_cumulative.price, + 42 + 81 * 4 - 100 * 3 + 10 * 2 + ); + assert_eq!(price_data.price_cumulative.conf, 2 * 5 + 5); + assert_eq!(price_data.price_cumulative.num_down_slots, 0); + } + + remove_publisher(&mut price_account); + // Big gap + populate_instruction(&mut instruction_data, 60, 4, 50); + update_clock_slot(&mut clock_account, 50); assert!(process_instruction( &program_id, @@ -322,14 +453,111 @@ fn test_upd_price() { { let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, -100); + assert_eq!(price_data.comp_[0].latest_.price_, 60); + assert_eq!(price_data.comp_[0].latest_.conf_, 4); + assert_eq!(price_data.comp_[0].latest_.pub_slot_, 50); + assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.valid_slot_, 10); + assert_eq!(price_data.agg_.pub_slot_, 50); + assert_eq!(price_data.agg_.price_, 60); + assert_eq!(price_data.agg_.conf_, 4); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!( + price_data.price_cumulative.price, + 42 + 81 * 4 - 100 * 3 + 10 * 2 + 60 * 40 + ); + assert_eq!(price_data.price_cumulative.conf, 2 * 5 + 5 + 40 * 4); + assert_eq!(price_data.price_cumulative.num_down_slots, 15); + } + + // add new test for multiple publishers and ensure that price_cumulative is updated correctly + add_publisher(&mut price_account, funding_account_two.key); + populate_instruction(&mut instruction_data, 10, 1, 100); + update_clock_slot(&mut clock_account, 100); + assert!(process_instruction( + &program_id, + &[ + funding_account.clone(), + price_account.clone(), + clock_account.clone() + ], + &instruction_data + ) + .is_ok()); + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.comp_[0].latest_.price_, 10); assert_eq!(price_data.comp_[0].latest_.conf_, 1); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 8); + assert_eq!(price_data.comp_[0].latest_.pub_slot_, 100); assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 8); - assert_eq!(price_data.agg_.pub_slot_, 9); - assert_eq!(price_data.agg_.price_, -100); + assert_eq!(price_data.valid_slot_, 50); + assert_eq!(price_data.agg_.pub_slot_, 100); + assert_eq!(price_data.agg_.price_, 10); + assert_eq!(price_data.agg_.conf_, 1); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 50); + assert_eq!(price_data.prev_price_, 60); + assert_eq!(price_data.prev_conf_, 4); + assert_eq!( + price_data.prev_price_cumulative.price, + 42 + 81 * 4 - 100 * 3 + 10 * 2 + 60 * 40 + ); + assert_eq!(price_data.prev_price_cumulative.conf, 2 * 5 + 5 + 40 * 4); + assert_eq!(price_data.prev_price_cumulative.num_down_slots, 15); + assert_eq!( + price_data.price_cumulative.price, + 42 + 81 * 4 - 100 * 3 + 10 * 2 + 60 * 40 + 10 * 50 + ); // (42 + 81 * 4 - 100 * 3 + 10 * 2 + 60 * 40) is the price cumulative from the previous test and 10 * 50 (slot_gap) is the price cumulative from this test + assert_eq!(price_data.price_cumulative.conf, 2 * 5 + 5 + 40 * 4 + 50); + assert_eq!(price_data.price_cumulative.num_down_slots, 40); // prev num_down_slots was 15 and since pub slot is 100 and last pub slot was 50, slot_gap is 50 and default latency is 25, so num_down_slots = 50 - 25 = 25, so total num_down_slots = 15 + 25 = 40 + } + + populate_instruction(&mut instruction_data, 20, 2, 100); + assert!(process_instruction( + &program_id, + &[ + funding_account_two.clone(), + price_account.clone(), + clock_account.clone() + ], + &instruction_data + ) + .is_ok()); + + { + let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); + assert_eq!(price_data.comp_[0].latest_.price_, 10); + assert_eq!(price_data.comp_[0].latest_.conf_, 1); + assert_eq!(price_data.comp_[0].latest_.pub_slot_, 100); + assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.comp_[1].latest_.price_, 20); + assert_eq!(price_data.comp_[1].latest_.conf_, 2); + assert_eq!(price_data.comp_[1].latest_.pub_slot_, 100); + assert_eq!(price_data.comp_[1].latest_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.valid_slot_, 100); + assert_eq!(price_data.agg_.pub_slot_, 100); + assert_eq!(price_data.agg_.price_, 14); + assert_eq!(price_data.agg_.conf_, 6); assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); + assert_eq!(price_data.prev_slot_, 50); + assert_eq!(price_data.prev_price_, 60); + assert_eq!(price_data.prev_conf_, 4); + assert_eq!( + price_data.prev_price_cumulative.price, + 42 + 81 * 4 - 100 * 3 + 10 * 2 + 60 * 40 + ); + assert_eq!(price_data.prev_price_cumulative.conf, 2 * 5 + 5 + 40 * 4); + assert_eq!(price_data.prev_price_cumulative.num_down_slots, 15); + assert_eq!( + price_data.price_cumulative.price, + 42 + 81 * 4 - 100 * 3 + 10 * 2 + 60 * 40 + 14 * 50 + ); // (42 + 81 * 4 - 100 * 3 + 10 * 2 + 60 * 40) is the price cumulative from the previous test and 14 * 50 (slot_gap) is the price cumulative from this test + assert_eq!( + price_data.price_cumulative.conf, + 2 * 5 + 5 + 40 * 4 + 6 * 50 + ); + assert_eq!(price_data.price_cumulative.num_down_slots, 40); // prev num_down_slots was 15 and since pub slot is 100 and last pub slot was 50, slot_gap is 50 and default latency is 25, so num_down_slots = 50 - 25 = 25, so total num_down_slots = 15 + 25 = 40 } } @@ -343,3 +571,17 @@ fn populate_instruction(instruction_data: &mut [u8], price: i64, conf: u64, pub_ cmd.publishing_slot = pub_slot; cmd.unused_ = 0; } + +fn add_publisher(price_account: &mut AccountInfo, publisher_key: &Pubkey) { + let mut price_data = load_checked::(price_account, PC_VERSION).unwrap(); + let index = price_data.num_ as usize; + price_data.comp_[index].pub_ = *publisher_key; + price_data.num_ += 1; +} + +fn remove_publisher(price_account: &mut AccountInfo) { + let mut price_data = load_checked::(price_account, PC_VERSION).unwrap(); + if price_data.num_ > 0 { + price_data.num_ -= 1; + } +} diff --git a/program/rust/src/tests/test_upd_price_no_fail_on_error.rs b/program/rust/src/tests/test_upd_price_no_fail_on_error.rs index 3483ad1d..1d175d8b 100644 --- a/program/rust/src/tests/test_upd_price_no_fail_on_error.rs +++ b/program/rust/src/tests/test_upd_price_no_fail_on_error.rs @@ -52,7 +52,6 @@ fn test_upd_price_no_fail_on_error_no_fail_on_error() { update_clock_slot(&mut clock_account, 1); - // Check that the normal upd_price fails populate_instruction(&mut instruction_data, 42, 9, 1, true); @@ -69,7 +68,6 @@ fn test_upd_price_no_fail_on_error_no_fail_on_error() { Err(OracleError::PermissionViolation.into()) ); - populate_instruction(&mut instruction_data, 42, 9, 1, false); // We haven't permissioned the publish account for the price account // yet, so any update should fail silently and have no effect. The @@ -85,7 +83,6 @@ fn test_upd_price_no_fail_on_error_no_fail_on_error() { ) .is_ok()); - { let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); assert_eq!(price_data.comp_[0].latest_.price_, 0); @@ -122,8 +119,8 @@ fn test_upd_price_no_fail_on_error_no_fail_on_error() { assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 0); assert_eq!(price_data.agg_.pub_slot_, 1); - assert_eq!(price_data.agg_.price_, 0); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.agg_.price_, 42); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); } // Invalid updates, such as publishing an update for the current slot, @@ -164,12 +161,11 @@ fn test_upd_price_no_fail_on_error_no_fail_on_error() { assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); assert_eq!(price_data.valid_slot_, 0); assert_eq!(price_data.agg_.pub_slot_, 1); - assert_eq!(price_data.agg_.price_, 0); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); + assert_eq!(price_data.agg_.price_, 42); + assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); } } - // Create an upd_price_no_fail_on_error or upd_price instruction with the provided parameters fn populate_instruction( instruction_data: &mut [u8], diff --git a/program/rust/src/tests/test_upd_price_v2.rs b/program/rust/src/tests/test_upd_price_v2.rs deleted file mode 100644 index 0e0c1f4b..00000000 --- a/program/rust/src/tests/test_upd_price_v2.rs +++ /dev/null @@ -1,456 +0,0 @@ -use { - crate::{ - accounts::{ - PriceAccount, - PythAccount, - }, - c_oracle_header::{ - PC_STATUS_IGNORED, - PC_STATUS_TRADING, - PC_STATUS_UNKNOWN, - PC_VERSION, - }, - deserialize::{ - load_checked, - load_mut, - }, - instruction::{ - OracleCommand, - UpdPriceArgs, - }, - processor::process_instruction, - tests::test_utils::{ - update_clock_slot, - AccountSetup, - }, - }, - solana_program::{ - program_error::ProgramError, - pubkey::Pubkey, - }, - std::mem::size_of, -}; - -#[test] -fn test_upd_price_v2() -> Result<(), Box> { - let mut instruction_data = [0u8; size_of::()]; - populate_instruction(&mut instruction_data, 42, 2, 1); - - let program_id = Pubkey::new_unique(); - - let mut funding_setup = AccountSetup::new_funding(); - let funding_account = funding_setup.as_account_info(); - - let mut price_setup = AccountSetup::new::(&program_id); - let mut price_account = price_setup.as_account_info(); - price_account.is_signer = false; - PriceAccount::initialize(&price_account, PC_VERSION).unwrap(); - - { - let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - price_data.num_ = 1; - price_data.comp_[0].pub_ = *funding_account.key; - } - - let mut clock_setup = AccountSetup::new_clock(); - let mut clock_account = clock_setup.as_account_info(); - clock_account.is_signer = false; - clock_account.is_writable = false; - - update_clock_slot(&mut clock_account, 1); - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 42); - assert_eq!(price_data.comp_[0].latest_.conf_, 2); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 1); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 0); - assert_eq!(price_data.agg_.pub_slot_, 1); - assert_eq!(price_data.agg_.price_, 0); - assert_eq!(price_data.agg_.conf_, 0); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); - - assert_eq!(price_data.price_cumulative.price, 0); - assert_eq!(price_data.price_cumulative.conf, 0); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // add some prices for current slot - get rejected - populate_instruction(&mut instruction_data, 43, 2, 1); - - assert_eq!( - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone() - ], - &instruction_data - ), - Err(ProgramError::InvalidArgument) - ); - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 42); - assert_eq!(price_data.comp_[0].latest_.conf_, 2); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 1); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 0); - assert_eq!(price_data.agg_.pub_slot_, 1); - assert_eq!(price_data.agg_.price_, 0); - assert_eq!(price_data.agg_.conf_, 0); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); - - assert_eq!(price_data.price_cumulative.price, 0); - assert_eq!(price_data.price_cumulative.conf, 0); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // add next price in new slot triggering snapshot and aggregate calc - populate_instruction(&mut instruction_data, 81, 2, 2); - update_clock_slot(&mut clock_account, 3); - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 81); - assert_eq!(price_data.comp_[0].latest_.conf_, 2); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 2); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 1); - assert_eq!(price_data.agg_.pub_slot_, 3); - assert_eq!(price_data.agg_.price_, 42); - assert_eq!(price_data.agg_.conf_, 2); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - assert_eq!(price_data.price_cumulative.price, 3 * 42); - assert_eq!(price_data.price_cumulative.conf, 3 * 2); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // next price doesn't change but slot does - populate_instruction(&mut instruction_data, 81, 2, 3); - update_clock_slot(&mut clock_account, 4); - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 81); - assert_eq!(price_data.comp_[0].latest_.conf_, 2); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 3); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 3); - assert_eq!(price_data.agg_.pub_slot_, 4); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.conf_, 2); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // next price doesn't change and neither does aggregate but slot does - populate_instruction(&mut instruction_data, 81, 2, 4); - update_clock_slot(&mut clock_account, 5); - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 81); - assert_eq!(price_data.comp_[0].latest_.conf_, 2); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 4); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 4); - assert_eq!(price_data.agg_.pub_slot_, 5); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.conf_, 2); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81 * 2); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 2); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // try to publish back-in-time - populate_instruction(&mut instruction_data, 81, 2, 1); - update_clock_slot(&mut clock_account, 5); - assert_eq!( - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone() - ], - &instruction_data - ), - Err(ProgramError::InvalidArgument) - ); - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 81); - assert_eq!(price_data.comp_[0].latest_.conf_, 2); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 4); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 4); - assert_eq!(price_data.agg_.pub_slot_, 5); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.conf_, 2); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81 * 2); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 2); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - populate_instruction(&mut instruction_data, 50, 20, 5); - update_clock_slot(&mut clock_account, 6); - - // Publishing a wide CI results in a status of unknown. - - // check that someone doesn't accidentally break the test. - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - } - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 50); - assert_eq!(price_data.comp_[0].latest_.conf_, 20); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 5); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_IGNORED); - assert_eq!(price_data.valid_slot_, 5); - assert_eq!(price_data.agg_.pub_slot_, 6); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.conf_, 2); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81 * 3); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 3); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // Crank one more time and aggregate should be unknown - populate_instruction(&mut instruction_data, 50, 20, 6); - update_clock_slot(&mut clock_account, 7); - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 50); - assert_eq!(price_data.comp_[0].latest_.conf_, 20); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 6); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_IGNORED); - assert_eq!(price_data.valid_slot_, 6); - assert_eq!(price_data.agg_.pub_slot_, 7); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.conf_, 2); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81 * 3); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 3); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // Negative prices are accepted - populate_instruction(&mut instruction_data, -100, 1, 7); - update_clock_slot(&mut clock_account, 8); - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, -100); - assert_eq!(price_data.comp_[0].latest_.conf_, 1); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 7); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 7); - assert_eq!(price_data.agg_.pub_slot_, 8); - assert_eq!(price_data.agg_.price_, 81); - assert_eq!(price_data.agg_.conf_, 2); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81 * 3); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 3); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // Crank again for aggregate - populate_instruction(&mut instruction_data, -100, 1, 8); - update_clock_slot(&mut clock_account, 9); - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, -100); - assert_eq!(price_data.comp_[0].latest_.conf_, 1); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 8); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 8); - assert_eq!(price_data.agg_.pub_slot_, 9); - assert_eq!(price_data.agg_.price_, -100); - assert_eq!(price_data.agg_.conf_, 1); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81 * 3 - 100 * 3); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 3 + 3); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // Big gap - - populate_instruction(&mut instruction_data, 60, 4, 50); - update_clock_slot(&mut clock_account, 50); - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 60); - assert_eq!(price_data.comp_[0].latest_.conf_, 4); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 50); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 9); - assert_eq!(price_data.agg_.pub_slot_, 50); - assert_eq!(price_data.agg_.price_, -100); - assert_eq!(price_data.agg_.conf_, 1); - assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN); - - assert_eq!(price_data.price_cumulative.price, 3 * 42 + 81 * 3 - 100 * 3); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 3 + 3); - assert_eq!(price_data.price_cumulative.num_down_slots, 0); - } - - // Crank again for aggregate - - populate_instruction(&mut instruction_data, 55, 5, 51); - update_clock_slot(&mut clock_account, 51); - - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.comp_[0].latest_.price_, 55); - assert_eq!(price_data.comp_[0].latest_.conf_, 5); - assert_eq!(price_data.comp_[0].latest_.pub_slot_, 51); - assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING); - assert_eq!(price_data.valid_slot_, 50); - assert_eq!(price_data.agg_.pub_slot_, 51); - assert_eq!(price_data.agg_.price_, 60); - assert_eq!(price_data.agg_.conf_, 4); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - assert_eq!( - price_data.price_cumulative.price, - 3 * 42 + 81 * 3 - 100 * 3 + 42 * 60 - ); - assert_eq!(price_data.price_cumulative.conf, 3 * 2 + 2 * 3 + 3 + 42 * 4); - assert_eq!(price_data.price_cumulative.num_down_slots, 17); - } - - Ok(()) -} - -// Create an upd_price instruction with the provided parameters -fn populate_instruction(instruction_data: &mut [u8], price: i64, conf: u64, pub_slot: u64) { - let mut cmd = load_mut::(instruction_data).unwrap(); - cmd.header = OracleCommand::UpdPrice.into(); - cmd.status = PC_STATUS_TRADING; - cmd.price = price; - cmd.confidence = conf; - cmd.publishing_slot = pub_slot; - cmd.unused_ = 0; -} diff --git a/program/rust/src/tests/test_utils.rs b/program/rust/src/tests/test_utils.rs index 853fdcd6..b5f02582 100644 --- a/program/rust/src/tests/test_utils.rs +++ b/program/rust/src/tests/test_utils.rs @@ -134,6 +134,12 @@ pub fn update_clock_slot(clock_account: &mut AccountInfo, slot: u64) { clock_data.to_account_info(clock_account); } +pub fn update_clock_timestamp(clock_account: &mut AccountInfo, timestamp: i64) { + let mut clock_data = clock::Clock::from_account_info(clock_account).unwrap(); + clock_data.unix_timestamp = timestamp; + clock_data.to_account_info(clock_account); +} + impl From for CommandHeader { fn from(val: OracleCommand) -> Self { CommandHeader {