Skip to content

Commit f82db51

Browse files
authored
Prove that new publishing slots work in a full-pubset test (#385)
* WIP: Prove that new publishing slots work in a full-pubset test * Uncomment to fix warning error * docker/Dockerfile: remove stray caching comment * utils.rs: get_status_for_update -> get_status_for_conf_price_ratio * test_full_publisher_set.rs: Uncomment WIP code, explain the test * build-bpf.sh: Remove development test filtering * upd_aggregate.h: Remove the paranoic NUM_COMP check * pyth_simulator.rs: apply clippy's lint * upd_price.rs: Remove stray debug message * test_full_publisher_set.rs: Use a 50/50 division instead of thirds * test_full_publisher_set.rs: Clarify comments, simplify slot advance * test_full_publisher_set.rs: Rewrite comment, rename test
1 parent 63e6356 commit f82db51

File tree

7 files changed

+121
-15
lines changed

7 files changed

+121
-15
lines changed

program/rust/src/processor/upd_price.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use {
2424
utils::{
2525
check_valid_funding_account,
2626
check_valid_writable_account,
27-
get_status_for_update,
27+
get_status_for_conf_price_ratio,
2828
is_component_update,
2929
pyth_assert,
3030
try_convert,
@@ -271,8 +271,11 @@ pub fn upd_price(
271271

272272
// Try to update the publisher's price
273273
if is_component_update(cmd_args)? {
274+
// IMPORTANT: If the publisher does not meet the price/conf
275+
// ratio condition, its price will not count for the next
276+
// aggregate.
274277
let status: u32 =
275-
get_status_for_update(cmd_args.price, cmd_args.confidence, cmd_args.status)?;
278+
get_status_for_conf_price_ratio(cmd_args.price, cmd_args.confidence, cmd_args.status)?;
276279

277280
{
278281
let publisher_price = &mut price_data.comp_[publisher_index].latest_;

program/rust/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod test_del_price;
88
mod test_del_product;
99
mod test_del_publisher;
1010
mod test_ema;
11+
mod test_full_publisher_set;
1112
mod test_init_mapping;
1213
mod test_init_price;
1314
mod test_message;

program/rust/src/tests/pyth_simulator.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ impl PythSimulator {
422422

423423
for (key, price_account) in price_accounts {
424424
let cmd = UpdPriceArgs {
425-
header: OracleCommand::UpdPriceNoFailOnError.into(),
425+
header: OracleCommand::UpdPrice.into(),
426426
status: quotes[key].status,
427427
unused_: 0,
428428
price: quotes[key].price,
@@ -536,15 +536,17 @@ impl PythSimulator {
536536
/// TODO : this fixture doesn't set the product metadata
537537
pub async fn setup_product_fixture(
538538
&mut self,
539-
publisher: Pubkey,
539+
publishers: &[Pubkey],
540540
security_authority: Pubkey,
541541
) -> HashMap<String, Pubkey> {
542542
let result_file =
543543
File::open("./test_data/publish/products.json").expect("Test file not found");
544544

545-
self.airdrop(&publisher, 100 * LAMPORTS_PER_SOL)
546-
.await
547-
.unwrap();
545+
for publisher in publishers {
546+
self.airdrop(publisher, 100 * LAMPORTS_PER_SOL)
547+
.await
548+
.unwrap();
549+
}
548550

549551
self.airdrop(&security_authority, 100 * LAMPORTS_PER_SOL)
550552
.await
@@ -570,7 +572,11 @@ impl PythSimulator {
570572
for symbol in product_metadatas.keys() {
571573
let product_keypair = self.add_product(&mapping_keypair).await.unwrap();
572574
let price_keypair = self.add_price(&product_keypair, -5).await.unwrap();
573-
self.add_publisher(&price_keypair, publisher).await.unwrap();
575+
for publisher in publishers.iter() {
576+
self.add_publisher(&price_keypair, *publisher)
577+
.await
578+
.unwrap();
579+
}
574580
price_accounts.insert(symbol.to_string(), price_keypair.pubkey());
575581
}
576582
price_accounts
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use {
2+
crate::{
3+
accounts::PriceAccount,
4+
c_oracle_header::{
5+
PC_NUM_COMP,
6+
PC_STATUS_TRADING,
7+
},
8+
tests::pyth_simulator::{
9+
PythSimulator,
10+
Quote,
11+
},
12+
},
13+
solana_sdk::{
14+
signature::Keypair,
15+
signer::Signer,
16+
},
17+
};
18+
19+
// Verify that the whole publisher set participates in aggregate
20+
// calculation. This is important for verifying that extra
21+
// publisher slots on Pythnet are working. Here's how this works:
22+
//
23+
// * Fill all publisher slots on a price
24+
// * Divide the price component array into two even halves: first_half, second_half
25+
// * Publish two distinct price values to either half
26+
// * Verify that the aggregate averages out to an expected value in the middle
27+
#[tokio::test]
28+
async fn test_full_publisher_set() -> Result<(), Box<dyn std::error::Error>> {
29+
let mut sim = PythSimulator::new().await;
30+
let pub_keypairs: Vec<_> = (0..PC_NUM_COMP).map(|_idx| Keypair::new()).collect();
31+
let pub_pubkeys: Vec<_> = pub_keypairs.iter().map(|kp| kp.pubkey()).collect();
32+
33+
let security_authority = Keypair::new();
34+
let price_accounts = sim
35+
.setup_product_fixture(pub_pubkeys.as_slice(), security_authority.pubkey())
36+
.await;
37+
let price = price_accounts["LTC"];
38+
39+
40+
let n_pubs = pub_keypairs.len();
41+
42+
// Divide publishers into two even parts (assuming the max PC_NUM_COMP size is even)
43+
let (first_half, second_half) = pub_keypairs.split_at(n_pubs / 2);
44+
45+
// Starting with the first publisher in each half, publish an update
46+
for (first_kp, second_kp) in first_half.iter().zip(second_half.iter()) {
47+
let first_quote = Quote {
48+
price: 100,
49+
confidence: 30,
50+
status: PC_STATUS_TRADING,
51+
};
52+
53+
sim.upd_price(first_kp, price, first_quote).await?;
54+
55+
let second_quote = Quote {
56+
price: 120,
57+
confidence: 30,
58+
status: PC_STATUS_TRADING,
59+
};
60+
61+
sim.upd_price(second_kp, price, second_quote).await?;
62+
}
63+
64+
// Advance slot once from 1 to 2
65+
sim.warp_to_slot(2).await?;
66+
67+
// Final price update to trigger aggregation
68+
let first_kp = pub_keypairs.first().unwrap();
69+
let first_quote = Quote {
70+
price: 100,
71+
confidence: 30,
72+
status: PC_STATUS_TRADING,
73+
};
74+
sim.upd_price(first_kp, price, first_quote).await?;
75+
76+
{
77+
let price_data = sim
78+
.get_account_data_as::<PriceAccount>(price)
79+
.await
80+
.unwrap();
81+
82+
assert_eq!(price_data.agg_.price_, 110);
83+
assert_eq!(price_data.agg_.conf_, 20);
84+
}
85+
86+
Ok(())
87+
}

program/rust/src/tests/test_publish.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async fn test_publish() {
2424
let publisher = Keypair::new();
2525
let security_authority = Keypair::new();
2626
let price_accounts = sim
27-
.setup_product_fixture(publisher.pubkey(), security_authority.pubkey())
27+
.setup_product_fixture(&[publisher.pubkey()], security_authority.pubkey())
2828
.await;
2929
let price = price_accounts["LTC"];
3030

program/rust/src/tests/test_publish_batch.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use {
1010
PythSimulator,
1111
Quote,
1212
},
13-
utils::get_status_for_update,
13+
utils::get_status_for_conf_price_ratio,
1414
},
1515
solana_program::pubkey::Pubkey,
1616
solana_sdk::{
@@ -26,7 +26,7 @@ async fn test_publish_batch() {
2626
let publisher = Keypair::new();
2727
let security_authority = Keypair::new();
2828
let price_accounts = sim
29-
.setup_product_fixture(publisher.pubkey(), security_authority.pubkey())
29+
.setup_product_fixture(&[publisher.pubkey()], security_authority.pubkey())
3030
.await;
3131

3232
for price in price_accounts.values() {
@@ -82,7 +82,7 @@ async fn test_publish_batch() {
8282
assert_eq!(price_data.comp_[0].latest_.conf_, quote.confidence);
8383
assert_eq!(
8484
price_data.comp_[0].latest_.status_,
85-
get_status_for_update(quote.price, quote.confidence, quote.status).unwrap()
85+
get_status_for_conf_price_ratio(quote.price, quote.confidence, quote.status).unwrap()
8686
);
8787
assert_eq!(price_data.comp_[0].agg_.price_, 0);
8888
assert_eq!(price_data.comp_[0].agg_.conf_, 0);
@@ -118,13 +118,18 @@ async fn test_publish_batch() {
118118
assert_eq!(price_data.comp_[0].latest_.conf_, new_quote.confidence);
119119
assert_eq!(
120120
price_data.comp_[0].latest_.status_,
121-
get_status_for_update(new_quote.price, new_quote.confidence, new_quote.status).unwrap()
121+
get_status_for_conf_price_ratio(
122+
new_quote.price,
123+
new_quote.confidence,
124+
new_quote.status
125+
)
126+
.unwrap()
122127
);
123128
assert_eq!(price_data.comp_[0].agg_.price_, quote.price);
124129
assert_eq!(price_data.comp_[0].agg_.conf_, quote.confidence);
125130
assert_eq!(
126131
price_data.comp_[0].agg_.status_,
127-
get_status_for_update(quote.price, quote.confidence, quote.status).unwrap()
132+
get_status_for_conf_price_ratio(quote.price, quote.confidence, quote.status).unwrap()
128133
);
129134
}
130135
}

program/rust/src/utils.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,11 @@ pub fn is_component_update(cmd_args: &UpdPriceArgs) -> Result<bool, OracleError>
200200
}
201201

202202
// Return PC_STATUS_IGNORED if confidence is bigger than price divided by MAX_CI_DIVISOR else returns status
203-
pub fn get_status_for_update(price: i64, confidence: u64, status: u32) -> Result<u32, OracleError> {
203+
pub fn get_status_for_conf_price_ratio(
204+
price: i64,
205+
confidence: u64,
206+
status: u32,
207+
) -> Result<u32, OracleError> {
204208
let mut threshold_conf = price / MAX_CI_DIVISOR;
205209

206210
if threshold_conf < 0 {

0 commit comments

Comments
 (0)