From 632208e35c0f54fc559975b2b72f93246b53b036 Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Mon, 9 Sep 2024 17:31:58 +0100 Subject: [PATCH 1/5] feat: resize mapping account --- program/c/src/oracle/oracle.h | 4 +- program/rust/src/error.rs | 2 + program/rust/src/instruction.rs | 3 + program/rust/src/processor.rs | 3 + program/rust/src/processor/resize_mapping.rs | 71 +++++++++++++++++++ program/rust/src/tests/mod.rs | 1 + program/rust/src/tests/pyth_simulator.rs | 25 +++++++ program/rust/src/tests/test_del_product.rs | 44 ++++++------ program/rust/src/tests/test_resize_mapping.rs | 54 ++++++++++++++ program/rust/src/tests/test_sizes.rs | 2 +- program/rust/src/tests/test_utils.rs | 12 ++-- 11 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 program/rust/src/processor/resize_mapping.rs create mode 100644 program/rust/src/tests/test_resize_mapping.rs diff --git a/program/c/src/oracle/oracle.h b/program/c/src/oracle/oracle.h index 05c623389..8fb92c59f 100644 --- a/program/c/src/oracle/oracle.h +++ b/program/c/src/oracle/oracle.h @@ -20,7 +20,7 @@ extern "C" { // various size constants #define PC_PUBKEY_SIZE 32 #define PC_PUBKEY_SIZE_64 (PC_PUBKEY_SIZE/sizeof(uint64_t)) -#define PC_MAP_TABLE_SIZE 640 +#define PC_MAP_TABLE_SIZE 5000 // Total price component slots available #define PC_NUM_COMP_PYTHNET 128 @@ -117,7 +117,7 @@ typedef struct pc_map_table pc_pub_key_t prod_[PC_MAP_TABLE_SIZE]; // product accounts } pc_map_table_t; -static_assert( sizeof( pc_map_table_t ) == 20536, "" ); +static_assert( sizeof( pc_map_table_t ) == 160056, "" ); // variable length string typedef struct pc_str diff --git a/program/rust/src/error.rs b/program/rust/src/error.rs index b4eb642b1..73b52deec 100644 --- a/program/rust/src/error.rs +++ b/program/rust/src/error.rs @@ -56,6 +56,8 @@ pub enum OracleError { MaxLastFeedIndexReached = 621, #[error("FeedIndexAlreadyInitialized")] FeedIndexAlreadyInitialized = 622, + #[error("NoNeedToResize")] + NoNeedToResize = 623, } impl From for ProgramError { diff --git a/program/rust/src/instruction.rs b/program/rust/src/instruction.rs index c61cbda93..43a32b9ca 100644 --- a/program/rust/src/instruction.rs +++ b/program/rust/src/instruction.rs @@ -109,6 +109,9 @@ pub enum OracleCommand { // account[1] price account [writable] // account[2] permissions account [writable] InitPriceFeedIndex = 19, + // account[0] funding account [signer writable] + // account[1] mapping account [writable] + ResizeMapping = 20, } #[repr(C)] diff --git a/program/rust/src/processor.rs b/program/rust/src/processor.rs index 8246e0e8f..3188ae87d 100644 --- a/program/rust/src/processor.rs +++ b/program/rust/src/processor.rs @@ -33,6 +33,7 @@ mod del_publisher; mod init_mapping; mod init_price; mod init_price_feed_index; +mod resize_mapping; mod set_max_latency; mod set_min_pub; mod upd_permissions; @@ -53,6 +54,7 @@ pub use { del_publisher::del_publisher, init_mapping::init_mapping, init_price::init_price, + resize_mapping::resize_mapping, set_max_latency::set_max_latency, set_min_pub::set_min_pub, upd_permissions::upd_permissions, @@ -106,6 +108,7 @@ pub fn process_instruction( UpdPermissions => upd_permissions(program_id, accounts, instruction_data), SetMaxLatency => set_max_latency(program_id, accounts, instruction_data), InitPriceFeedIndex => init_price_feed_index(program_id, accounts, instruction_data), + ResizeMapping => resize_mapping(program_id, accounts, instruction_data), } } diff --git a/program/rust/src/processor/resize_mapping.rs b/program/rust/src/processor/resize_mapping.rs new file mode 100644 index 000000000..914d1b0ca --- /dev/null +++ b/program/rust/src/processor/resize_mapping.rs @@ -0,0 +1,71 @@ +use { + crate::{ + accounts::{ + AccountHeader, + MappingAccount, + PythAccount, + }, + c_oracle_header::PC_MAGIC, + deserialize::{ + load, + load_account_as, + }, + instruction::CommandHeader, + utils::{ + check_valid_writable_account, + pyth_assert, + }, + OracleError, + }, + solana_program::{ + account_info::AccountInfo, + entrypoint::{ + ProgramResult, + MAX_PERMITTED_DATA_INCREASE, + }, + pubkey::Pubkey, + }, + std::{ + cmp::min, + mem::size_of, + }, +}; + +/// Resize mapping account. +// account[1] mapping account [writable] +pub fn resize_mapping( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let mapping_account = match accounts { + [x] => Ok(x), + _ => Err(OracleError::InvalidNumberOfAccounts), + }?; + + let hdr = load::(instruction_data)?; + + check_valid_writable_account(program_id, mapping_account)?; + + { + let account_header = load_account_as::(mapping_account)?; + pyth_assert( + account_header.magic_number == PC_MAGIC + && account_header.version == hdr.version + && account_header.account_type == MappingAccount::ACCOUNT_TYPE, + OracleError::InvalidAccountHeader.into(), + )?; + } + + pyth_assert( + mapping_account.data_len() < size_of::(), + OracleError::NoNeedToResize.into(), + )?; + let new_size = min( + size_of::(), + mapping_account.data_len() + MAX_PERMITTED_DATA_INCREASE, + ); + mapping_account.realloc(new_size, true)?; + + Ok(()) +} diff --git a/program/rust/src/tests/mod.rs b/program/rust/src/tests/mod.rs index 33178ccde..789d78430 100644 --- a/program/rust/src/tests/mod.rs +++ b/program/rust/src/tests/mod.rs @@ -17,6 +17,7 @@ mod test_message; mod test_permission_migration; mod test_publish; mod test_publish_batch; +mod test_resize_mapping; mod test_set_max_latency; mod test_set_min_pub; mod test_sizes; diff --git a/program/rust/src/tests/pyth_simulator.rs b/program/rust/src/tests/pyth_simulator.rs index 876405039..b8f59f801 100644 --- a/program/rust/src/tests/pyth_simulator.rs +++ b/program/rust/src/tests/pyth_simulator.rs @@ -478,6 +478,31 @@ impl PythSimulator { .map(|_| permissions_pubkey) } + pub async fn truncate_account(&mut self, key: Pubkey, size: usize) { + let mut account = self.get_account(key).await.unwrap(); + account.data.truncate(size); + self.context.set_account(&key, &account.into()); + } + + pub async fn resize_mapping( + &mut self, + mapping_keypair: &Keypair, + ) -> Result<(), BanksClientError> { + let cmd: CommandHeader = OracleCommand::ResizeMapping.into(); + let instruction = Instruction::new_with_bytes( + self.program_id, + bytes_of(&cmd), + vec![AccountMeta::new(mapping_keypair.pubkey(), true)], + ); + + self.process_ixs( + &[instruction], + &vec![mapping_keypair], + ©_keypair(&self.genesis_keypair), + ) + .await + } + /// Get the account at `key`. Returns `None` if no such account exists. pub async fn get_account(&mut self, key: Pubkey) -> Option { self.context.banks_client.get_account(key).await.unwrap() diff --git a/program/rust/src/tests/test_del_product.rs b/program/rust/src/tests/test_del_product.rs index 02e315ef8..d33d1dc6a 100644 --- a/program/rust/src/tests/test_del_product.rs +++ b/program/rust/src/tests/test_del_product.rs @@ -1,6 +1,7 @@ use { crate::{ accounts::MappingAccount, + deserialize::load, tests::pyth_simulator::PythSimulator, }, solana_program::pubkey::Pubkey, @@ -41,32 +42,31 @@ async fn test_del_product() { assert!(sim.get_account(product2.pubkey()).await.is_none()); - let mapping_data = sim - .get_account_data_as::(mapping_keypair.pubkey()) - .await - .unwrap(); - assert!(mapping_product_list_equals( - &mapping_data, - vec![ - product1.pubkey(), - product5.pubkey(), - product3.pubkey(), - product4.pubkey() - ] - )); + { + let mapping_account = sim.get_account(mapping_keypair.pubkey()).await.unwrap(); + let mapping_data = load::(&mapping_account.data).unwrap(); + assert!(mapping_product_list_equals( + mapping_data, + vec![ + product1.pubkey(), + product5.pubkey(), + product3.pubkey(), + product4.pubkey() + ] + )); + } assert!(sim.get_account(product5.pubkey()).await.is_some()); assert!(sim.del_product(&mapping_keypair, &product4).await.is_ok()); - let mapping_data = sim - .get_account_data_as::(mapping_keypair.pubkey()) - .await - .unwrap(); - - assert!(mapping_product_list_equals( - &mapping_data, - vec![product1.pubkey(), product5.pubkey(), product3.pubkey()] - )); + { + let mapping_account = sim.get_account(mapping_keypair.pubkey()).await.unwrap(); + let mapping_data = load::(&mapping_account.data).unwrap(); + assert!(mapping_product_list_equals( + mapping_data, + vec![product1.pubkey(), product5.pubkey(), product3.pubkey()] + )); + } } /// Returns true if the list of products in `mapping_data` contains the keys in `expected` (in the diff --git a/program/rust/src/tests/test_resize_mapping.rs b/program/rust/src/tests/test_resize_mapping.rs new file mode 100644 index 000000000..99672e41f --- /dev/null +++ b/program/rust/src/tests/test_resize_mapping.rs @@ -0,0 +1,54 @@ +use { + super::pyth_simulator::PythSimulator, + crate::{ + accounts::MappingAccount, + error::OracleError, + }, + solana_sdk::{ + instruction::InstructionError, + signer::Signer, + transaction::TransactionError, + }, + std::mem::size_of, +}; + + +#[tokio::test] +async fn test_resize_mapping() { + let mut sim = PythSimulator::new().await; + let mapping_keypair = sim.init_mapping().await.unwrap(); + assert_eq!( + sim.resize_mapping(&mapping_keypair) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(OracleError::NoNeedToResize as u32) + ) + ); + + sim.truncate_account(mapping_keypair.pubkey(), 20536).await; // Old size. + for i in 0..14 { + println!("resize #{i}"); + sim.resize_mapping(&mapping_keypair).await.unwrap(); + } + assert_eq!( + sim.resize_mapping(&mapping_keypair) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(OracleError::NoNeedToResize as u32) + ) + ); + assert_eq!( + sim.get_account(mapping_keypair.pubkey()) + .await + .unwrap() + .data + .len(), + size_of::() + ); +} diff --git a/program/rust/src/tests/test_sizes.rs b/program/rust/src/tests/test_sizes.rs index 7a1d900db..dbd725e29 100644 --- a/program/rust/src/tests/test_sizes.rs +++ b/program/rust/src/tests/test_sizes.rs @@ -86,7 +86,7 @@ fn test_sizes() { assert_eq!(size_of::(), 40); assert_eq!(size_of::(), 32); assert_eq!(size_of::(), 16); - assert_eq!(size_of::(), 20536); + assert_eq!(size_of::(), 160056); assert_eq!(size_of::(), 48); assert_eq!(size_of::(), 96); assert_eq!(size_of::(), 24); diff --git a/program/rust/src/tests/test_utils.rs b/program/rust/src/tests/test_utils.rs index 28d8f44ce..e39c84013 100644 --- a/program/rust/src/tests/test_utils.rs +++ b/program/rust/src/tests/test_utils.rs @@ -34,7 +34,7 @@ use { solana_sdk::transaction::TransactionError, }; -const UPPER_BOUND_OF_ALL_ACCOUNT_SIZES: usize = 20536; +const UPPER_BOUND_OF_ALL_ACCOUNT_SIZES: usize = 160056; /// The goal of this struct is to easily instantiate fresh solana accounts /// for the Pyth program to use in tests. @@ -50,7 +50,7 @@ pub struct AccountSetup { owner: Pubkey, balance: u64, size: usize, - data: [u8; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES], + data: Vec, } impl AccountSetup { @@ -59,7 +59,7 @@ impl AccountSetup { let owner = *owner; let balance = Rent::minimum_balance(&Rent::default(), T::MINIMUM_SIZE); let size = T::MINIMUM_SIZE; - let data = [0; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; + let data = vec![0; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; AccountSetup { key, owner, @@ -74,7 +74,7 @@ impl AccountSetup { let owner = system_program::id(); let balance = LAMPORTS_PER_SOL; let size = 0; - let data = [0; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; + let data = vec![0; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; AccountSetup { key, owner, @@ -89,7 +89,7 @@ impl AccountSetup { let owner = *owner; let balance = Rent::minimum_balance(&Rent::default(), PermissionAccount::NEW_ACCOUNT_SPACE); let size = PermissionAccount::NEW_ACCOUNT_SPACE; - let data = [0; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; + let data = vec![0; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; AccountSetup { key, owner, @@ -104,7 +104,7 @@ impl AccountSetup { let owner = sysvar::id(); let balance = Rent::minimum_balance(&Rent::default(), clock::Clock::size_of()); let size = clock::Clock::size_of(); - let data = [0u8; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; + let data = vec![0u8; UPPER_BOUND_OF_ALL_ACCOUNT_SIZES]; AccountSetup { key, owner, From b538d5b3efef69978734a7112f169ae9e82e1fa7 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Mon, 6 Jan 2025 13:35:05 -0800 Subject: [PATCH 2/5] fix: remove initPriceFeedIndex instr to reduce binary size --- program/rust/src/processor.rs | 18 ++--- .../src/processor/init_price_feed_index.rs | 66 ------------------- program/rust/src/tests/test_add_price.rs | 52 --------------- 3 files changed, 9 insertions(+), 127 deletions(-) delete mode 100644 program/rust/src/processor/init_price_feed_index.rs diff --git a/program/rust/src/processor.rs b/program/rust/src/processor.rs index 3188ae87d..b0a20c565 100644 --- a/program/rust/src/processor.rs +++ b/program/rust/src/processor.rs @@ -32,7 +32,6 @@ mod del_product; mod del_publisher; mod init_mapping; mod init_price; -mod init_price_feed_index; mod resize_mapping; mod set_max_latency; mod set_min_pub; @@ -67,15 +66,13 @@ pub use { }, upd_product::upd_product, }; -use { - init_price_feed_index::init_price_feed_index, - solana_program::{ - program_error::ProgramError, - rent::Rent, - sysvar::Sysvar, - }, +use solana_program::{ + program_error::ProgramError, + rent::Rent, + sysvar::Sysvar, }; + /// Dispatch to the right instruction in the oracle. pub fn process_instruction( program_id: &Pubkey, @@ -107,7 +104,10 @@ pub fn process_instruction( DelProduct => del_product(program_id, accounts, instruction_data), UpdPermissions => upd_permissions(program_id, accounts, instruction_data), SetMaxLatency => set_max_latency(program_id, accounts, instruction_data), - InitPriceFeedIndex => init_price_feed_index(program_id, accounts, instruction_data), + InitPriceFeedIndex => { + solana_program::msg!("Oracle init price feed index instruction has been removed. Bailing out!"); + Err(OracleError::UnrecognizedInstruction.into()) + } ResizeMapping => resize_mapping(program_id, accounts, instruction_data), } } diff --git a/program/rust/src/processor/init_price_feed_index.rs b/program/rust/src/processor/init_price_feed_index.rs deleted file mode 100644 index 72efc81ff..000000000 --- a/program/rust/src/processor/init_price_feed_index.rs +++ /dev/null @@ -1,66 +0,0 @@ -use { - super::reserve_new_price_feed_index, - crate::{ - accounts::PriceAccount, - deserialize::{ - load, - load_checked, - }, - instruction::CommandHeader, - utils::{ - check_permissioned_funding_account, - check_valid_funding_account, - check_valid_writable_account, - pyth_assert, - }, - OracleError, - }, - solana_program::{ - account_info::AccountInfo, - entrypoint::ProgramResult, - program_error::ProgramError, - pubkey::Pubkey, - }, - std::mem::size_of, -}; - -/// Init price feed index -// account[0] funding account [signer writable] -// account[1] price account [writable] -// account[2] permissions account [writable] -pub fn init_price_feed_index( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let cmd = load::(instruction_data)?; - - pyth_assert( - instruction_data.len() == size_of::(), - ProgramError::InvalidArgument, - )?; - - let (funding_account, price_account, permissions_account) = match accounts { - [x, y, p] => Ok((x, y, p)), - _ => Err(OracleError::InvalidNumberOfAccounts), - }?; - - check_valid_funding_account(funding_account)?; - check_permissioned_funding_account( - program_id, - price_account, - funding_account, - permissions_account, - cmd, - )?; - check_valid_writable_account(program_id, permissions_account)?; - - let mut price_account_data = load_checked::(price_account, cmd.version)?; - pyth_assert( - price_account_data.feed_index == 0, - OracleError::FeedIndexAlreadyInitialized.into(), - )?; - price_account_data.feed_index = reserve_new_price_feed_index(permissions_account)?; - - Ok(()) -} diff --git a/program/rust/src/tests/test_add_price.rs b/program/rust/src/tests/test_add_price.rs index 689d852ba..01a9cfd96 100644 --- a/program/rust/src/tests/test_add_price.rs +++ b/program/rust/src/tests/test_add_price.rs @@ -130,58 +130,6 @@ fn test_add_price() { assert!(product_data.first_price_account == *price_account_2.key); } - // Emulate pre-existing price accounts without a feed index. - { - let mut price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - price_data.feed_index = 0; - let mut price_data_2 = load_checked::(&price_account_2, PC_VERSION).unwrap(); - price_data_2.feed_index = 0; - } - let hdr_init_price_feed_index = CommandHeader::from(OracleCommand::InitPriceFeedIndex); - let instruction_data_init_price_feed_index = bytes_of(&hdr_init_price_feed_index); - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account.clone(), - permissions_account.clone(), - ], - instruction_data_init_price_feed_index, - ) - .unwrap(); - { - let price_data = load_checked::(&price_account, PC_VERSION).unwrap(); - assert_eq!(price_data.feed_index, 3); - } - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account_2.clone(), - permissions_account.clone(), - ], - instruction_data_init_price_feed_index, - ) - .unwrap(); - { - let price_data_2 = load_checked::(&price_account_2, PC_VERSION).unwrap(); - assert_eq!(price_data_2.feed_index, 4); - } - - // Feed index is already set. - assert_eq!( - process_instruction( - &program_id, - &[ - funding_account.clone(), - price_account_2.clone(), - permissions_account.clone(), - ], - instruction_data_init_price_feed_index, - ), - Err(OracleError::FeedIndexAlreadyInitialized.into()) - ); - // Wrong number of accounts assert_eq!( process_instruction( From f15fac5975ba3b99363015e47ebb39d271431d75 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Mon, 6 Jan 2025 13:48:31 -0800 Subject: [PATCH 3/5] refactor: format --- program/rust/src/processor.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/program/rust/src/processor.rs b/program/rust/src/processor.rs index b0a20c565..80ee17407 100644 --- a/program/rust/src/processor.rs +++ b/program/rust/src/processor.rs @@ -44,6 +44,11 @@ pub use add_publisher::{ DISABLE_ACCUMULATOR_V2, ENABLE_ACCUMULATOR_V2, }; +use solana_program::{ + program_error::ProgramError, + rent::Rent, + sysvar::Sysvar, +}; pub use { add_price::add_price, add_product::add_product, @@ -66,11 +71,6 @@ pub use { }, upd_product::upd_product, }; -use solana_program::{ - program_error::ProgramError, - rent::Rent, - sysvar::Sysvar, -}; /// Dispatch to the right instruction in the oracle. @@ -105,7 +105,9 @@ pub fn process_instruction( UpdPermissions => upd_permissions(program_id, accounts, instruction_data), SetMaxLatency => set_max_latency(program_id, accounts, instruction_data), InitPriceFeedIndex => { - solana_program::msg!("Oracle init price feed index instruction has been removed. Bailing out!"); + solana_program::msg!( + "Oracle init price feed index instruction has been removed. Bailing out!" + ); Err(OracleError::UnrecognizedInstruction.into()) } ResizeMapping => resize_mapping(program_id, accounts, instruction_data), From e3fa87022a61c56f96b02cb98d224e5386f28df3 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 7 Jan 2025 14:18:05 -0800 Subject: [PATCH 4/5] fix: remove explicit funding account from resizemapping ix --- program/rust/src/tests/pyth_simulator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/program/rust/src/tests/pyth_simulator.rs b/program/rust/src/tests/pyth_simulator.rs index b8f59f801..f6704e8d9 100644 --- a/program/rust/src/tests/pyth_simulator.rs +++ b/program/rust/src/tests/pyth_simulator.rs @@ -492,12 +492,12 @@ impl PythSimulator { let instruction = Instruction::new_with_bytes( self.program_id, bytes_of(&cmd), - vec![AccountMeta::new(mapping_keypair.pubkey(), true)], + vec![AccountMeta::new(mapping_keypair.pubkey(), false)], ); self.process_ixs( &[instruction], - &vec![mapping_keypair], + &vec![], ©_keypair(&self.genesis_keypair), ) .await From a18e3bf9878574a663899b8350c8e02416cc5e89 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Wed, 8 Jan 2025 13:34:53 +0100 Subject: [PATCH 5/5] Apply suggestions from code review --- program/rust/src/instruction.rs | 3 +-- program/rust/src/processor/resize_mapping.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/program/rust/src/instruction.rs b/program/rust/src/instruction.rs index 43a32b9ca..58ff3425e 100644 --- a/program/rust/src/instruction.rs +++ b/program/rust/src/instruction.rs @@ -109,8 +109,7 @@ pub enum OracleCommand { // account[1] price account [writable] // account[2] permissions account [writable] InitPriceFeedIndex = 19, - // account[0] funding account [signer writable] - // account[1] mapping account [writable] + // account[0] mapping account [writable] ResizeMapping = 20, } diff --git a/program/rust/src/processor/resize_mapping.rs b/program/rust/src/processor/resize_mapping.rs index 914d1b0ca..3ca8b1063 100644 --- a/program/rust/src/processor/resize_mapping.rs +++ b/program/rust/src/processor/resize_mapping.rs @@ -32,7 +32,7 @@ use { }; /// Resize mapping account. -// account[1] mapping account [writable] +// account[0] mapping account [writable] pub fn resize_mapping( program_id: &Pubkey, accounts: &[AccountInfo],