Skip to content

Commit b33a781

Browse files
authored
[oracle] Bring back resize account (#360)
* checkpoint * Remove all old sma files * Bring back warnings denial * Checkpoint * Add some tests * Cleanup * Add test sizes * Add another test * Add another test * Rename * Finish test * Delete weird files * Delete resize_price_account * Remove test gating * Renames * set initial size * More tests * Set initial size to the actual size for price accounts * Checkpoint * Update logic * More changes * Revert change * Fix * Format * Fix test * Write test * Cleanup * Fix comments * Useless copy * Comments * Add size update * Replace priceaccountsize by size_of * CI * Revert change * Cleanup comment * Comment
1 parent 923e4ae commit b33a781

File tree

12 files changed

+267
-28
lines changed

12 files changed

+267
-28
lines changed

program/rust/src/accounts.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ pub trait PythAccount: Pod {
111111
account.data_len() >= Self::MINIMUM_SIZE,
112112
OracleError::AccountTooSmall.into(),
113113
)?;
114-
114+
// Solana accounts are guaranteed to be zeroed when given to the program by an external creator.
115+
// Since all already initialized accounts of the program have a non-zero header, an account with a zeroed header
116+
// is guaranteed to be all 0s. Therefore we only need to check the header here.
115117
check_valid_fresh_account(account)?;
116-
117118
{
118119
let mut account_header = load_account_as_mut::<AccountHeader>(account)?;
119120
account_header.magic_number = PC_MAGIC;

program/rust/src/accounts/permission.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ impl PermissionAccount {
4444
#[allow(clippy::match_like_matches_macro)]
4545
match (*key, command) {
4646
(pubkey, _) if pubkey == self.master_authority => true,
47-
(pubkey, OracleCommand::SetMinPub) if pubkey == self.security_authority => true,
47+
(pubkey, OracleCommand::ResizePriceAccount) if pubkey == self.security_authority => {
48+
true
49+
} // Allow for an admin key to resize the price account
4850
_ => false,
4951
}
5052
}

program/rust/src/processor.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mod del_product;
2222
mod del_publisher;
2323
mod init_mapping;
2424
mod init_price;
25+
mod resize_price_account;
2526
mod set_min_pub;
2627
mod upd_permissions;
2728
mod upd_price;
@@ -37,6 +38,7 @@ pub use {
3738
del_publisher::del_publisher,
3839
init_mapping::init_mapping,
3940
init_price::init_price,
41+
resize_price_account::resize_price_account,
4042
set_min_pub::set_min_pub,
4143
upd_permissions::upd_permissions,
4244
upd_price::{
@@ -71,7 +73,7 @@ pub fn process_instruction(
7173
UpdTest => Err(OracleError::UnrecognizedInstruction.into()),
7274
SetMinPub => set_min_pub(program_id, accounts, instruction_data),
7375
UpdPriceNoFailOnError => upd_price_no_fail_on_error(program_id, accounts, instruction_data),
74-
ResizePriceAccount => Err(OracleError::UnrecognizedInstruction.into()),
76+
ResizePriceAccount => resize_price_account(program_id, accounts, instruction_data),
7577
DelPrice => del_price(program_id, accounts, instruction_data),
7678
DelProduct => del_product(program_id, accounts, instruction_data),
7779
UpdPermissions => upd_permissions(program_id, accounts, instruction_data),
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use {
2+
crate::{
3+
accounts::{
4+
PriceAccount,
5+
PriceAccountV2,
6+
},
7+
c_oracle_header::PC_VERSION,
8+
deserialize::{
9+
load,
10+
load_checked,
11+
},
12+
instruction::CommandHeader,
13+
utils::{
14+
check_valid_funding_account,
15+
check_valid_signable_account_or_permissioned_funding_account,
16+
get_rent,
17+
pyth_assert,
18+
send_lamports,
19+
try_convert,
20+
},
21+
OracleError,
22+
},
23+
solana_program::{
24+
account_info::AccountInfo,
25+
entrypoint::ProgramResult,
26+
pubkey::Pubkey,
27+
rent::Rent,
28+
system_program::check_id,
29+
},
30+
std::mem::size_of,
31+
};
32+
33+
// Increase the size of an account allocated for a PriceAccount such that it can hold a PriceAccountV2
34+
// account[0] funding account [signer writable]
35+
// account[1] price account [signer writable]
36+
// account[2] system program []
37+
pub fn resize_price_account(
38+
program_id: &Pubkey,
39+
accounts: &[AccountInfo],
40+
instruction_data: &[u8],
41+
) -> ProgramResult {
42+
let (funding_account, price_account, system_program, permissions_account_option) =
43+
match accounts {
44+
[x, y, z] => Ok((x, y, z, None)),
45+
[x, y, z, p] => Ok((x, y, z, Some(p))),
46+
_ => Err(OracleError::InvalidNumberOfAccounts),
47+
}?;
48+
49+
let hdr = load::<CommandHeader>(instruction_data)?;
50+
51+
check_valid_funding_account(funding_account)?;
52+
check_valid_signable_account_or_permissioned_funding_account(
53+
program_id,
54+
price_account,
55+
funding_account,
56+
permissions_account_option,
57+
hdr,
58+
)?;
59+
pyth_assert(
60+
check_id(system_program.key),
61+
OracleError::InvalidSystemAccount.into(),
62+
)?;
63+
64+
// Check that it is a valid initialized price account
65+
{
66+
load_checked::<PriceAccount>(price_account, PC_VERSION)?;
67+
}
68+
let account_len = price_account.try_data_len()?;
69+
if account_len < size_of::<PriceAccountV2>() {
70+
// Ensure account is still rent exempt after resizing
71+
let rent: Rent = get_rent()?;
72+
let lamports_needed: u64 = rent
73+
.minimum_balance(size_of::<PriceAccountV2>())
74+
.saturating_sub(price_account.lamports());
75+
if lamports_needed > 0 {
76+
send_lamports(
77+
funding_account,
78+
price_account,
79+
system_program,
80+
lamports_needed,
81+
)?;
82+
}
83+
// We do not need to zero allocate because we won't access the data in the same
84+
// instruction
85+
price_account.realloc(size_of::<PriceAccountV2>(), false)?;
86+
87+
// Check that we can still load the account, update header size for homogeneity
88+
{
89+
let mut price_data = load_checked::<PriceAccount>(price_account, PC_VERSION)?;
90+
price_data.header.size = try_convert(size_of::<PriceAccountV2>())?;
91+
}
92+
}
93+
94+
Ok(())
95+
}

program/rust/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod test_message;
1515
mod test_permission_migration;
1616
mod test_publish;
1717
mod test_publish_batch;
18+
mod test_resize_price_account;
1819
mod test_set_min_pub;
1920
mod test_sizes;
2021
mod test_twap;

program/rust/src/tests/pyth_simulator.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,16 +505,37 @@ impl PythSimulator {
505505
/// Setup 3 product accounts with 1 price account each and add a publisher to all of them.
506506
/// Returns the mapping of product symbol to price account pubkey.
507507
/// TODO : this fixture doesn't set the product metadata
508-
pub async fn setup_product_fixture(&mut self, publisher: Pubkey) -> HashMap<String, Pubkey> {
508+
pub async fn setup_product_fixture(
509+
&mut self,
510+
publisher: Pubkey,
511+
security_authority: Pubkey,
512+
) -> HashMap<String, Pubkey> {
509513
let result_file =
510514
File::open("./test_data/publish/products.json").expect("Test file not found");
511515

512516
self.airdrop(&publisher, 100 * LAMPORTS_PER_SOL)
513517
.await
514518
.unwrap();
515519

520+
self.airdrop(&security_authority, 100 * LAMPORTS_PER_SOL)
521+
.await
522+
.unwrap();
523+
524+
self.upd_permissions(
525+
UpdPermissionsArgs {
526+
header: OracleCommand::UpdPermissions.into(),
527+
master_authority: self.upgrade_authority.pubkey(),
528+
data_curation_authority: self.upgrade_authority.pubkey(),
529+
security_authority,
530+
},
531+
&copy_keypair(&self.upgrade_authority),
532+
)
533+
.await
534+
.unwrap();
535+
516536
let product_metadatas: HashMap<String, ProductMetadata> =
517537
serde_json::from_reader(&result_file).unwrap();
538+
518539
let mut price_accounts: HashMap<String, Pubkey> = HashMap::new();
519540
let mapping_keypair: Keypair = self.init_mapping().await.unwrap();
520541
for symbol in product_metadatas.keys() {
@@ -530,6 +551,29 @@ impl PythSimulator {
530551
pub async fn warp_to_slot(&mut self, slot: u64) -> Result<(), ProgramTestError> {
531552
self.context.warp_to_slot(slot)
532553
}
554+
555+
/// Resize a price account (using the resize_price_account
556+
/// instruction).
557+
pub async fn resize_price_account(
558+
&mut self,
559+
price_account: Pubkey,
560+
security_authority: &Keypair,
561+
) -> Result<(), BanksClientError> {
562+
let cmd: CommandHeader = OracleCommand::ResizePriceAccount.into();
563+
let instruction = Instruction::new_with_bytes(
564+
self.program_id,
565+
bytes_of(&cmd),
566+
vec![
567+
AccountMeta::new(security_authority.pubkey(), true),
568+
AccountMeta::new(price_account, false),
569+
AccountMeta::new(system_program::id(), false),
570+
AccountMeta::new_readonly(self.get_permissions_pubkey(), false),
571+
],
572+
);
573+
574+
self.process_ixs(&[instruction], &vec![], security_authority)
575+
.await
576+
}
533577
}
534578

535579
pub fn copy_keypair(keypair: &Keypair) -> Keypair {

program/rust/src/tests/test_permission_migration.rs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -272,21 +272,23 @@ fn test_permission_migration() {
272272
);
273273

274274

275-
// Security authority can change minimum number of publishers
276-
process_instruction(
277-
&program_id,
278-
&[
279-
security_auth_account.clone(),
280-
price_account.clone(),
281-
permissions_account.clone(),
282-
],
283-
bytes_of::<SetMinPubArgs>(&SetMinPubArgs {
284-
header: SetMinPub.into(),
285-
minimum_publishers: 5,
286-
unused_: [0; 3],
287-
}),
288-
)
289-
.unwrap();
275+
// Security authority can't change minimum number of publishers
276+
assert_eq!(
277+
process_instruction(
278+
&program_id,
279+
&[
280+
security_auth_account.clone(),
281+
price_account.clone(),
282+
permissions_account.clone(),
283+
],
284+
bytes_of::<SetMinPubArgs>(&SetMinPubArgs {
285+
header: SetMinPub.into(),
286+
minimum_publishers: 5,
287+
unused_: [0; 3],
288+
}),
289+
),
290+
Err(OracleError::PermissionViolation.into())
291+
);
290292

291293
// Security authority can't add publishers
292294
assert_eq!(

program/rust/src/tests/test_publish.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use {
22
crate::{
3-
accounts::PriceAccount,
3+
accounts::{
4+
PriceAccount,
5+
PriceAccountV2,
6+
PythAccount,
7+
},
48
c_oracle_header::{
59
PC_STATUS_TRADING,
610
PC_STATUS_UNKNOWN,
@@ -13,17 +17,22 @@ use {
1317
},
1418
solana_program::pubkey::Pubkey,
1519
solana_sdk::{
20+
account::Account,
1621
signature::Keypair,
1722
signer::Signer,
1823
},
24+
std::mem::size_of,
1925
};
2026

2127

2228
#[tokio::test]
2329
async fn test_publish() {
2430
let mut sim = PythSimulator::new().await;
2531
let publisher = Keypair::new();
26-
let price_accounts = sim.setup_product_fixture(publisher.pubkey()).await;
32+
let security_authority = Keypair::new();
33+
let price_accounts = sim
34+
.setup_product_fixture(publisher.pubkey(), security_authority.pubkey())
35+
.await;
2736
let price = price_accounts["LTC"];
2837

2938
// Check price account before publishing
@@ -106,6 +115,16 @@ async fn test_publish() {
106115
assert_eq!(price_data.comp_[0].agg_.status_, PC_STATUS_TRADING);
107116
}
108117

118+
// Resize, check if publishing still works
119+
let price_account: Account = sim.get_account(price).await.unwrap();
120+
assert_eq!(price_account.data.len(), PriceAccount::MINIMUM_SIZE);
121+
122+
sim.resize_price_account(price, &security_authority)
123+
.await
124+
.unwrap();
125+
let price_account: Account = sim.get_account(price).await.unwrap();
126+
assert_eq!(price_account.data.len(), size_of::<PriceAccountV2>());
127+
109128
sim.warp_to_slot(3).await.unwrap();
110129
sim.upd_price(
111130
&publisher,

program/rust/src/tests/test_publish_batch.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ use {
2424
async fn test_publish_batch() {
2525
let mut sim = PythSimulator::new().await;
2626
let publisher = Keypair::new();
27-
let price_accounts = sim.setup_product_fixture(publisher.pubkey()).await;
27+
let security_authority = Keypair::new();
28+
let price_accounts = sim
29+
.setup_product_fixture(publisher.pubkey(), security_authority.pubkey())
30+
.await;
2831

2932
for price in price_accounts.values() {
3033
let price_data = sim
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use {
2+
crate::{
3+
accounts::{
4+
PriceAccount,
5+
PriceAccountV2,
6+
},
7+
error::OracleError,
8+
tests::pyth_simulator::PythSimulator,
9+
},
10+
solana_sdk::{
11+
account::Account,
12+
signature::Keypair,
13+
signer::Signer,
14+
},
15+
std::mem::size_of,
16+
};
17+
18+
#[tokio::test]
19+
async fn test_resize_price_account() {
20+
let mut sim = PythSimulator::new().await;
21+
let publisher = Keypair::new();
22+
let security_authority = Keypair::new();
23+
let price_accounts = sim
24+
.setup_product_fixture(publisher.pubkey(), security_authority.pubkey())
25+
.await;
26+
let price = price_accounts["LTC"];
27+
28+
// Check size after initialization
29+
let price_account = sim.get_account(price).await.unwrap();
30+
assert_eq!(price_account.data.len(), size_of::<PriceAccount>());
31+
32+
// Run with the wrong authority
33+
assert_eq!(
34+
sim.resize_price_account(price, &publisher)
35+
.await
36+
.unwrap_err()
37+
.unwrap(),
38+
OracleError::PermissionViolation.into()
39+
);
40+
41+
// Run the instruction once
42+
assert!(sim
43+
.resize_price_account(price, &security_authority)
44+
.await
45+
.is_ok());
46+
// Check new size
47+
let price_account: Account = sim.get_account(price).await.unwrap();
48+
assert_eq!(price_account.data.len(), size_of::<PriceAccountV2>());
49+
50+
// Future calls don't change the size
51+
assert!(sim
52+
.resize_price_account(price, &security_authority)
53+
.await
54+
.is_ok());
55+
let price_account = sim.get_account(price).await.unwrap();
56+
assert_eq!(price_account.data.len(), size_of::<PriceAccountV2>());
57+
}

0 commit comments

Comments
 (0)