diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..1bfb9a75 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "rust-analyzer.check.command": "clippy", + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "rust-analyzer.rustfmt.overrideCommand": [ + "rustfmt", + "+nightly" + ], + "editor.formatOnSave": true, +} \ No newline at end of file diff --git a/program/c/src/oracle/oracle.h b/program/c/src/oracle/oracle.h index 05c62338..5e4f3b50 100644 --- a/program/c/src/oracle/oracle.h +++ b/program/c/src/oracle/oracle.h @@ -21,8 +21,12 @@ extern "C" { #define PC_PUBKEY_SIZE 32 #define PC_PUBKEY_SIZE_64 (PC_PUBKEY_SIZE/sizeof(uint64_t)) #define PC_MAP_TABLE_SIZE 640 +#define PC_MAX_PUBLISHERS 256 +#define PC_MAX_SYMBOLS 1024 +// This is the number of 64-bit words needed to store the bitset of symbols +#define PC_MAX_SYMBOLS_64 (PC_MAX_SYMBOLS/64) - // Total price component slots available +// Total price component slots available #define PC_NUM_COMP_PYTHNET 128 // PC_NUM_COMP - number of price components in use @@ -49,11 +53,12 @@ extern "C" { #define PC_STATUS_IGNORED 4 // account types -#define PC_ACCTYPE_MAPPING 1 -#define PC_ACCTYPE_PRODUCT 2 -#define PC_ACCTYPE_PRICE 3 -#define PC_ACCTYPE_TEST 4 -#define PC_ACCTYPE_PERMISSIONS 5 +#define PC_ACCTYPE_MAPPING 1 +#define PC_ACCTYPE_PRODUCT 2 +#define PC_ACCTYPE_PRICE 3 +#define PC_ACCTYPE_TEST 4 +#define PC_ACCTYPE_PERMISSIONS 5 +#define PC_ACCTYPE_SCORE 6 // Compute budget requested per price update instruction diff --git a/program/rust/Cargo.toml b/program/rust/Cargo.toml index df753755..b4b90bbc 100644 --- a/program/rust/Cargo.toml +++ b/program/rust/Cargo.toml @@ -13,11 +13,12 @@ solana-program = "=1.13.3" bytemuck = "1.11.0" thiserror = "1.0" num-derive = "0.3" -num-traits = "0.2" byteorder = "1.4.3" serde = { version = "1.0", features = ["derive"], optional = true } strum = { version = "0.24.1", features = ["derive"], optional = true } pythnet-sdk = { git = "https://github.com/pyth-network/pyth-crosschain", rev="60144002053a93f424be70decd8a8ccb8d618d81"} +num-traits = "0.2" + [dev-dependencies] solana-program-test = "=1.13.3" @@ -38,6 +39,7 @@ csv = "1.1" check = [] # Skips make build in build.rs, use with cargo-clippy and cargo-check debug = [] library = [] +test = [] [lib] crate-type = ["cdylib", "lib"] diff --git a/program/rust/build.rs b/program/rust/build.rs index b33d8dd7..e8d06278 100644 --- a/program/rust/build.rs +++ b/program/rust/build.rs @@ -89,10 +89,10 @@ fn do_make_build(targets: Vec<&str>, out_dir: &Path) { if !make_output.status.success() { panic!( "C oracle make build did not exit with 0 (code - ({:?}).\n\nstdout:\n{}\n\nstderr:\n{}", + ({:?}).\n\nstdout:\n{}\n\nstderr:\n{}", make_output.status.code(), - String::from_utf8(make_output.stdout).unwrap_or("".to_owned()), - String::from_utf8(make_output.stderr).unwrap_or("".to_owned()) + String::from_utf8(make_output.stdout).unwrap_or_else(|_| "".to_owned()), + String::from_utf8(make_output.stderr).unwrap_or_else(|_| "".to_owned()) ); } } diff --git a/program/rust/src/accounts.rs b/program/rust/src/accounts.rs index 5958fc88..9639123c 100644 --- a/program/rust/src/accounts.rs +++ b/program/rust/src/accounts.rs @@ -1,5 +1,13 @@ //! Account types and utilities for working with Pyth accounts. +#[cfg(any(feature = "test", test))] +use { + crate::utils::get_rent, + crate::utils::try_convert, + solana_program::program::invoke_signed, + solana_program::pubkey::Pubkey, + solana_program::system_instruction::create_account, +}; use { crate::{ c_oracle_header::PC_MAGIC, @@ -7,9 +15,7 @@ use { error::OracleError, utils::{ check_valid_fresh_account, - get_rent, pyth_assert, - try_convert, }, }, bytemuck::{ @@ -18,10 +24,7 @@ use { }, solana_program::{ account_info::AccountInfo, - program::invoke_signed, program_error::ProgramError, - pubkey::Pubkey, - system_instruction::create_account, }, std::{ cell::RefMut, @@ -39,6 +42,7 @@ mod mapping; mod permission; mod price; mod product; +mod publisher_caps; // Some types only exist during use as a library. #[cfg(feature = "strum")] @@ -64,6 +68,7 @@ pub use { update_product_metadata, ProductAccount, }, + publisher_caps::PublisherCapsAccount, }; // PDA seeds for accounts. @@ -76,7 +81,10 @@ pub const PERMISSIONS_SEED: &str = "permissions"; /// such that the caller can authenticate its origin. pub const UPD_PRICE_WRITE_SEED: &str = "upd_price_write"; +pub const CAPS_WRITE_SEED: &str = "caps_write"; + #[repr(C)] +#[cfg_attr(test, derive(Debug))] #[derive(Copy, Clone, Zeroable, Pod)] pub struct AccountHeader { pub magic_number: u32, @@ -131,6 +139,7 @@ pub trait PythAccount: Pod { /// Creates PDA accounts only when needed, and initializes it as one of the Pyth accounts. /// This PDA initialization assumes that the account has 0 lamports. /// TO DO: Fix this once we can resize the program. + #[cfg(any(feature = "test", test))] fn initialize_pda<'a>( account: &AccountInfo<'a>, funding_account: &AccountInfo<'a>, @@ -158,6 +167,7 @@ pub trait PythAccount: Pod { } } +#[cfg(any(feature = "test", test))] fn create<'a>( from: &AccountInfo<'a>, to: &AccountInfo<'a>, diff --git a/program/rust/src/accounts/publisher_caps.rs b/program/rust/src/accounts/publisher_caps.rs new file mode 100644 index 00000000..14b54b59 --- /dev/null +++ b/program/rust/src/accounts/publisher_caps.rs @@ -0,0 +1,368 @@ +use { + super::{ + AccountHeader, + PythAccount, + }, + crate::c_oracle_header::{ + PC_ACCTYPE_SCORE, + PC_MAX_PUBLISHERS, + PC_MAX_SYMBOLS, + PC_MAX_SYMBOLS_64, + }, + bytemuck::{ + Pod, + Zeroable, + }, + solana_program::{ + program_error::ProgramError, + pubkey::Pubkey, + }, + std::cmp::max, +}; + +#[repr(C)] +#[cfg_attr(test, derive(Debug))] +#[derive(Copy, Clone, Pod, Zeroable)] + +/// This account is part of Community Integrity Pool (CIP) project. +/// It is used to store the caps of the publishers which will be sent +/// to the `integrity_pool` program on mainnet to calculate the rewards. +pub struct PublisherCapsAccount { + pub header: AccountHeader, + pub num_publishers: usize, + pub num_symbols: usize, + // Z is a constant used to normalize the caps + pub z: u64, + // M is a constant showing the target stake per symbol + pub m: u64, + + // array[x][y] is a u64 whose bits represent if publisher x publishes symbols 64*y to 64*(y+1) - 1 + pub publisher_permissions: [[u64; PC_MAX_SYMBOLS_64 as usize]; PC_MAX_PUBLISHERS as usize], + pub caps: [u64; PC_MAX_PUBLISHERS as usize], + pub publishers: [Pubkey; PC_MAX_PUBLISHERS as usize], + pub prices: [Pubkey; PC_MAX_SYMBOLS as usize], +} + +impl PublisherCapsAccount { + fn get_publisher_permission(&self, x: usize, y: usize) -> bool { + (self.publisher_permissions[x][y / 64] >> (y % 64)) & 1 == 1 + } + + fn set_publisher_permission(&mut self, x: usize, y: usize, value: bool) { + if value { + self.publisher_permissions[x][y / 64] |= 1 << (y % 64); + } else { + self.publisher_permissions[x][y / 64] &= !(1 << (y % 64)); + } + } + + pub fn add_publisher( + &mut self, + publisher: Pubkey, + price_account: Pubkey, + ) -> Result<(), ProgramError> { + let publisher_index = self + .publishers + .iter() + .position(|&x| x == publisher) + .unwrap_or(self.num_publishers); + + if publisher_index == self.num_publishers { + if self.num_publishers == PC_MAX_PUBLISHERS as usize { + return Err(ProgramError::AccountDataTooSmall); + } + + self.publishers[self.num_publishers] = publisher; + self.num_publishers += 1; + } + + let symbol_index = self + .prices + .iter() + .position(|&x| x == price_account) + .ok_or(ProgramError::InvalidArgument)?; + + self.set_publisher_permission(publisher_index, symbol_index, true); + + self.calculate_caps() + } + + pub fn del_publisher( + &mut self, + publisher: Pubkey, + price_account: Pubkey, + ) -> Result<(), ProgramError> { + let publisher_index = self + .publishers + .iter() + .position(|&x| x == publisher) + .ok_or(ProgramError::InvalidArgument)?; + + let price_index = self + .prices + .iter() + .position(|&x| x == price_account) + .ok_or(ProgramError::InvalidArgument)?; + + self.set_publisher_permission(publisher_index, price_index, false); + self.calculate_caps() + } + + pub fn add_price(&mut self, symbol: Pubkey) -> Result<(), ProgramError> { + let price_index = self + .prices + .iter() + .position(|&x| x == symbol) + .unwrap_or(self.num_symbols); + + if price_index == self.num_symbols { + if self.num_symbols == PC_MAX_SYMBOLS as usize { + return Err(ProgramError::AccountDataTooSmall); + } + + self.prices[self.num_symbols] = symbol; + self.num_symbols += 1; + } + + Ok(()) + } + + pub fn del_price(&mut self, symbol: Pubkey) -> Result<(), ProgramError> { + let price_index = self + .prices + .iter() + .position(|&x| x == symbol) + .ok_or(ProgramError::InvalidArgument)?; + + // update symbol list + self.prices[price_index] = self.prices[self.num_symbols - 1]; + self.prices[self.num_symbols - 1] = Pubkey::default(); + + // update publisher permissions + for i in 0..self.num_publishers { + let value = self.get_publisher_permission(i, self.num_symbols - 1); + self.set_publisher_permission(i, price_index, value) + } + + self.num_symbols -= 1; + self.calculate_caps() + } + + + pub fn calculate_caps(&mut self) -> Result<(), ProgramError> { + let price_weights: Vec = self + .prices + .iter() + .enumerate() + .map(|(j, _)| { + let weight = self + .publisher_permissions + .iter() + .enumerate() + .filter(|(i, _)| self.get_publisher_permission(*i, j)) + .count() as u64; + max(weight, self.z) + }) + .collect(); + + for i in 0..self.num_publishers { + self.caps[i] = self + .prices + .iter() + .enumerate() + .filter(|(j, _)| self.get_publisher_permission(i, *j)) + .map(|(j, _)| self.m * 1_000_000_000_u64 / price_weights[j]) + .sum(); + } + Ok(()) + } + + pub fn get_caps_message(&self) -> Vec> { + self.publishers + .iter() + .enumerate() + .map(|(i, pubkey)| { + pubkey + .to_bytes() + .into_iter() + .chain(self.caps[i].to_le_bytes().into_iter()) + .collect() + }) + .collect() + } +} + +impl PythAccount for PublisherCapsAccount { + const ACCOUNT_TYPE: u32 = PC_ACCTYPE_SCORE; + // Calculate the initial size of the account + const INITIAL_SIZE: u32 = 75824; +} + + +#[cfg(test)] +mod tests { + use { + super::*, + crate::c_oracle_header::{ + PC_ACCTYPE_SCORE, + PC_MAGIC, + PC_VERSION, + }, + quickcheck::Arbitrary, + quickcheck_macros::quickcheck, + solana_program::pubkey::Pubkey, + std::mem::size_of, + }; + + fn arbitrary_pubkey(g: &mut quickcheck::Gen) -> Pubkey { + let mut key = [0u8; 32]; + key.iter_mut().for_each(|item| *item = u8::arbitrary(g)); + Pubkey::new_from_array(key) + } + + impl Arbitrary for PublisherCapsAccount { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut account = PublisherCapsAccount { + header: AccountHeader { + magic_number: PC_MAGIC, + account_type: PC_ACCTYPE_SCORE, + version: PC_VERSION, + size: size_of::() as u32, + }, + num_publishers: usize::arbitrary(g) % (PC_MAX_PUBLISHERS - 10) as usize + 10, + num_symbols: usize::arbitrary(g) % (PC_MAX_SYMBOLS - 10) as usize + 10, + z: u64::arbitrary(g) % 5 + 2, + m: u64::arbitrary(g) % 1000 + 1000, + publisher_permissions: [[u64::arbitrary(g); PC_MAX_SYMBOLS_64 as usize]; + PC_MAX_PUBLISHERS as usize], + caps: [0; PC_MAX_PUBLISHERS as usize], + publishers: [arbitrary_pubkey(g); PC_MAX_PUBLISHERS as usize], + prices: [arbitrary_pubkey(g); PC_MAX_SYMBOLS as usize], + }; + + account.calculate_caps().unwrap(); + account + } + } + + fn get_empty_account() -> PublisherCapsAccount { + PublisherCapsAccount { + header: AccountHeader { + magic_number: PC_MAGIC, + account_type: PC_ACCTYPE_SCORE, + version: PC_VERSION, + size: size_of::() as u32, + }, + num_publishers: 0, + num_symbols: 0, + z: 2, + m: 1000, + publisher_permissions: [[0; PC_MAX_SYMBOLS_64 as usize]; PC_MAX_PUBLISHERS as usize], + caps: [0; PC_MAX_PUBLISHERS as usize], + publishers: [Pubkey::default(); PC_MAX_PUBLISHERS as usize], + prices: [Pubkey::default(); PC_MAX_SYMBOLS as usize], + } + } + + #[test] + fn test_size_of_publisher_caps_account() { + let account = get_empty_account(); + assert_eq!( + PublisherCapsAccount::INITIAL_SIZE as usize, + size_of::() + ); + assert_eq!( + account.header.size as usize, + size_of::() + ); + } + + #[test] + fn test_publisher_caps_account() { + let mut account = get_empty_account(); + + let publisher1 = Pubkey::new_unique(); + let publisher2 = Pubkey::new_unique(); + let publisher3 = Pubkey::new_unique(); + let symbol1 = Pubkey::new_unique(); + let symbol2 = Pubkey::new_unique(); + + account.add_price(symbol1).unwrap(); + account.add_price(symbol2).unwrap(); + + account.add_publisher(publisher1, symbol1).unwrap(); + assert_eq!(account.caps[..2], [account.m * 500_000_000, 0]); + + account.add_publisher(publisher1, symbol2).unwrap(); + assert!(account.get_publisher_permission(0, 0)); + assert!(account.get_publisher_permission(0, 1)); + assert_eq!(account.caps[..2], [account.m * 1_000_000_000, 0]); + + + account.add_publisher(publisher2, symbol1).unwrap(); + assert_eq!( + account.caps[..2], + [account.m * 1_000_000_000, account.m * 500_000_000] + ); + + account.add_publisher(publisher2, symbol2).unwrap(); + assert!(account.get_publisher_permission(1, 0)); + assert!(account.get_publisher_permission(1, 1)); + assert_eq!( + account.caps[..2], + [account.m * 1_000_000_000, account.m * 1_000_000_000] + ); + + account.add_publisher(publisher3, symbol1).unwrap(); + assert_eq!( + account.caps[..3], + [833_333_333_333, 833_333_333_333, 333333333333] + ); + + account.del_publisher(publisher1, symbol1).unwrap(); + assert_eq!(account.num_publishers, 3); + assert_eq!(account.num_symbols, 2); + assert!(!account.get_publisher_permission(0, 0)); + assert!(account.get_publisher_permission(0, 1)); + assert!(account.get_publisher_permission(1, 0)); + assert!(account.get_publisher_permission(1, 1)); + + account.del_publisher(publisher1, symbol2).unwrap(); + assert_eq!(account.num_publishers, 3); + assert_eq!(account.num_symbols, 2); + assert!(!account.get_publisher_permission(0, 0)); + assert!(!account.get_publisher_permission(0, 1)); + assert!(account.get_publisher_permission(1, 0)); + assert!(account.get_publisher_permission(1, 1)); + + account.del_publisher(publisher2, symbol1).unwrap(); + assert_eq!(account.num_publishers, 3); + assert_eq!(account.num_symbols, 2); + assert!(!account.get_publisher_permission(0, 0)); + assert!(!account.get_publisher_permission(0, 1)); + assert!(!account.get_publisher_permission(1, 0)); + assert!(account.get_publisher_permission(1, 1)); + + account.del_publisher(publisher2, symbol2).unwrap(); + assert_eq!(account.num_publishers, 3); + assert_eq!(account.num_symbols, 2); + assert!(!account.get_publisher_permission(0, 0)); + assert!(!account.get_publisher_permission(0, 1)); + assert!(!account.get_publisher_permission(1, 0)); + assert!(!account.get_publisher_permission(1, 1)); + + assert_eq!(account.caps[..3], [0, 0, 500000000000]); + } + + #[allow(clippy::assertions_on_constants)] + #[quickcheck] + fn test_arbitrary_account(account: PublisherCapsAccount) { + assert_eq!(account.header.magic_number, PC_MAGIC); + assert_eq!(account.header.account_type, PC_ACCTYPE_SCORE); + assert_eq!(account.header.version, PC_VERSION); + assert_eq!( + account.header.size as usize, + size_of::() + ); + } +} diff --git a/program/rust/src/instruction.rs b/program/rust/src/instruction.rs index c3d8a3de..cdedd5b7 100644 --- a/program/rust/src/instruction.rs +++ b/program/rust/src/instruction.rs @@ -1,3 +1,8 @@ +#[cfg(any(feature = "test", test))] +use num_derive::{ + FromPrimitive, + ToPrimitive, +}; use { crate::{ c_oracle_header::PC_VERSION, @@ -8,17 +13,14 @@ use { Pod, Zeroable, }, - num_derive::{ - FromPrimitive, - ToPrimitive, - }, - num_traits::FromPrimitive, solana_program::pubkey::Pubkey, }; /// WARNING : NEW COMMANDS SHOULD BE ADDED AT THE END OF THE LIST -#[repr(i32)] -#[derive(PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[repr(u32)] +#[cfg_attr(any(feature = "test", test), derive(FromPrimitive, ToPrimitive))] +#[derive(PartialEq, Eq)] +#[allow(dead_code)] pub enum OracleCommand { /// Initialize first mapping list account // account[0] funding account [signer writable] @@ -105,11 +107,21 @@ pub enum OracleCommand { SetMaxLatency = 18, } +impl OracleCommand { + pub fn from_u32(value: u32) -> Option { + if value < 19 { + Some(unsafe { std::mem::transmute(value) }) + } else { + None + } + } +} + #[repr(C)] #[derive(Zeroable, Pod, Copy, Clone)] pub struct CommandHeader { pub version: u32, - pub command: i32, + pub command: u32, } pub fn load_command_header_checked(data: &[u8]) -> Result { @@ -118,7 +130,7 @@ pub fn load_command_header_checked(data: &[u8]) -> Result init_mapping(program_id, accounts, instruction_data), + InitMapping => { + #[cfg(any(feature = "test", test))] + { + init_mapping(program_id, accounts, instruction_data) + } + #[cfg(not(any(feature = "test", test)))] + { + Err(OracleError::UnrecognizedInstruction.into()) + } + } AddMapping => Err(OracleError::UnrecognizedInstruction.into()), AddProduct => add_product(program_id, accounts, instruction_data), UpdProduct => upd_product(program_id, accounts, instruction_data), @@ -77,7 +91,16 @@ pub fn process_instruction( } DelPrice => del_price(program_id, accounts, instruction_data), DelProduct => del_product(program_id, accounts, instruction_data), - UpdPermissions => upd_permissions(program_id, accounts, instruction_data), + UpdPermissions => { + #[cfg(any(feature = "test", test))] + { + upd_permissions(program_id, accounts, instruction_data) + } + #[cfg(not(any(feature = "test", test)))] + { + Err(OracleError::UnrecognizedInstruction.into()) + } + } SetMaxLatency => set_max_latency(program_id, accounts, instruction_data), } } diff --git a/program/rust/src/processor/add_price.rs b/program/rust/src/processor/add_price.rs index cd6da424..f1b25d04 100644 --- a/program/rust/src/processor/add_price.rs +++ b/program/rust/src/processor/add_price.rs @@ -3,6 +3,7 @@ use { accounts::{ PriceAccount, ProductAccount, + PublisherCapsAccount, PythAccount, }, c_oracle_header::{ @@ -34,6 +35,7 @@ use { // account[0] funding account [signer writable] // account[1] product account [signer writable] // account[2] new price account [signer writable] +// account[3] caps account [signer writable] pub fn add_price( program_id: &Pubkey, accounts: &[AccountInfo], @@ -48,10 +50,12 @@ pub fn add_price( )?; - let (funding_account, product_account, price_account, permissions_account) = match accounts { - [x, y, z, p] => Ok((x, y, z, p)), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; + let (funding_account, product_account, price_account, permissions_account, caps_account) = + match accounts { + [x, y, z, p] => Ok((x, y, z, p, None)), + [x, y, z, p, s] => Ok((x, y, z, p, Some(s))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; check_valid_funding_account(funding_account)?; check_permissioned_funding_account( @@ -80,5 +84,11 @@ pub fn add_price( price_data.min_pub_ = PRICE_ACCOUNT_DEFAULT_MIN_PUB; product_data.first_price_account = *price_account.key; + if let Some(caps_account) = caps_account { + let mut caps_account = + load_checked::(caps_account, cmd_args.header.version)?; + caps_account.add_price(*price_account.key)?; + } + Ok(()) } diff --git a/program/rust/src/processor/add_publisher.rs b/program/rust/src/processor/add_publisher.rs index 841de3b9..44ce3b33 100644 --- a/program/rust/src/processor/add_publisher.rs +++ b/program/rust/src/processor/add_publisher.rs @@ -3,7 +3,9 @@ use { accounts::{ PriceAccount, PriceComponent, + PublisherCapsAccount, PythAccount, + CAPS_WRITE_SEED, }, c_oracle_header::PC_NUM_COMP, deserialize::{ @@ -15,7 +17,9 @@ use { check_permissioned_funding_account, check_valid_funding_account, pyth_assert, + send_message_to_message_buffer, try_convert, + MessageBufferAccounts, }, OracleError, }, @@ -24,10 +28,7 @@ use { account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, - program_memory::{ - sol_memcmp, - sol_memset, - }, + program_memory::sol_memset, pubkey::Pubkey, }, std::mem::size_of, @@ -36,6 +37,7 @@ use { /// Add publisher to symbol account // account[0] funding account [signer writable] // account[1] price account [signer writable] +// account[2] caps account [signer writable] pub fn add_publisher( program_id: &Pubkey, accounts: &[AccountInfo], @@ -48,8 +50,26 @@ pub fn add_publisher( ProgramError::InvalidArgument, )?; - let (funding_account, price_account, permissions_account) = match accounts { - [x, y, p] => Ok((x, y, p)), + let ( + funding_account, + price_account, + permissions_account, + caps_account, + maybe_accumulator_accounts, + ) = match accounts { + [x, y, p] => Ok((x, y, p, None, None)), + [x, y, p, s, a, b, c, d] => Ok(( + x, + y, + p, + Some(s), + Some(MessageBufferAccounts { + program_id: a, + whitelist: b, + oracle_auth_pda: c, + message_buffer_data: d, + }), + )), _ => Err(OracleError::InvalidNumberOfAccounts), }?; @@ -65,13 +85,23 @@ pub fn add_publisher( let mut price_data = load_checked::(price_account, cmd_args.header.version)?; - // Use the call with the default pubkey (000..) as a trigger to sort the publishers as a - // migration step from unsorted list to sorted list. - if cmd_args.publisher == Pubkey::default() { - let num_comps = try_convert::(price_data.num_)?; - sort_price_comps(&mut price_data.comp_, num_comps)?; - return Ok(()); - } + // TODO: uncomment for migration + // Use the call with the default pubkey (000..) as a trigger to update caps account + // migration step for initializing the caps account + // if cmd_args.publisher == Pubkey::default() { + // let num_comps = try_convert::(price_data.num_)?; + + // if let Some(caps_account) = caps_account { + // let mut caps_account = + // load_checked::(caps_account, cmd_args.header.version)?; + // caps_account.add_price(*price_account.key)?; + // for publisher in price_data.comp_[..num_comps].iter() { + // caps_account.add_publisher(publisher.pub_, *price_account.key)?; + // } + // } + + // return Ok(()); + // } if price_data.num_ >= PC_NUM_COMP { return Err(ProgramError::InvalidArgument); @@ -93,139 +123,38 @@ pub fn add_publisher( price_data.num_ += 1; // Sort the publishers in the list + #[allow(clippy::manual_swap)] { - let num_comps = try_convert::(price_data.num_)?; - sort_price_comps(&mut price_data.comp_, num_comps)?; - } - - price_data.header.size = try_convert::<_, u32>(PriceAccount::INITIAL_SIZE)?; - Ok(()) -} - -/// A copy of rust slice/sort.rs heapsort implementation which is small and fast. We couldn't use -/// the sort directly because it was only accessible behind a unstable feature flag at the time of -/// writing this code. -fn heapsort(v: &mut [(Pubkey, usize)]) { - // This binary heap respects the invariant `parent >= child`. - let sift_down = |v: &mut [(Pubkey, usize)], mut node: usize| { - loop { - // Children of `node`. - let mut child = 2 * node + 1; - if child >= v.len() { - break; - } - - // Choose the greater child. - if child + 1 < v.len() - && sol_memcmp(v[child].0.as_ref(), v[child + 1].0.as_ref(), 32) < 0 - { - child += 1; - } - - // Stop if the invariant holds at `node`. - if sol_memcmp(v[node].0.as_ref(), v[child].0.as_ref(), 32) >= 0 { - break; - } - - // Swap `node` with the greater child, move one step down, and continue sifting. - v.swap(node, child); - node = child; + let mut comp_index = try_convert::(price_data.num_ - 1)?; + while comp_index > 0 + && price_data.comp_[comp_index - 1].pub_ > price_data.comp_[comp_index].pub_ + { + let tmp = price_data.comp_[comp_index - 1]; + price_data.comp_[comp_index - 1] = price_data.comp_[comp_index]; + price_data.comp_[comp_index] = tmp; + comp_index -= 1; } - }; - - // Build the heap in linear time. - for i in (0..v.len() / 2).rev() { - sift_down(v, i); } - // Pop maximal elements from the heap. - for i in (1..v.len()).rev() { - v.swap(0, i); - sift_down(&mut v[..i], 0); - } -} + price_data.header.size = try_convert::<_, u32>(PriceAccount::INITIAL_SIZE)?; -/// Sort the publishers price component list in place by performing minimal swaps. -/// This code is inspired by the sort_by_cached_key implementation in the Rust stdlib. -/// The rust stdlib implementation is not used because it uses a fast sort variant that has -/// a large code size. -/// -/// num_publishers is the number of publishers in the list that should be sorted. It is explicitly -/// passed to avoid callers mistake of passing the full slice which may contain uninitialized values. -fn sort_price_comps(comps: &mut [PriceComponent], num_comps: usize) -> Result<(), ProgramError> { - let comps = comps - .get_mut(..num_comps) - .ok_or(ProgramError::InvalidArgument)?; - - // Publishers are likely sorted in ascending order but - // heapsorts creates a max-heap so we reverse the order - // of the keys to make the heapify step faster. - let mut keys = comps - .iter() - .enumerate() - .map(|(i, x)| (x.pub_, i)) - .rev() - .collect::>(); - - heapsort(&mut keys); - - for i in 0..num_comps { - // We know that the publisher with key[i].0 should be at index i in the sorted array and - // want to swap them in-place in O(n). Normally, the publisher at key[i].0 should be at - // key[i].1 but if it is swapped, we need to find the correct index by following the chain - // of swaps. - let mut index = keys[i].1; - - while index < i { - index = keys[index].1; + if let Some(caps_account_info) = caps_account { + let mut caps_account = + load_checked::(caps_account_info, cmd_args.header.version)?; + caps_account.add_publisher(cmd_args.publisher, *price_account.key)?; + + // Feature-gated accumulator-specific code, used only on pythnet/pythtest + if let Some(accumulator_accounts) = maybe_accumulator_accounts { + send_message_to_message_buffer( + program_id, + caps_account_info.key, + CAPS_WRITE_SEED, + accounts, + accumulator_accounts, + caps_account.get_caps_message(), + )?; } - // Setting the final index here is important to make the code linear as we won't - // loop over from i to index again when we reach i again. - keys[i].1 = index; - comps.swap(i, index); } Ok(()) } - -#[cfg(test)] -mod test { - use { - super::*, - quickcheck_macros::quickcheck, - }; - - #[quickcheck] - pub fn test_sort_price_comps(mut comps: Vec) { - let mut rust_std_sorted_comps = comps.clone(); - rust_std_sorted_comps.sort_by_key(|x| x.pub_); - - let num_comps = comps.len(); - assert_eq!( - sort_price_comps(&mut comps, num_comps + 1), - Err(ProgramError::InvalidArgument) - ); - - assert_eq!(sort_price_comps(&mut comps, num_comps), Ok(())); - assert_eq!(comps, rust_std_sorted_comps); - } - - #[quickcheck] - pub fn test_sort_price_comps_smaller_slice( - mut comps: Vec, - mut num_comps: usize, - ) { - num_comps = if comps.is_empty() { - 0 - } else { - num_comps % comps.len() - }; - - let mut rust_std_sorted_comps = comps.get(..num_comps).unwrap().to_vec(); - rust_std_sorted_comps.sort_by_key(|x| x.pub_); - - - assert_eq!(sort_price_comps(&mut comps, num_comps), Ok(())); - assert_eq!(comps.get(..num_comps).unwrap(), rust_std_sorted_comps); - } -} diff --git a/program/rust/src/processor/del_price.rs b/program/rust/src/processor/del_price.rs index 92f3fe54..fb6ca808 100644 --- a/program/rust/src/processor/del_price.rs +++ b/program/rust/src/processor/del_price.rs @@ -3,6 +3,7 @@ use { accounts::{ PriceAccount, ProductAccount, + PublisherCapsAccount, }, deserialize::{ load, @@ -30,6 +31,7 @@ use { // account[0] funding account [signer writable] // account[1] product account [signer writable] // account[2] price account [signer writable] +// account[3] caps account [signer writable] /// Warning: This function is dangerous and will break any programs that depend on the deleted /// price account! pub fn del_price( @@ -37,10 +39,12 @@ pub fn del_price( accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { - let (funding_account, product_account, price_account, permissions_account) = match accounts { - [w, x, y, p] => Ok((w, x, y, p)), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; + let (funding_account, product_account, price_account, permissions_account, caps_account) = + match accounts { + [x, y, z, p] => Ok((x, y, z, p, None)), + [x, y, z, p, s] => Ok((x, y, z, p, Some(s))), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; let cmd_args = load::(instruction_data)?; @@ -83,5 +87,11 @@ pub fn del_price( **price_account.lamports.borrow_mut() = 0; **funding_account.lamports.borrow_mut() += lamports; + if let Some(caps_account) = caps_account { + let mut caps_account = + load_checked::(caps_account, cmd_args.version)?; + caps_account.del_price(*price_account.key)?; + } + Ok(()) } diff --git a/program/rust/src/processor/del_publisher.rs b/program/rust/src/processor/del_publisher.rs index cd9459f6..18bdb307 100644 --- a/program/rust/src/processor/del_publisher.rs +++ b/program/rust/src/processor/del_publisher.rs @@ -3,6 +3,7 @@ use { accounts::{ PriceAccount, PriceComponent, + PublisherCapsAccount, PythAccount, }, deserialize::{ @@ -32,6 +33,7 @@ use { /// Delete publisher from symbol account // account[0] funding account [signer writable] // account[1] price account [signer writable] +// account[2] caps account [signer writable] pub fn del_publisher( program_id: &Pubkey, accounts: &[AccountInfo], @@ -45,8 +47,9 @@ pub fn del_publisher( ProgramError::InvalidArgument, )?; - let (funding_account, price_account, permissions_account) = match accounts { - [x, y, p] => Ok((x, y, p)), + let (funding_account, price_account, permissions_account, caps_account) = match accounts { + [x, y, p] => Ok((x, y, p, None)), + [x, y, p, s] => Ok((x, y, p, Some(s))), _ => Err(OracleError::InvalidNumberOfAccounts), }?; @@ -59,6 +62,12 @@ pub fn del_publisher( &cmd_args.header, )?; + if let Some(caps_account) = caps_account { + let mut caps_account = + load_checked::(caps_account, cmd_args.header.version)?; + caps_account.del_publisher(cmd_args.publisher, *price_account.key)?; + } + let mut price_data = load_checked::(price_account, cmd_args.header.version)?; for i in 0..(try_convert::(price_data.num_)?) { diff --git a/program/rust/src/processor/upd_price.rs b/program/rust/src/processor/upd_price.rs index 278673b4..44ddd065 100644 --- a/program/rust/src/processor/upd_price.rs +++ b/program/rust/src/processor/upd_price.rs @@ -18,7 +18,9 @@ use { get_status_for_conf_price_ratio, is_component_update, pyth_assert, + send_message_to_message_buffer, try_convert, + MessageBufferAccounts, }, OracleError, }, @@ -26,11 +28,6 @@ use { account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, - instruction::{ - AccountMeta, - Instruction, - }, - program::invoke_signed, program_error::ProgramError, program_memory::sol_memcmp, pubkey::Pubkey, @@ -202,36 +199,6 @@ pub fn upd_price( { if let Some(accumulator_accounts) = maybe_accumulator_accounts { if price_data.message_sent_ == 0 { - // Check that the oracle PDA is correctly configured for the program we are calling. - let oracle_auth_seeds: &[&[u8]] = &[ - UPD_PRICE_WRITE_SEED.as_bytes(), - &accumulator_accounts.program_id.key.to_bytes(), - ]; - let (expected_oracle_auth_pda, bump) = - Pubkey::find_program_address(oracle_auth_seeds, program_id); - pyth_assert( - expected_oracle_auth_pda == *accumulator_accounts.oracle_auth_pda.key, - OracleError::InvalidPda.into(), - )?; - - let account_metas = vec![ - AccountMeta { - pubkey: *accumulator_accounts.whitelist.key, - is_signer: false, - is_writable: false, - }, - AccountMeta { - pubkey: *accumulator_accounts.oracle_auth_pda.key, - is_signer: true, - is_writable: false, - }, - AccountMeta { - pubkey: *accumulator_accounts.message_buffer_data.key, - is_signer: false, - is_writable: true, - }, - ]; - let message = vec![ price_data .as_price_feed_message(price_account.key) @@ -239,23 +206,15 @@ pub fn upd_price( price_data.as_twap_message(price_account.key).to_bytes(), ]; - // Append a TWAP message if available - - // anchor discriminator for "global:put_all" - let discriminator: [u8; 8] = [212, 225, 193, 91, 151, 238, 20, 93]; - let create_inputs_ix = Instruction::new_with_borsh( - *accumulator_accounts.program_id.key, - &(discriminator, price_account.key.to_bytes(), message), - account_metas, - ); - - let auth_seeds_with_bump: &[&[u8]] = &[ - UPD_PRICE_WRITE_SEED.as_bytes(), - &accumulator_accounts.program_id.key.to_bytes(), - &[bump], - ]; + send_message_to_message_buffer( + program_id, + price_account.key, + UPD_PRICE_WRITE_SEED, + accounts, + accumulator_accounts, + message, + )?; - invoke_signed(&create_inputs_ix, accounts, &[auth_seeds_with_bump])?; price_data.message_sent_ = 1; } } @@ -319,33 +278,7 @@ fn find_publisher_index(comps: &[PriceComponent], key: &Pubkey) -> Option } } } - - match binary_search_result { - Some(index) => Some(index), - None => { - let mut index = 0; - while index < comps.len() { - if sol_memcmp(comps[index].pub_.as_ref(), key.as_ref(), 32) == 0 { - break; - } - index += 1; - } - if index == comps.len() { - None - } else { - Some(index) - } - } - } -} - -#[allow(dead_code)] -// Wrapper struct for the accounts required to add data to the accumulator program. -struct MessageBufferAccounts<'a, 'b: 'a> { - program_id: &'a AccountInfo<'b>, - whitelist: &'a AccountInfo<'b>, - oracle_auth_pda: &'a AccountInfo<'b>, - message_buffer_data: &'a AccountInfo<'b>, + binary_search_result } #[cfg(test)] @@ -357,21 +290,6 @@ mod test { solana_program::pubkey::Pubkey, }; - /// Test the find_publisher_index method works with an unordered list of components. - #[quickcheck] - pub fn test_find_publisher_index_unordered_comp(comps: Vec) { - comps.iter().enumerate().for_each(|(idx, comp)| { - assert_eq!(find_publisher_index(&comps, &comp.pub_), Some(idx)); - }); - - let mut key_not_in_list = Pubkey::new_unique(); - while comps.iter().any(|comp| comp.pub_ == key_not_in_list) { - key_not_in_list = Pubkey::new_unique(); - } - - assert_eq!(find_publisher_index(&comps, &key_not_in_list), None); - } - /// Test the find_publisher_index method works with a sorted list of components. #[quickcheck] pub fn test_find_publisher_index_ordered_comp(mut comps: Vec) { diff --git a/program/rust/src/tests/test_add_price.rs b/program/rust/src/tests/test_add_price.rs index b282f1a5..15d7cb82 100644 --- a/program/rust/src/tests/test_add_price.rs +++ b/program/rust/src/tests/test_add_price.rs @@ -6,6 +6,7 @@ use { PermissionAccount, PriceAccount, ProductAccount, + PublisherCapsAccount, PythAccount, }, c_oracle_header::{ @@ -59,9 +60,16 @@ fn test_add_price() { let mut price_setup_2 = AccountSetup::new::(&program_id); let price_account_2 = price_setup_2.as_account_info(); + let mut price_setup_3 = AccountSetup::new::(&program_id); + let price_account_3 = price_setup_3.as_account_info(); + let mut permissions_setup = AccountSetup::new_permission(&program_id); let permissions_account = permissions_setup.as_account_info(); + let mut caps_setup = AccountSetup::new::(&program_id); + let caps_account = caps_setup.as_account_info(); + PublisherCapsAccount::initialize(&caps_account, PC_VERSION).unwrap(); + { let mut permissions_account_data = PermissionAccount::initialize(&permissions_account, PC_VERSION).unwrap(); @@ -82,6 +90,26 @@ fn test_add_price() { ) .is_ok()); + // add price with caps account + assert!(process_instruction( + &program_id, + &[ + funding_account.clone(), + product_account.clone(), + price_account_3.clone(), + permissions_account.clone(), + caps_account.clone(), + ], + instruction_data_add_price + ) + .is_ok()); + + { + let score_data = load_checked::(&caps_account, PC_VERSION).unwrap(); + assert_eq!(score_data.prices[0], *price_account_3.key); + assert_eq!(score_data.num_symbols, 1); + } + assert!(process_instruction( &program_id, &[ @@ -101,7 +129,7 @@ fn test_add_price() { assert_eq!(price_data.price_type, 1); assert_eq!(price_data.min_pub_, PRICE_ACCOUNT_DEFAULT_MIN_PUB); assert!(price_data.product_account == *product_account.key); - assert!(price_data.next_price_account == Pubkey::default()); + assert!(price_data.next_price_account == *price_account_3.key); assert!(product_data.first_price_account == *price_account.key); } @@ -138,6 +166,7 @@ fn test_add_price() { price_account.clone(), permissions_account.clone(), permissions_account.clone(), + permissions_account.clone(), ], instruction_data_add_price ), diff --git a/program/rust/src/tests/test_add_publisher.rs b/program/rust/src/tests/test_add_publisher.rs index 41dcfe67..57f2a346 100644 --- a/program/rust/src/tests/test_add_publisher.rs +++ b/program/rust/src/tests/test_add_publisher.rs @@ -157,37 +157,4 @@ fn test_add_publisher() { assert!(price_data.comp_[i as usize].pub_ > price_data.comp_[(i - 1) as usize].pub_); } } - - // Test sorting by reordering the publishers to be in reverse order - // and then adding the default (000...) publisher to trigger the sorting. - { - let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - price_data - .comp_ - .get_mut(..PC_NUM_COMP as usize) - .unwrap() - .reverse(); - } - - cmd.publisher = Pubkey::default(); - instruction_data = bytes_of::(&cmd); - assert!(process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - permissions_account.clone(), - ], - instruction_data - ) - .is_ok()); - - // Make sure that publishers get sorted after adding the default publisher - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert!(price_data.num_ == PC_NUM_COMP); - for i in 1..PC_NUM_COMP { - assert!(price_data.comp_[i as usize].pub_ > price_data.comp_[(i - 1) as usize].pub_); - } - } } diff --git a/program/rust/src/tests/test_check_valid_signable_account_or_permissioned_funding_account.rs b/program/rust/src/tests/test_check_valid_signable_account_or_permissioned_funding_account.rs index 8a56f110..41600dd3 100644 --- a/program/rust/src/tests/test_check_valid_signable_account_or_permissioned_funding_account.rs +++ b/program/rust/src/tests/test_check_valid_signable_account_or_permissioned_funding_account.rs @@ -60,7 +60,7 @@ pub fn test_permissions_account_mandatory() { &permissions_account, &CommandHeader { version: PC_VERSION, - command: OracleCommand::UpdProduct as i32, + command: OracleCommand::UpdProduct as u32, }, ), Ok(()), diff --git a/program/rust/src/tests/test_upd_price_v2.rs b/program/rust/src/tests/test_upd_price_v2.rs index 63785819..0e0c1f4b 100644 --- a/program/rust/src/tests/test_upd_price_v2.rs +++ b/program/rust/src/tests/test_upd_price_v2.rs @@ -444,87 +444,6 @@ fn test_upd_price_v2() -> Result<(), Box> { Ok(()) } -#[test] -fn test_upd_works_with_unordered_publisher_set() -> Result<(), Box> { - let mut instruction_data = [0u8; size_of::()]; - - let program_id = Pubkey::new_unique(); - - 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 publishers_setup: Vec<_> = (0..20).map(|_| AccountSetup::new_funding()).collect(); - let mut publishers: Vec<_> = publishers_setup - .iter_mut() - .map(|s| s.as_account_info()) - .collect(); - - publishers.sort_by_key(|x| x.key); - - { - let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - price_data.num_ = 20; - // Store the publishers in reverse order - publishers - .iter() - .rev() - .enumerate() - .for_each(|(i, account)| { - price_data.comp_[i].pub_ = *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); - - for (i, publisher) in publishers.iter().enumerate() { - populate_instruction(&mut instruction_data, (i + 100) as i64, 10, 1); - process_instruction( - &program_id, - &[ - publisher.clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - } - - update_clock_slot(&mut clock_account, 2); - - // Trigger the aggregate calculation by sending another price - // update - populate_instruction(&mut instruction_data, 100, 10, 2); - process_instruction( - &program_id, - &[ - publishers[0].clone(), - price_account.clone(), - clock_account.clone(), - ], - &instruction_data, - )?; - - - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - - // The result will be the following only if all the - // publishers prices are included in the aggregate. - assert_eq!(price_data.valid_slot_, 1); - assert_eq!(price_data.agg_.pub_slot_, 2); - assert_eq!(price_data.agg_.price_, 109); - assert_eq!(price_data.agg_.conf_, 8); - assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING); - - 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(); diff --git a/program/rust/src/tests/test_utils.rs b/program/rust/src/tests/test_utils.rs index 853fdcd6..6fcdd13f 100644 --- a/program/rust/src/tests/test_utils.rs +++ b/program/rust/src/tests/test_utils.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +use num_traits::ToPrimitive; use { crate::{ accounts::{ @@ -12,7 +14,6 @@ use { OracleCommand, }, }, - num_traits::ToPrimitive, solana_program::{ account_info::AccountInfo, clock::{ @@ -34,7 +35,8 @@ use { solana_sdk::transaction::TransactionError, }; -const UPPER_BOUND_OF_ALL_ACCOUNT_SIZES: usize = 20536; + +const UPPER_BOUND_OF_ALL_ACCOUNT_SIZES: usize = 75824; /// The goal of this struct is to easily instantiate fresh solana accounts /// for the Pyth program to use in tests. @@ -134,11 +136,12 @@ pub fn update_clock_slot(clock_account: &mut AccountInfo, slot: u64) { clock_data.to_account_info(clock_account); } +#[cfg(test)] impl From for CommandHeader { fn from(val: OracleCommand) -> Self { CommandHeader { version: PC_VERSION, - command: val.to_i32().unwrap(), // This can never fail and is only used in tests + command: val.to_u32().unwrap(), // This can never fail and is only used in tests } } } diff --git a/program/rust/src/utils.rs b/program/rust/src/utils.rs index 97ef9b53..2ed0a8b4 100644 --- a/program/rust/src/utils.rs +++ b/program/rust/src/utils.rs @@ -25,15 +25,26 @@ use { Pod, Zeroable, }, - num_traits::FromPrimitive, solana_program::{ account_info::AccountInfo, - bpf_loader_upgradeable, - program::invoke, + instruction::{ + AccountMeta, + Instruction, + }, + program::{ + invoke, + invoke_signed, + }, program_error::ProgramError, pubkey::Pubkey, system_instruction::transfer, - sysvar::rent::Rent, + }, +}; +#[cfg(any(feature = "test", test))] +use { + solana_program::{ + bpf_loader_upgradeable, + rent::Rent, }, std::cell::Ref, }; @@ -73,7 +84,7 @@ pub fn check_permissioned_funding_account( pyth_assert( permissions_account_data.is_authorized( funding_account.key, - OracleCommand::from_i32(cmd_hdr.command).ok_or(OracleError::UnrecognizedInstruction)?, + OracleCommand::from_u32(cmd_hdr.command).ok_or(OracleError::UnrecognizedInstruction)?, ), OracleError::PermissionViolation.into(), )?; @@ -161,7 +172,7 @@ pub fn check_valid_permissions_account( /// Checks whether this instruction is trying to update an individual publisher's price (`true`) or /// is only trying to refresh the aggregate (`false`) pub fn is_component_update(cmd_args: &UpdPriceArgs) -> Result { - match OracleCommand::from_i32(cmd_args.header.command) + match OracleCommand::from_u32(cmd_args.header.command) .ok_or(OracleError::UnrecognizedInstruction)? { OracleCommand::UpdPrice | OracleCommand::UpdPriceNoFailOnError => Ok(true), @@ -204,6 +215,7 @@ struct ProgramdataAccount { /// Check that `programdata_account` is actually the buffer for `program_id`. /// Check that the authority in `programdata_account` matches `upgrade_authority_account`. +#[cfg(any(feature = "test", test))] pub fn check_is_upgrade_authority_for_program( upgrade_authority_account: &AccountInfo, programdata_account: &AccountInfo, @@ -234,13 +246,13 @@ pub fn check_is_upgrade_authority_for_program( Ok(()) } -#[cfg(not(test))] -pub fn get_rent() -> Result { - use solana_program::sysvar::Sysvar; - Rent::get() -} +// #[cfg(not(test))] +// pub fn get_rent() -> Result { +// use solana_program::sysvar::Sysvar; +// Rent::get() +// } -#[cfg(test)] +#[cfg(any(feature = "test", test))] pub fn get_rent() -> Result { Ok(Rent::default()) } @@ -259,3 +271,70 @@ pub fn send_lamports<'a>( )?; Ok(()) } + +// Wrapper struct for the accounts required to add data to the accumulator program. +pub struct MessageBufferAccounts<'a, 'b: 'a> { + pub program_id: &'a AccountInfo<'b>, + pub whitelist: &'a AccountInfo<'b>, + pub oracle_auth_pda: &'a AccountInfo<'b>, + pub message_buffer_data: &'a AccountInfo<'b>, +} + +pub fn send_message_to_message_buffer( + program_id: &Pubkey, + base_account_key: &Pubkey, + seed: &str, + accounts: &[AccountInfo], + accumulator_accounts: MessageBufferAccounts, + message: Vec>, +) -> Result<(), ProgramError> { + let account_metas = vec![ + AccountMeta { + pubkey: *accumulator_accounts.whitelist.key, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: *accumulator_accounts.oracle_auth_pda.key, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: *accumulator_accounts.message_buffer_data.key, + is_signer: false, + is_writable: true, + }, + ]; + + // Check that the oracle PDA is correctly configured for the program we are calling. + let oracle_auth_seeds: &[&[u8]] = &[ + seed.as_bytes(), + &accumulator_accounts.program_id.key.to_bytes(), + ]; + let (expected_oracle_auth_pda, bump) = + Pubkey::find_program_address(oracle_auth_seeds, program_id); + pyth_assert( + expected_oracle_auth_pda == *accumulator_accounts.oracle_auth_pda.key, + OracleError::InvalidPda.into(), + )?; + + // Append a TWAP message if available + + // anchor discriminator for "global:put_all" + let discriminator: [u8; 8] = [212, 225, 193, 91, 151, 238, 20, 93]; + let create_inputs_ix = Instruction::new_with_borsh( + *accumulator_accounts.program_id.key, + &(discriminator, base_account_key.to_bytes(), message), + account_metas, + ); + + let auth_seeds_with_bump: &[&[u8]] = &[ + seed.as_bytes(), + &accumulator_accounts.program_id.key.to_bytes(), + &[bump], + ]; + + invoke_signed(&create_inputs_ix, accounts, &[auth_seeds_with_bump])?; + + Ok(()) +} diff --git a/scripts/build-bpf.sh b/scripts/build-bpf.sh index fd85e4fa..64b95458 100755 --- a/scripts/build-bpf.sh +++ b/scripts/build-bpf.sh @@ -20,7 +20,7 @@ set -x cd "${PYTH_DIR}" # Re-run tests affected by features -cargo-test-bpf +cargo-test-bpf --features test cargo-build-bpf -- --locked -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort sha256sum ./target/**/*.so diff --git a/scripts/check-size.sh b/scripts/check-size.sh index bf3fe541..3d6264ec 100755 --- a/scripts/check-size.sh +++ b/scripts/check-size.sh @@ -4,6 +4,8 @@ # While Solana doesn't support resizing programs, the oracle binary needs to be smaller than 81760 bytes # (The available space for the oracle program on pythnet is 88429 and mainnet is 81760) ORACLE_SIZE=$(wc -c ./target/deploy/pyth_oracle.so | awk '{print $1}') +echo "Size of pyth_oracle.so is ${ORACLE_SIZE} bytes" + if [ $ORACLE_SIZE -lt ${1} ] then echo "Size of pyth_oracle.so is small enough to be deployed, since ${ORACLE_SIZE} is less than ${1}"