Skip to content

Commit 8b2f52f

Browse files
committed
refactor: use ordered publisher list
This change brings down the average cost of a price update without aggregation from 5k to 2k because binary search requires much less lookups. There are mechanims implemented in the code to make sure that upon the upgrade we sort the array without adding overhead to the happy path (working binary search). Also add_publisher now does an insertion sort to keep the list sorted and del_publisher won't change the order of the list.
1 parent ceb4a32 commit 8b2f52f

File tree

3 files changed

+151
-12
lines changed

3 files changed

+151
-12
lines changed

program/rust/src/processor/add_publisher.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,21 @@ pub fn add_publisher(
7373
}
7474
}
7575

76-
let current_index: usize = try_convert(price_data.num_)?;
76+
let mut current_index: usize = try_convert(price_data.num_)?;
7777
sol_memset(
7878
bytes_of_mut(&mut price_data.comp_[current_index]),
7979
0,
8080
size_of::<PriceComponent>(),
8181
);
8282
price_data.comp_[current_index].pub_ = cmd_args.publisher;
83+
84+
while current_index > 0
85+
&& price_data.comp_[current_index].pub_ < price_data.comp_[current_index - 1].pub_
86+
{
87+
price_data.comp_.swap(current_index, current_index - 1);
88+
current_index -= 1;
89+
}
90+
8391
price_data.num_ += 1;
8492
price_data.header.size = try_convert::<_, u32>(PriceAccount::INITIAL_SIZE)?;
8593
Ok(())

program/rust/src/processor/upd_price.rs

+58-11
Original file line numberDiff line numberDiff line change
@@ -127,27 +127,74 @@ pub fn upd_price(
127127
// Check clock
128128
let clock = Clock::from_account_info(clock_account)?;
129129

130-
let mut publisher_index: usize = 0;
130+
let publisher_index: usize;
131131
let latest_aggregate_price: PriceInfo;
132132

133133
// The price_data borrow happens in a scope because it must be
134134
// dropped before we borrow again as raw data pointer for the C
135135
// aggregation logic.
136136
{
137137
// Verify that symbol account is initialized
138-
let price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
138+
let mut price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
139139

140-
// Verify that publisher is authorized
141-
while publisher_index < try_convert::<u32, usize>(price_data.num_)? {
142-
if price_data.comp_[publisher_index].pub_ == *funding_account.key {
143-
break;
140+
// Verify that publisher is authorized by initially binary searching
141+
// for the publisher's component in the price account. The binary
142+
// search might not work if the publisher list is not sorted; therefore
143+
// we fall back to a linear search and during the search we try to bring
144+
// the publishers list closer to a sorted state. (We do not sort them
145+
// entirely to avoid exceeding the compute budget.)
146+
let mut binary_search_result = None;
147+
148+
{
149+
// Binary search to find the publisher key. Rust std binary search is not used because
150+
// they guarantee valid outcome only if the array is sorted whereas we want to rely on
151+
// a Equal match if it is a result on an unsorted array. Currently the rust
152+
// implementation behaves the same but we do not want to rely on api internals.
153+
let mut left = 0;
154+
let mut right = try_convert::<u32, usize>(price_data.num_)?;
155+
while left < right {
156+
let mid = left + (right - left) / 2;
157+
match price_data.comp_[mid].pub_.cmp(funding_account.key) {
158+
std::cmp::Ordering::Less => {
159+
left = mid + 1;
160+
}
161+
std::cmp::Ordering::Greater => {
162+
right = mid;
163+
}
164+
std::cmp::Ordering::Equal => {
165+
binary_search_result = Some(mid);
166+
break;
167+
}
168+
}
144169
}
145-
publisher_index += 1;
146170
}
147-
pyth_assert(
148-
publisher_index < try_convert::<u32, usize>(price_data.num_)?,
149-
OracleError::PermissionViolation.into(),
150-
)?;
171+
172+
publisher_index = match binary_search_result {
173+
Some(index) => index,
174+
None => {
175+
// One pass through the publishers list to make it closer
176+
// to being sorted.
177+
for i in 1..try_convert::<u32, usize>(price_data.num_)? {
178+
if price_data.comp_[i].pub_ < price_data.comp_[i - 1].pub_ {
179+
price_data.comp_.swap(i, i - 1);
180+
}
181+
}
182+
183+
let mut index = 0;
184+
while index < try_convert::<u32, usize>(price_data.num_)? {
185+
if price_data.comp_[index].pub_ == *funding_account.key {
186+
break;
187+
}
188+
index += 1;
189+
}
190+
pyth_assert(
191+
index < try_convert::<u32, usize>(price_data.num_)?,
192+
OracleError::PermissionViolation.into(),
193+
)?;
194+
index
195+
}
196+
};
197+
151198

152199
latest_aggregate_price = price_data.agg_;
153200
let latest_publisher_price = price_data.comp_[publisher_index].latest_;

program/rust/src/tests/test_upd_price_v2.rs

+84
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,90 @@ fn test_upd_price_v2() -> Result<(), Box<dyn std::error::Error>> {
444444
Ok(())
445445
}
446446

447+
#[test]
448+
fn test_upd_works_with_unordered_publisher_set() -> Result<(), Box<dyn std::error::Error>> {
449+
let mut instruction_data = [0u8; size_of::<UpdPriceArgs>()];
450+
451+
let program_id = Pubkey::new_unique();
452+
453+
let mut price_setup = AccountSetup::new::<PriceAccount>(&program_id);
454+
let mut price_account = price_setup.as_account_info();
455+
price_account.is_signer = false;
456+
PriceAccount::initialize(&price_account, PC_VERSION).unwrap();
457+
458+
let mut publishers_setup: Vec<_> = (0..20).map(|_| AccountSetup::new_funding()).collect();
459+
let mut publishers: Vec<_> = publishers_setup
460+
.iter_mut()
461+
.map(|s| s.as_account_info())
462+
.collect();
463+
464+
publishers.sort_by_key(|x| x.key);
465+
466+
{
467+
let mut price_data = load_checked::<PriceAccount>(&price_account, PC_VERSION).unwrap();
468+
price_data.num_ = 20;
469+
// Store the publishers in reverse order
470+
publishers
471+
.iter()
472+
.rev()
473+
.enumerate()
474+
.for_each(|(i, account)| {
475+
println!("{:} {:?}", i, account.key);
476+
price_data.comp_[i].pub_ = *account.key;
477+
});
478+
}
479+
480+
let mut clock_setup = AccountSetup::new_clock();
481+
let mut clock_account = clock_setup.as_account_info();
482+
clock_account.is_signer = false;
483+
clock_account.is_writable = false;
484+
485+
update_clock_slot(&mut clock_account, 1);
486+
487+
// repeat 10 times to also make sure that price updates
488+
// make the publisher list sorted
489+
for slot in 1..10 {
490+
for (i, publisher) in publishers.iter().enumerate() {
491+
println!("{:} {:?}", i, publisher.key);
492+
populate_instruction(&mut instruction_data, (i + 100) as i64, 10, slot);
493+
process_instruction(
494+
&program_id,
495+
&[
496+
publisher.clone(),
497+
price_account.clone(),
498+
clock_account.clone(),
499+
],
500+
&instruction_data,
501+
)?;
502+
}
503+
504+
update_clock_slot(&mut clock_account, slot + 1);
505+
}
506+
507+
{
508+
let price_data = load_checked::<PriceAccount>(&price_account, PC_VERSION).unwrap();
509+
510+
// Check publishers are sorted
511+
let price_data_pubs: Vec<_> = price_data.comp_[..20].iter().map(|c| c.pub_).collect();
512+
assert_eq!(
513+
price_data_pubs,
514+
publishers.iter().map(|p| *p.key).collect::<Vec<_>>()
515+
);
516+
517+
assert_eq!(price_data.comp_[0].latest_.price_, 100);
518+
assert_eq!(price_data.comp_[0].latest_.conf_, 10);
519+
assert_eq!(price_data.comp_[0].latest_.pub_slot_, 9);
520+
assert_eq!(price_data.comp_[0].latest_.status_, PC_STATUS_TRADING);
521+
// aggregate is calculated at slot 9 for up to slot 8
522+
assert_eq!(price_data.valid_slot_, 8);
523+
assert_eq!(price_data.agg_.pub_slot_, 9);
524+
assert_eq!(price_data.agg_.price_, 109);
525+
assert_eq!(price_data.agg_.conf_, 8);
526+
assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING);
527+
}
528+
Ok(())
529+
}
530+
447531
// Create an upd_price instruction with the provided parameters
448532
fn populate_instruction(instruction_data: &mut [u8], price: i64, conf: u64, pub_slot: u64) {
449533
let mut cmd = load_mut::<UpdPriceArgs>(instruction_data).unwrap();

0 commit comments

Comments
 (0)