Skip to content

Commit 256b575

Browse files
authored
Add feed index for batch publish (#416)
* batch publish wip * refactor: separate header checks and account data casts * feat: feed index (wip) * feat: assign price feed indexes to old accounts * feat: init feed index when creating prices and init permission account * refactor: remove transfer to save contract size * test: add test for price feed indexes * refactor: address feedback * chore: fix some outdated account list docs
1 parent 5ee8b5f commit 256b575

16 files changed

+331
-75
lines changed

program/rust/build.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ fn do_make_build(targets: Vec<&str>, out_dir: &Path) {
9191
"C oracle make build did not exit with 0 (code
9292
({:?}).\n\nstdout:\n{}\n\nstderr:\n{}",
9393
make_output.status.code(),
94-
String::from_utf8(make_output.stdout).unwrap_or("<non-utf8>".to_owned()),
95-
String::from_utf8(make_output.stderr).unwrap_or("<non-utf8>".to_owned())
94+
String::from_utf8_lossy(&make_output.stdout),
95+
String::from_utf8_lossy(&make_output.stderr)
9696
);
9797
}
9898
}

program/rust/src/accounts.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub use {
5959
PriceEma,
6060
PriceInfo,
6161
PythOracleSerialize,
62+
MAX_FEED_INDEX,
6263
},
6364
product::{
6465
update_product_metadata,
@@ -105,6 +106,11 @@ pub trait PythAccount: Pod {
105106
/// have. `INITIAL_SIZE` <= `minimum_size()`
106107
const MINIMUM_SIZE: usize = size_of::<Self>();
107108

109+
/// Size of the account data on creation. Usually this is the same as `MINIMUM_SIZE` but it's
110+
/// different for `PermissionAccount` because we've added new fields to it. In this case
111+
/// we cannot increase `MINIMUM_SIZE` because that would break reading the account.
112+
const NEW_ACCOUNT_SPACE: usize = Self::MINIMUM_SIZE;
113+
108114
/// Given an `AccountInfo`, verify it is sufficiently large and has the correct discriminator.
109115
fn initialize<'a>(
110116
account: &'a AccountInfo,
@@ -139,15 +145,15 @@ pub trait PythAccount: Pod {
139145
seeds: &[&[u8]],
140146
version: u32,
141147
) -> Result<(), ProgramError> {
142-
let target_rent = get_rent()?.minimum_balance(Self::MINIMUM_SIZE);
148+
let target_rent = get_rent()?.minimum_balance(Self::NEW_ACCOUNT_SPACE);
143149

144150
if account.data_len() == 0 {
145151
create(
146152
funding_account,
147153
account,
148154
system_program,
149155
program_id,
150-
Self::MINIMUM_SIZE,
156+
Self::NEW_ACCOUNT_SPACE,
151157
target_rent,
152158
seeds,
153159
)?;

program/rust/src/accounts/permission.rs

+25-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ use {
1111
Pod,
1212
Zeroable,
1313
},
14-
solana_program::pubkey::Pubkey,
15-
std::mem::size_of,
14+
solana_program::{
15+
account_info::AccountInfo,
16+
program_error::ProgramError,
17+
pubkey::Pubkey,
18+
},
19+
std::{
20+
cell::RefMut,
21+
mem::size_of,
22+
},
1623
};
1724

1825
/// This account stores the pubkeys that can execute administrative instructions in the Pyth
@@ -50,9 +57,24 @@ impl PermissionAccount {
5057
_ => false,
5158
}
5259
}
60+
61+
pub fn load_last_feed_index_mut<'a>(
62+
account: &'a AccountInfo,
63+
) -> Result<RefMut<'a, u32>, ProgramError> {
64+
let start = size_of::<PermissionAccount>();
65+
let end = start + size_of::<u32>();
66+
assert_eq!(Self::NEW_ACCOUNT_SPACE, end);
67+
if account.data_len() < end {
68+
return Err(ProgramError::AccountDataTooSmall);
69+
}
70+
Ok(RefMut::map(account.try_borrow_mut_data()?, |data| {
71+
bytemuck::from_bytes_mut(&mut data[start..end])
72+
}))
73+
}
5374
}
5475

5576
impl PythAccount for PermissionAccount {
5677
const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PERMISSIONS;
57-
const INITIAL_SIZE: u32 = size_of::<PermissionAccount>() as u32;
78+
const NEW_ACCOUNT_SPACE: usize = size_of::<PermissionAccount>() + size_of::<u32>();
79+
const INITIAL_SIZE: u32 = Self::NEW_ACCOUNT_SPACE as u32;
5880
}

program/rust/src/accounts/price.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ mod price_pythnet {
6969
pub max_latency_: u8,
7070
/// Various flags
7171
pub flags: PriceAccountFlags,
72-
/// Unused placeholder for alignment
73-
pub unused_3_: i32,
72+
/// Globally unique price feed index used for publishing.
73+
/// Limited to 28 bites so that it can be packed together with trading status in a single u32.
74+
pub feed_index: u32,
7475
/// Corresponding product account
7576
pub product_account: Pubkey,
7677
/// Next price account in the list
@@ -94,6 +95,10 @@ mod price_pythnet {
9495
pub price_cumulative: PriceCumulative,
9596
}
9697

98+
// Feed index is limited to 28 bites so that it can be packed
99+
// together with trading status in a single u32.
100+
pub const MAX_FEED_INDEX: u32 = (1 << 28) - 1;
101+
97102
bitflags! {
98103
#[repr(C)]
99104
#[derive(Copy, Clone, Pod, Zeroable)]

program/rust/src/error.rs

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ pub enum OracleError {
5252
PermissionViolation = 619,
5353
#[error("NeedsSuccesfulAggregation")]
5454
NeedsSuccesfulAggregation = 620,
55+
#[error("MaxLastFeedIndexReached")]
56+
MaxLastFeedIndexReached = 621,
57+
#[error("FeedIndexAlreadyInitialized")]
58+
FeedIndexAlreadyInitialized = 622,
5559
}
5660

5761
impl From<OracleError> for ProgramError {

program/rust/src/instruction.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ pub enum OracleCommand {
3939
// account[1] product account [signer writable]
4040
UpdProduct = 3,
4141
/// Add new price account to a product account
42-
// account[0] funding account [signer writable]
43-
// account[1] product account [signer writable]
44-
// account[2] new price account [signer writable]
42+
// account[0] funding account [signer writable]
43+
// account[1] product account [writable]
44+
// account[2] new price account [writable]
45+
// account[3] permissions account [writable]
4546
AddPrice = 4,
4647
/// Add publisher to symbol account
4748
// account[0] funding account [signer writable]
@@ -103,6 +104,11 @@ pub enum OracleCommand {
103104
// account[0] funding account [signer writable]
104105
// account[1] price account [signer writable]
105106
SetMaxLatency = 18,
107+
/// Init price feed index
108+
// account[0] funding account [signer writable]
109+
// account[1] price account [writable]
110+
// account[2] permissions account [writable]
111+
InitPriceFeedIndex = 19,
106112
}
107113

108114
#[repr(C)]

program/rust/src/lib.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#![deny(warnings)]
21
// Allow non upper case globals from C
32
#![allow(non_upper_case_globals)]
43

@@ -29,6 +28,7 @@ mod log;
2928
// While we have `pyth-sdk-rs` which exposes a more friendly interface, this is still useful when a
3029
// downstream user wants to confirm for example that they can compile against the binary interface
3130
// of this program for their specific solana version.
31+
pub use crate::error::OracleError;
3232
#[cfg(feature = "strum")]
3333
pub use accounts::MessageType;
3434
#[cfg(feature = "library")]
@@ -45,8 +45,12 @@ pub use accounts::{
4545
PythAccount,
4646
PythOracleSerialize,
4747
};
48+
#[cfg(feature = "library")]
49+
pub use {
50+
processor::find_publisher_index,
51+
utils::get_status_for_conf_price_ratio,
52+
};
4853
use {
49-
crate::error::OracleError,
5054
processor::process_instruction,
5155
solana_program::entrypoint,
5256
};

program/rust/src/processor.rs

+45
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
use {
22
crate::{
3+
accounts::{
4+
AccountHeader,
5+
PermissionAccount,
6+
PythAccount,
7+
MAX_FEED_INDEX,
8+
},
9+
deserialize::load_account_as_mut,
310
error::OracleError,
411
instruction::{
512
load_command_header_checked,
613
OracleCommand,
714
},
15+
utils::{
16+
pyth_assert,
17+
try_convert,
18+
},
819
},
920
solana_program::{
1021
entrypoint::ProgramResult,
@@ -21,6 +32,7 @@ mod del_product;
2132
mod del_publisher;
2233
mod init_mapping;
2334
mod init_price;
35+
mod init_price_feed_index;
2436
mod set_max_latency;
2537
mod set_min_pub;
2638
mod upd_permissions;
@@ -47,11 +59,20 @@ pub use {
4759
upd_price::{
4860
c_upd_aggregate,
4961
c_upd_twap,
62+
find_publisher_index,
5063
upd_price,
5164
upd_price_no_fail_on_error,
5265
},
5366
upd_product::upd_product,
5467
};
68+
use {
69+
init_price_feed_index::init_price_feed_index,
70+
solana_program::{
71+
program_error::ProgramError,
72+
rent::Rent,
73+
sysvar::Sysvar,
74+
},
75+
};
5576

5677
/// Dispatch to the right instruction in the oracle.
5778
pub fn process_instruction(
@@ -84,5 +105,29 @@ pub fn process_instruction(
84105
DelProduct => del_product(program_id, accounts, instruction_data),
85106
UpdPermissions => upd_permissions(program_id, accounts, instruction_data),
86107
SetMaxLatency => set_max_latency(program_id, accounts, instruction_data),
108+
InitPriceFeedIndex => init_price_feed_index(program_id, accounts, instruction_data),
109+
}
110+
}
111+
112+
fn reserve_new_price_feed_index(permissions_account: &AccountInfo) -> Result<u32, ProgramError> {
113+
if permissions_account.data_len() < PermissionAccount::NEW_ACCOUNT_SPACE {
114+
let new_size = PermissionAccount::NEW_ACCOUNT_SPACE;
115+
let rent = Rent::get()?;
116+
let new_minimum_balance = rent.minimum_balance(new_size);
117+
pyth_assert(
118+
permissions_account.lamports() >= new_minimum_balance,
119+
ProgramError::AccountNotRentExempt,
120+
)?;
121+
122+
permissions_account.realloc(new_size, true)?;
123+
let mut header = load_account_as_mut::<AccountHeader>(permissions_account)?;
124+
header.size = try_convert(new_size)?;
87125
}
126+
let mut last_feed_index = PermissionAccount::load_last_feed_index_mut(permissions_account)?;
127+
*last_feed_index += 1;
128+
pyth_assert(
129+
*last_feed_index <= MAX_FEED_INDEX,
130+
OracleError::MaxLastFeedIndexReached.into(),
131+
)?;
132+
Ok(*last_feed_index)
88133
}

program/rust/src/processor/add_price.rs

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use {
2+
super::reserve_new_price_feed_index,
23
crate::{
34
accounts::{
45
PriceAccount,
@@ -18,6 +19,7 @@ use {
1819
check_exponent_range,
1920
check_permissioned_funding_account,
2021
check_valid_funding_account,
22+
check_valid_writable_account,
2123
pyth_assert,
2224
},
2325
OracleError,
@@ -31,9 +33,10 @@ use {
3133
};
3234

3335
/// Add new price account to a product account
34-
// account[0] funding account [signer writable]
35-
// account[1] product account [signer writable]
36-
// account[2] new price account [signer writable]
36+
// account[0] funding account [signer writable]
37+
// account[1] product account [writable]
38+
// account[2] new price account [writable]
39+
// account[3] permissions account [writable]
3740
pub fn add_price(
3841
program_id: &Pubkey,
3942
accounts: &[AccountInfo],
@@ -68,6 +71,7 @@ pub fn add_price(
6871
permissions_account,
6972
&cmd_args.header,
7073
)?;
74+
check_valid_writable_account(program_id, permissions_account)?;
7175

7276
let mut product_data =
7377
load_checked::<ProductAccount>(product_account, cmd_args.header.version)?;
@@ -78,6 +82,7 @@ pub fn add_price(
7882
price_data.product_account = *product_account.key;
7983
price_data.next_price_account = product_data.first_price_account;
8084
price_data.min_pub_ = PRICE_ACCOUNT_DEFAULT_MIN_PUB;
85+
price_data.feed_index = reserve_new_price_feed_index(permissions_account)?;
8186
product_data.first_price_account = *price_account.key;
8287

8388
Ok(())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use {
2+
super::reserve_new_price_feed_index,
3+
crate::{
4+
accounts::PriceAccount,
5+
deserialize::{
6+
load,
7+
load_checked,
8+
},
9+
instruction::CommandHeader,
10+
utils::{
11+
check_permissioned_funding_account,
12+
check_valid_funding_account,
13+
check_valid_writable_account,
14+
pyth_assert,
15+
},
16+
OracleError,
17+
},
18+
solana_program::{
19+
account_info::AccountInfo,
20+
entrypoint::ProgramResult,
21+
program_error::ProgramError,
22+
pubkey::Pubkey,
23+
},
24+
std::mem::size_of,
25+
};
26+
27+
/// Init price feed index
28+
// account[0] funding account [signer writable]
29+
// account[1] price account [writable]
30+
// account[2] permissions account [writable]
31+
pub fn init_price_feed_index(
32+
program_id: &Pubkey,
33+
accounts: &[AccountInfo],
34+
instruction_data: &[u8],
35+
) -> ProgramResult {
36+
let cmd = load::<CommandHeader>(instruction_data)?;
37+
38+
pyth_assert(
39+
instruction_data.len() == size_of::<CommandHeader>(),
40+
ProgramError::InvalidArgument,
41+
)?;
42+
43+
let (funding_account, price_account, permissions_account) = match accounts {
44+
[x, y, p] => Ok((x, y, p)),
45+
_ => Err(OracleError::InvalidNumberOfAccounts),
46+
}?;
47+
48+
check_valid_funding_account(funding_account)?;
49+
check_permissioned_funding_account(
50+
program_id,
51+
price_account,
52+
funding_account,
53+
permissions_account,
54+
cmd,
55+
)?;
56+
check_valid_writable_account(program_id, permissions_account)?;
57+
58+
let mut price_account_data = load_checked::<PriceAccount>(price_account, cmd.version)?;
59+
pyth_assert(
60+
price_account_data.feed_index == 0,
61+
OracleError::FeedIndexAlreadyInitialized.into(),
62+
)?;
63+
price_account_data.feed_index = reserve_new_price_feed_index(permissions_account)?;
64+
65+
Ok(())
66+
}

0 commit comments

Comments
 (0)