diff --git a/.github/workflows/pyth.yml b/.github/workflows/pyth.yml new file mode 100644 index 0000000000..291a49bf3f --- /dev/null +++ b/.github/workflows/pyth.yml @@ -0,0 +1,51 @@ +name: Check Pythnet + +on: + pull_request: + push: + branches: [pyth-v1.14.17] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.60.0 + components: clippy + override: true + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang cmake make libprotobuf-dev + - name: Run tests + run: cargo test -p solana-runtime pyth + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.60.0 + components: clippy + override: true + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang cmake make libprotobuf-dev + - name: Check clippy + run: cargo clippy --bins --tests --examples -- --deny warnings + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2022-04-01 + components: rustfmt + override: true + - name: Check formatting + run: cargo fmt -- --check diff --git a/Cargo.lock b/Cargo.lock index 34a08fbd3f..60f548e2a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,7 +519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive 0.10.3", - "hashbrown 0.11.2", + "hashbrown 0.12.3", ] [[package]] @@ -3601,7 +3601,7 @@ dependencies = [ [[package]] name = "pyth-oracle" version = "2.33.0" -source = "git+https://github.com/pyth-network/pyth-client?tag=oracle-v2.33.0#5ee8b5f63f8b1d72f84c1177adaf564d7d61d092" +source = "git+https://github.com/pyth-network/pyth-client?rev=256b57#256b575a13e29bec93ba592c8f86cc5fad521915" dependencies = [ "bindgen 0.60.1", "bitflags 2.6.0", @@ -3615,6 +3615,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "pyth-price-publisher" +version = "0.1.0" +source = "git+https://github.com/pyth-network/pyth-crosschain?branch=add-publisher-program#1fa5089f0f5abf1fe1d48ba0dda489843fcd545a" +dependencies = [ + "bytemuck", + "solana-program 1.14.17", + "thiserror", +] + [[package]] name = "pythnet-sdk" version = "1.13.6" @@ -6200,6 +6210,7 @@ dependencies = [ "once_cell", "ouroboros", "pyth-oracle", + "pyth-price-publisher", "pythnet-sdk 1.13.6", "rand 0.7.3", "rand_chacha 0.2.2", diff --git a/core/src/validator.rs b/core/src/validator.rs index 923be65d65..ca3bafb122 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -74,7 +74,7 @@ use { accounts_db::{AccountShrinkThreshold, AccountsDbConfig}, accounts_index::AccountSecondaryIndexes, accounts_update_notifier_interface::AccountsUpdateNotifier, - bank::{pyth_accumulator, Bank}, + bank::{pyth, Bank}, bank_forks::BankForks, commitment::BlockCommitmentCache, cost_model::CostModel, @@ -1521,14 +1521,8 @@ fn load_blockstore( } } - for (key_name, pk_res) in pyth_accumulator::get_accumulator_keys() { - match pk_res { - Ok(pk) => info!("Accumulator {}: {}", key_name, pk), - Err(err) => { - error!("Failed to get Accumulator {}: {:?}", key_name, err); - std::process::abort(); - } - } + for (key_name, pk) in pyth::get_pyth_keys() { + info!("Pyth key {}: {}", key_name, pk); } leader_schedule_cache.set_fixed_leader_schedule(config.fixed_leader_schedule.clone()); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 72c9fd2090..95a0824c5c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -35,8 +35,9 @@ num-traits = { version = "0.2" } num_cpus = "1.13.1" once_cell = "1.12.0" ouroboros = "0.15.0" -pyth-oracle = { git = "https://github.com/pyth-network/pyth-client", tag = "oracle-v2.33.0", features = ["library"] } +pyth-oracle = { git = "https://github.com/pyth-network/pyth-client", rev = "256b57", features = ["library"] } pythnet-sdk = { git = "https://github.com/pyth-network/pyth-crosschain", version = "1.13.6", rev = "33f901aa45f4f0005aa5a84a1479b78ca9033074" } +pyth-price-publisher = { git = "https://github.com/pyth-network/pyth-crosschain", branch = "add-publisher-program" } rand = "0.7.0" rayon = "1.5.3" regex = "1.5.6" diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 760901289b..a5330d1fe1 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -198,10 +198,7 @@ mod builtin_programs; mod sysvar_cache; mod transaction_account_state_info; -pub mod pyth_accumulator; - -#[cfg(test)] -mod pyth_accumulator_tests; +pub mod pyth; pub const SECONDS_PER_YEAR: f64 = 365.25 * 24.0 * 60.0 * 60.0; @@ -1410,7 +1407,7 @@ impl Bank { // state before the accumulator is used. bank is in a fully // updated state before the accumulator is used. if !accumulator_moved_to_end_of_block { - pyth_accumulator::update_accumulator(&bank); + pyth::accumulator::update_accumulator(&bank); } bank @@ -1796,7 +1793,7 @@ impl Bank { // the accumulator sysvar updates. sysvars are in a fully updated // state before the accumulator sysvar updates. if !accumulator_moved_to_end_of_block { - pyth_accumulator::update_accumulator(&new); + pyth::accumulator::update_accumulator(&new); } }); @@ -3238,7 +3235,7 @@ impl Bank { // other tasks when freezing to avoid any conflicts. if accumulator_moved_to_end_of_block { let mut measure = Measure::start("accumulator"); - pyth_accumulator::update_accumulator(self); + pyth::accumulator::update_accumulator(self); measure.stop(); debug!( diff --git a/runtime/src/bank/pyth_accumulator.rs b/runtime/src/bank/pyth/accumulator.rs similarity index 91% rename from runtime/src/bank/pyth_accumulator.rs rename to runtime/src/bank/pyth/accumulator.rs index f915cc3f98..ee86f9c559 100644 --- a/runtime/src/bank/pyth_accumulator.rs +++ b/runtime/src/bank/pyth/accumulator.rs @@ -1,7 +1,11 @@ use { - super::Bank, - crate::accounts_index::{IndexKey, ScanConfig, ScanError}, + super::batch_publish, + crate::{ + accounts_index::{IndexKey, ScanConfig, ScanError}, + bank::Bank, + }, byteorder::{LittleEndian, ReadBytesExt}, + itertools::Itertools, log::*, pyth_oracle::validator::AggregationError, pythnet_sdk::{ @@ -17,7 +21,10 @@ use { hash::hashv, pubkey::Pubkey, }, - std::env::{self, VarError}, + std::{ + collections::HashMap, + env::{self, VarError}, + }, }; pub const ACCUMULATOR_RING_SIZE: u32 = 10_000; @@ -51,6 +58,13 @@ lazy_static! { .parse() .unwrap(), ); + pub static ref BATCH_PUBLISH_PID: Pubkey = env_pubkey_or( + "BATCH_PUBLISH_PID", + // TODO: replace with real program id + "FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epA" + .parse() + .unwrap(), + ); } /// Accumulator specific error type. It would be nice to use `transaction::Error` but it does @@ -116,25 +130,6 @@ fn env_pubkey_or(var: &str, default: Pubkey) -> Pubkey { } } -/// Get all accumulator related pubkeys from environment variables -/// or return default if the variable is not set. -pub fn get_accumulator_keys() -> Vec<( - &'static str, - std::result::Result, -)> { - vec![ - ("MESSAGE_BUFFER_PID", Ok(*MESSAGE_BUFFER_PID)), - ("ACCUMULATOR_EMITTER_ADDR", Ok(*ACCUMULATOR_EMITTER_ADDR)), - ("ACCUMULATOR_SEQUENCE_ADDR", Ok(*ACCUMULATOR_SEQUENCE_ADDR)), - ("WORMHOLE_PID", Ok(*WORMHOLE_PID)), - ("ORACLE_PID", Ok(*ORACLE_PID)), - ( - "STAKE_CAPS_PARAMETERS_ADDR", - Ok(*STAKE_CAPS_PARAMETERS_ADDR), - ), - ] -} - pub fn update_v1( bank: &Bank, v2_messages: &[Vec], @@ -427,21 +422,37 @@ pub fn update_v2(bank: &Bank) -> std::result::Result<(), AccumulatorUpdateErrorV v2_messages.push(publisher_stake_caps_message); } - let mut measure = Measure::start("update_v2_aggregate_price"); + let mut measure = Measure::start("extract_batch_publish_prices"); + let mut new_prices = batch_publish::extract_batch_publish_prices(bank).unwrap_or_else(|err| { + warn!("extract_batch_publish_prices failed: {}", err); + HashMap::new() + }); + measure.stop(); + debug!("batch publish: loaded prices in {}us", measure.as_us()); + let mut measure = Measure::start("update_v2_aggregate_price"); for (pubkey, mut account) in accounts { let mut price_account_data = account.data().to_owned(); + let price_account = + match pyth_oracle::validator::checked_load_price_account_mut(&mut price_account_data) { + Ok(data) => data, + Err(_err) => { + continue; + } + }; + + let mut need_save = + batch_publish::apply_published_prices(price_account, &mut new_prices, bank.slot()); // Perform Accumulation match pyth_oracle::validator::aggregate_price( bank.slot(), bank.clock().unix_timestamp, &pubkey.to_bytes().into(), - &mut price_account_data, + price_account, ) { Ok(messages) => { - account.set_data(price_account_data); - bank.store_account_and_update_capitalization(&pubkey, &account); + need_save = true; v2_messages.extend(messages); } Err(err) => match err { @@ -451,6 +462,16 @@ pub fn update_v2(bank: &Bank) -> std::result::Result<(), AccumulatorUpdateErrorV } }, } + if need_save { + account.set_data(price_account_data); + bank.store_account_and_update_capitalization(&pubkey, &account); + } + } + if !new_prices.is_empty() { + warn!( + "pyth batch publish: missing price feed accounts for indexes: {}", + new_prices.keys().join(", ") + ); } measure.stop(); diff --git a/runtime/src/bank/pyth/batch_publish.rs b/runtime/src/bank/pyth/batch_publish.rs new file mode 100644 index 0000000000..a7a7962dc6 --- /dev/null +++ b/runtime/src/bank/pyth/batch_publish.rs @@ -0,0 +1,165 @@ +use { + super::accumulator::BATCH_PUBLISH_PID, + crate::{ + accounts_index::{IndexKey, ScanConfig, ScanError}, + bank::Bank, + }, + log::{info, warn}, + pyth_oracle::{ + find_publisher_index, get_status_for_conf_price_ratio, solana_program::pubkey::Pubkey, + OracleError, PriceAccount, + }, + pyth_price_publisher::accounts::publisher_prices as publisher_prices_account, + solana_sdk::{account::ReadableAccount, clock::Slot}, + std::collections::HashMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum HandleBatchPublishError { + #[error("failed to get program accounts: {0}")] + GetProgramAccounts(#[from] ScanError), +} + +#[derive(Debug)] +pub struct PublisherPriceValue { + pub publisher: Pubkey, + pub trading_status: u32, + pub price: i64, + pub confidence: u64, +} + +pub fn extract_batch_publish_prices( + bank: &Bank, +) -> Result>, HandleBatchPublishError> { + assert!( + bank.account_indexes_include_key(&*BATCH_PUBLISH_PID), + "Oracle program account index missing" + ); + + let publisher_prices_accounts = bank + .get_filtered_indexed_accounts( + &IndexKey::ProgramId(*BATCH_PUBLISH_PID), + |account| account.owner() == &*BATCH_PUBLISH_PID, + &ScanConfig::new(true), + None, + ) + .map_err(HandleBatchPublishError::GetProgramAccounts)?; + + let mut all_prices = HashMap::>::new(); + let mut num_found_accounts = 0; + let mut num_found_prices = 0; + for (account_key, account) in publisher_prices_accounts { + if !publisher_prices_account::format_matches(account.data()) { + continue; + } + let (header, prices) = match publisher_prices_account::read(account.data()) { + Ok(r) => r, + Err(err) => { + warn!("invalid publisher prices account {}: {}", account_key, err); + continue; + } + }; + num_found_accounts += 1; + if header.slot != bank.slot() { + // Updates from earlier slots have already been applied. + continue; + } + let publisher = header.publisher.into(); + for price in prices { + let value = PublisherPriceValue { + publisher, + trading_status: price.trading_status(), + price: price.price, + confidence: price.confidence, + }; + all_prices + .entry(price.feed_index()) + .or_default() + .push(value); + num_found_prices += 1; + } + } + info!( + "pyth batch publish: found {} prices in {} accounts at slot {}", + num_found_prices, + num_found_accounts, + bank.slot() + ); + Ok(all_prices) +} + +pub fn apply_published_prices( + price_data: &mut PriceAccount, + new_prices: &mut HashMap>, + slot: Slot, +) -> bool { + if price_data.feed_index == 0 { + return false; + } + let mut any_update = false; + for new_price in new_prices + .remove(&price_data.feed_index) + .unwrap_or_default() + { + match apply_published_price(price_data, &new_price, slot) { + Ok(()) => { + any_update = true; + } + Err(err) => { + warn!( + "failed to apply publisher price to price feed {}: {}", + price_data.feed_index, err + ); + } + } + } + any_update +} + +#[derive(Debug, Error)] +enum ApplyPublishedPriceError { + #[error("publisher {1} is not allowed to publish prices for feed {0}")] + NoPermission(u32, Pubkey), + #[error("bad conf price ratio: {0}")] + BadConfPriceRatio(#[from] OracleError), + #[error("invalid publishers num_")] + InvalidPublishersNum, + #[error("invalid publisher index")] + InvalidPublisherIndex, +} + +fn apply_published_price( + price_data: &mut PriceAccount, + new_price: &PublisherPriceValue, + slot: Slot, +) -> Result<(), ApplyPublishedPriceError> { + let publishers = price_data + .comp_ + .get(..price_data.num_.try_into().unwrap()) + .ok_or(ApplyPublishedPriceError::InvalidPublishersNum)?; + + let publisher_index = find_publisher_index(publishers, &new_price.publisher).ok_or( + ApplyPublishedPriceError::NoPermission(price_data.feed_index, new_price.publisher), + )?; + + // 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( + new_price.price, + new_price.confidence, + new_price.trading_status, + )?; + + let publisher_price = &mut price_data + .comp_ + .get_mut(publisher_index) + .ok_or(ApplyPublishedPriceError::InvalidPublisherIndex)? + .latest_; + publisher_price.price_ = new_price.price; + publisher_price.conf_ = new_price.confidence; + publisher_price.status_ = status; + publisher_price.pub_slot_ = slot; + Ok(()) +} diff --git a/runtime/src/bank/pyth/mod.rs b/runtime/src/bank/pyth/mod.rs new file mode 100644 index 0000000000..bbc334d14b --- /dev/null +++ b/runtime/src/bank/pyth/mod.rs @@ -0,0 +1,27 @@ +use { + accumulator::{ + ACCUMULATOR_EMITTER_ADDR, ACCUMULATOR_SEQUENCE_ADDR, BATCH_PUBLISH_PID, MESSAGE_BUFFER_PID, + ORACLE_PID, STAKE_CAPS_PARAMETERS_ADDR, WORMHOLE_PID, + }, + solana_sdk::pubkey::Pubkey, +}; + +pub mod accumulator; +mod batch_publish; + +#[cfg(test)] +mod tests; + +/// Get all pyth related pubkeys from environment variables +/// or return default if the variable is not set. +pub fn get_pyth_keys() -> Vec<(&'static str, Pubkey)> { + vec![ + ("MESSAGE_BUFFER_PID", *MESSAGE_BUFFER_PID), + ("ACCUMULATOR_EMITTER_ADDR", *ACCUMULATOR_EMITTER_ADDR), + ("ACCUMULATOR_SEQUENCE_ADDR", *ACCUMULATOR_SEQUENCE_ADDR), + ("WORMHOLE_PID", *WORMHOLE_PID), + ("ORACLE_PID", *ORACLE_PID), + ("STAKE_CAPS_PARAMETERS_ADDR", *STAKE_CAPS_PARAMETERS_ADDR), + ("BATCH_PUBLISH_PID", *BATCH_PUBLISH_PID), + ] +} diff --git a/runtime/src/bank/pyth_accumulator_tests.rs b/runtime/src/bank/pyth/tests/accumulator_tests.rs similarity index 97% rename from runtime/src/bank/pyth_accumulator_tests.rs rename to runtime/src/bank/pyth/tests/accumulator_tests.rs index 68a1bfe0b9..70272be51b 100644 --- a/runtime/src/bank/pyth_accumulator_tests.rs +++ b/runtime/src/bank/pyth/tests/accumulator_tests.rs @@ -1,13 +1,13 @@ use { - super::pyth_accumulator::MESSAGE_BUFFER_PID, crate::{ - accounts_db::AccountShrinkThreshold, - accounts_index::{ - AccountIndex, AccountSecondaryIndexes, AccountSecondaryIndexesIncludeExclude, - }, bank::{ - pyth_accumulator::{ - get_accumulator_keys, ACCUMULATOR_RING_SIZE, ORACLE_PID, STAKE_CAPS_PARAMETERS_ADDR, + pyth::{ + accumulator::{ + ACCUMULATOR_RING_SIZE, BATCH_PUBLISH_PID, ORACLE_PID, + STAKE_CAPS_PARAMETERS_ADDR, + }, + get_pyth_keys, + tests::{create_new_bank_for_tests_with_index, new_from_parent}, }, Bank, }, @@ -33,7 +33,6 @@ use { epoch_schedule::EpochSchedule, feature::{self, Feature}, feature_set, - genesis_config::GenesisConfig, hash::hashv, pubkey::Pubkey, signature::keypair_from_seed, @@ -42,21 +41,6 @@ use { std::{io::Read, mem::size_of, sync::Arc}, }; -fn create_new_bank_for_tests_with_index(genesis_config: &GenesisConfig) -> Bank { - Bank::new_with_config_for_tests( - genesis_config, - AccountSecondaryIndexes { - keys: Some(AccountSecondaryIndexesIncludeExclude { - exclude: false, - keys: [*ORACLE_PID, *MESSAGE_BUFFER_PID].into_iter().collect(), - }), - indexes: [AccountIndex::ProgramId].into_iter().collect(), - }, - false, - AccountShrinkThreshold::default(), - ) -} - // Create Message Account Bytes // // NOTE: This was serialized by hand, but should be replaced with the pythnet-sdk @@ -395,10 +379,6 @@ fn test_update_accumulator_sysvar() { // 3. Check if message offset is > message size to prevent validator crash. } -fn new_from_parent(parent: &Arc) -> Bank { - Bank::new_from_parent(parent, &Pubkey::default(), parent.slot() + 1) -} - #[test] fn test_update_accumulator_end_of_block() { let leader_pubkey = solana_sdk::pubkey::new_rand(); @@ -1005,8 +985,6 @@ fn test_publisher_stake_caps() { ], ), generate_price(&bank, b"seeds_5", false, &[]), - - ]; // Publishers are sorted in the publisher stake caps message so we sort them here too @@ -1072,9 +1050,7 @@ fn test_publisher_stake_caps() { // We add some badly formatted stake cap parameters let mut stake_cap_parameters_account = AccountSharedData::new(42, size_of::(), &ORACLE_PID); - stake_cap_parameters_account.set_data( - vec![1,2,3,4], - ); + stake_cap_parameters_account.set_data(vec![1, 2, 3, 4]); bank.store_account(&STAKE_CAPS_PARAMETERS_ADDR, &stake_cap_parameters_account); // Nothing should change as the stake cap parameters are invalid @@ -1130,10 +1106,7 @@ fn test_publisher_stake_caps() { #[test] fn test_get_accumulator_keys() { use pythnet_sdk::{pythnet, ACCUMULATOR_EMITTER_ADDRESS, MESSAGE_BUFFER_PID}; - let accumulator_keys: Vec = get_accumulator_keys() - .iter() - .map(|(_, pk_res)| *pk_res.as_ref().unwrap()) - .collect(); + let accumulator_keys: Vec = get_pyth_keys().iter().map(|(_, pk)| *pk).collect(); let expected_pyth_keys = vec![ Pubkey::new_from_array(MESSAGE_BUFFER_PID), Pubkey::new_from_array(ACCUMULATOR_EMITTER_ADDRESS), @@ -1141,6 +1114,7 @@ fn test_get_accumulator_keys() { Pubkey::new_from_array(pythnet::WORMHOLE_PID), *ORACLE_PID, *STAKE_CAPS_PARAMETERS_ADDR, + *BATCH_PUBLISH_PID, ]; assert_eq!(accumulator_keys, expected_pyth_keys); } diff --git a/runtime/src/bank/pyth/tests/batch_publish_tests.rs b/runtime/src/bank/pyth/tests/batch_publish_tests.rs new file mode 100644 index 0000000000..b26f547976 --- /dev/null +++ b/runtime/src/bank/pyth/tests/batch_publish_tests.rs @@ -0,0 +1,160 @@ +use { + crate::{ + bank::pyth::{ + accumulator::{BATCH_PUBLISH_PID, ORACLE_PID}, + tests::{create_new_bank_for_tests_with_index, new_from_parent}, + }, + genesis_utils::{create_genesis_config_with_leader, GenesisConfigInfo}, + }, + bytemuck::{cast_slice, checked::from_bytes}, + pyth_oracle::{ + solana_program::account_info::AccountInfo, PriceAccount, PriceAccountFlags, PythAccount, + }, + pyth_price_publisher::accounts::publisher_prices::{ + self as publisher_prices_account, PublisherPrice, + }, + solana_sdk::{ + account::{AccountSharedData, ReadableAccount, WritableAccount}, + clock::Epoch, + epoch_schedule::EpochSchedule, + feature_set, + pubkey::Pubkey, + signature::keypair_from_seed, + signer::Signer, + }, + std::{mem::size_of, sync::Arc}, +}; + +#[test] +fn test_batch_publish() { + let leader_pubkey = solana_sdk::pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + + // Set epoch length to 32 so we can advance epochs quickly. We also skip past slot 0 here + // due to slot 0 having special handling. + let slots_in_epoch = 32; + genesis_config.epoch_schedule = EpochSchedule::new(slots_in_epoch); + let mut bank = create_new_bank_for_tests_with_index(&genesis_config); + + let generate_publisher = |seed, new_prices| { + let publisher1_key = keypair_from_seed(seed).unwrap(); + + let (publisher1_prices_key, _bump) = Pubkey::find_program_address( + &[b"BUFFER", &publisher1_key.pubkey().to_bytes()], + &BATCH_PUBLISH_PID, + ); + let mut publisher1_prices_account = + AccountSharedData::new(42, publisher_prices_account::size(100), &BATCH_PUBLISH_PID); + { + let (header, prices) = publisher_prices_account::create( + publisher1_prices_account.data_mut(), + publisher1_key.pubkey().to_bytes(), + ) + .unwrap(); + publisher_prices_account::extend(header, prices, cast_slice(new_prices)).unwrap(); + } + bank.store_account(&publisher1_prices_key, &publisher1_prices_account); + + publisher1_key + }; + + let publishers = [ + generate_publisher( + &[1u8; 32], + &[ + PublisherPrice::new(1, 1, 10, 2).unwrap(), + PublisherPrice::new(2, 1, 20, 3).unwrap(), + // Attempt to publish with price_index == 0, + // but it will not be applied. + PublisherPrice { + trading_status_and_feed_index: 0, + price: 30, + confidence: 35, + }, + ], + ), + generate_publisher( + &[2u8; 32], + &[ + PublisherPrice::new(1, 1, 15, 2).unwrap(), + PublisherPrice::new(2, 1, 25, 3).unwrap(), + ], + ), + ]; + + let generate_price = |seeds, index| { + let (price_feed_key, _bump) = Pubkey::find_program_address(&[seeds], &ORACLE_PID); + let mut price_feed_account = + AccountSharedData::new(42, size_of::(), &ORACLE_PID); + + let messages = { + let price_feed_info_key = &price_feed_key.to_bytes().into(); + let price_feed_info_lamports = &mut 0; + let price_feed_info_owner = &ORACLE_PID.to_bytes().into(); + let price_feed_info_data = price_feed_account.data_mut(); + let price_feed_info = AccountInfo::new( + price_feed_info_key, + false, + true, + price_feed_info_lamports, + price_feed_info_data, + price_feed_info_owner, + false, + Epoch::default(), + ); + + let mut price_account = PriceAccount::initialize(&price_feed_info, 0).unwrap(); + price_account.flags.insert( + PriceAccountFlags::ACCUMULATOR_V2 | PriceAccountFlags::MESSAGE_BUFFER_CLEARED, + ); + price_account.feed_index = index; + price_account.comp_[0].pub_ = publishers[0].pubkey().to_bytes().into(); + price_account.comp_[1].pub_ = publishers[1].pubkey().to_bytes().into(); + price_account.num_ = 2; + }; + + bank.store_account(&price_feed_key, &price_feed_account); + (price_feed_key, messages) + }; + + assert!(bank + .feature_set + .is_active(&feature_set::enable_accumulator_sysvar::id())); + assert!(bank + .feature_set + .is_active(&feature_set::move_accumulator_to_end_of_block::id())); + assert!(bank + .feature_set + .is_active(&feature_set::undo_move_accumulator_to_end_of_block::id())); + assert!(bank + .feature_set + .is_active(&feature_set::redo_move_accumulator_to_end_of_block::id())); + + let prices_with_messages = [ + generate_price(b"seeds_1", 1), + generate_price(b"seeds_2", 2), + generate_price(b"seeds_3", 3), + generate_price(b"seeds_4", 0), + ]; + + bank = new_from_parent(&Arc::new(bank)); // Advance slot 1. + bank = new_from_parent(&Arc::new(bank)); // Advance slot 2. + + let new_price_feed1_account = bank.get_account(&prices_with_messages[0].0).unwrap(); + let new_price_feed1_data: &PriceAccount = from_bytes(new_price_feed1_account.data()); + assert_eq!(new_price_feed1_data.comp_[0].latest_.price_, 10); + assert_eq!(new_price_feed1_data.comp_[0].latest_.conf_, 2); + assert_eq!(new_price_feed1_data.comp_[0].latest_.status_, 1); + assert_eq!(new_price_feed1_data.comp_[1].latest_.price_, 15); + + let new_price_feed2_account = bank.get_account(&prices_with_messages[1].0).unwrap(); + let new_price_feed2_data: &PriceAccount = from_bytes(new_price_feed2_account.data()); + assert_eq!(new_price_feed2_data.comp_[0].latest_.price_, 20); + assert_eq!(new_price_feed2_data.comp_[1].latest_.price_, 25); + + let new_price_feed4_account = bank.get_account(&prices_with_messages[3].0).unwrap(); + let new_price_feed4_data: &PriceAccount = from_bytes(new_price_feed4_account.data()); + assert_eq!(new_price_feed4_data.comp_[0].latest_.price_, 0); +} diff --git a/runtime/src/bank/pyth/tests/mod.rs b/runtime/src/bank/pyth/tests/mod.rs new file mode 100644 index 0000000000..5fef8052b9 --- /dev/null +++ b/runtime/src/bank/pyth/tests/mod.rs @@ -0,0 +1,36 @@ +use { + super::accumulator::{BATCH_PUBLISH_PID, MESSAGE_BUFFER_PID, ORACLE_PID}, + crate::{ + accounts_db::AccountShrinkThreshold, + accounts_index::{ + AccountIndex, AccountSecondaryIndexes, AccountSecondaryIndexesIncludeExclude, + }, + bank::Bank, + }, + solana_sdk::{genesis_config::GenesisConfig, pubkey::Pubkey}, + std::sync::Arc, +}; + +mod accumulator_tests; +mod batch_publish_tests; + +fn new_from_parent(parent: &Arc) -> Bank { + Bank::new_from_parent(parent, &Pubkey::default(), parent.slot() + 1) +} + +fn create_new_bank_for_tests_with_index(genesis_config: &GenesisConfig) -> Bank { + Bank::new_with_config_for_tests( + genesis_config, + AccountSecondaryIndexes { + keys: Some(AccountSecondaryIndexesIncludeExclude { + exclude: false, + keys: [*ORACLE_PID, *MESSAGE_BUFFER_PID, *BATCH_PUBLISH_PID] + .into_iter() + .collect(), + }), + indexes: [AccountIndex::ProgramId].into_iter().collect(), + }, + false, + AccountShrinkThreshold::default(), + ) +} diff --git a/validator/src/main.rs b/validator/src/main.rs index 711c3b7806..4103606a42 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -52,7 +52,7 @@ use { AccountIndex, AccountSecondaryIndexes, AccountSecondaryIndexesIncludeExclude, AccountsIndexConfig, IndexLimitMb, }, - bank::pyth_accumulator::{MESSAGE_BUFFER_PID, ORACLE_PID}, + bank::pyth::accumulator::{BATCH_PUBLISH_PID, MESSAGE_BUFFER_PID, ORACLE_PID}, hardened_unpack::MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, runtime_config::RuntimeConfig, snapshot_config::SnapshotConfig, @@ -3200,24 +3200,28 @@ fn process_account_indexes(matches: &ArgMatches) -> AccountSecondaryIndexes { let exclude_keys = !account_indexes_exclude_keys.is_empty(); let include_keys = !account_indexes_include_keys.is_empty(); - if include_keys { - if !account_indexes_include_keys.contains(&*ORACLE_PID) + if include_keys + && (!account_indexes_include_keys.contains(&*ORACLE_PID) || !account_indexes_include_keys.contains(&*MESSAGE_BUFFER_PID) - { - panic!( + || !account_indexes_include_keys.contains(&*BATCH_PUBLISH_PID)) + { + panic!( "The oracle program id and message buffer program id must be included in the account index. Add the following flags\n\ --account-index-include-key {}\n\ + --account-index-include-key {}\n\ --account-index-include-key {}\n", - &*ORACLE_PID, &*MESSAGE_BUFFER_PID + &*ORACLE_PID, &*MESSAGE_BUFFER_PID, &*BATCH_PUBLISH_PID, ); - } } if exclude_keys { - if account_indexes_exclude_keys.contains(&*ORACLE_PID) - || account_indexes_exclude_keys.contains(&*MESSAGE_BUFFER_PID) - { - panic!("The oracle program id and message buffer program id must *not* be excluded from the account index."); + for key in &[&*ORACLE_PID, &*MESSAGE_BUFFER_PID, &*BATCH_PUBLISH_PID] { + if account_indexes_exclude_keys.contains(key) { + panic!( + "This key must *not* be excluded from the account index: {}", + key + ); + } } }