Skip to content

Commit 6a58b0a

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 6a58b0a

File tree

3 files changed

+311
-15
lines changed

3 files changed

+311
-15
lines changed

program/rust/src/processor/add_publisher.rs

+108-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ use {
2424
account_info::AccountInfo,
2525
entrypoint::ProgramResult,
2626
program_error::ProgramError,
27-
program_memory::sol_memset,
27+
program_memory::{
28+
sol_memcmp,
29+
sol_memset,
30+
},
2831
pubkey::Pubkey,
2932
},
3033
std::mem::size_of,
@@ -41,8 +44,7 @@ pub fn add_publisher(
4144
let cmd_args = load::<AddPublisherArgs>(instruction_data)?;
4245

4346
pyth_assert(
44-
instruction_data.len() == size_of::<AddPublisherArgs>()
45-
&& cmd_args.publisher != Pubkey::default(),
47+
instruction_data.len() == size_of::<AddPublisherArgs>(),
4648
ProgramError::InvalidArgument,
4749
)?;
4850

@@ -67,20 +69,122 @@ pub fn add_publisher(
6769
return Err(ProgramError::InvalidArgument);
6870
}
6971

72+
// Use the call with the default pubkey (000..) as a trigger to sort the publishers as a
73+
// migration step from unsorted list to sorted list.
74+
if cmd_args.publisher == Pubkey::default() {
75+
let num_publishers = try_convert::<u32, usize>(price_data.num_)?;
76+
sort_publishers(&mut price_data.comp_, num_publishers)?;
77+
return Ok(());
78+
}
79+
7080
for i in 0..(try_convert::<u32, usize>(price_data.num_)?) {
7181
if cmd_args.publisher == price_data.comp_[i].pub_ {
7282
return Err(ProgramError::InvalidArgument);
7383
}
7484
}
7585

76-
let current_index: usize = try_convert(price_data.num_)?;
86+
let mut current_index: usize = try_convert(price_data.num_)?;
7787
sol_memset(
7888
bytes_of_mut(&mut price_data.comp_[current_index]),
7989
0,
8090
size_of::<PriceComponent>(),
8191
);
8292
price_data.comp_[current_index].pub_ = cmd_args.publisher;
93+
94+
// Shift the element back to keep the publishers components sorted.
95+
while current_index > 0
96+
&& price_data.comp_[current_index].pub_ < price_data.comp_[current_index - 1].pub_
97+
{
98+
price_data.comp_.swap(current_index, current_index - 1);
99+
current_index -= 1;
100+
}
101+
83102
price_data.num_ += 1;
84103
price_data.header.size = try_convert::<_, u32>(PriceAccount::INITIAL_SIZE)?;
85104
Ok(())
86105
}
106+
107+
/// A copy of rust slice/sort.rs heapsort implementation which is small and fast. We couldn't use
108+
/// the sort directly because it was only accessible behind a unstable feature flag at the time of
109+
/// writing this code.
110+
#[inline(always)]
111+
fn heapsort(v: &mut [(Pubkey, usize)]) {
112+
// This binary heap respects the invariant `parent >= child`.
113+
let sift_down = |v: &mut [(Pubkey, usize)], mut node: usize| {
114+
loop {
115+
// Children of `node`.
116+
let mut child = 2 * node + 1;
117+
if child >= v.len() {
118+
break;
119+
}
120+
121+
// Choose the greater child.
122+
if child + 1 < v.len()
123+
&& sol_memcmp(v[child].0.as_ref(), v[child + 1].0.as_ref(), 32) < 0
124+
{
125+
child += 1;
126+
}
127+
128+
// Stop if the invariant holds at `node`.
129+
if sol_memcmp(v[node].0.as_ref(), v[child].0.as_ref(), 32) >= 0 {
130+
break;
131+
}
132+
133+
// Swap `node` with the greater child, move one step down, and continue sifting.
134+
v.swap(node, child);
135+
node = child;
136+
}
137+
};
138+
139+
// Build the heap in linear time.
140+
for i in (0..v.len() / 2).rev() {
141+
sift_down(v, i);
142+
}
143+
144+
// Pop maximal elements from the heap.
145+
for i in (1..v.len()).rev() {
146+
v.swap(0, i);
147+
sift_down(&mut v[..i], 0);
148+
}
149+
}
150+
151+
/// Sort the publishers price component list in place by performing minimal swaps.
152+
#[inline(always)]
153+
fn sort_publishers(
154+
comps: &mut [PriceComponent],
155+
num_publishers: usize,
156+
) -> Result<(), ProgramError> {
157+
let comps = comps
158+
.get_mut(..num_publishers)
159+
.ok_or(ProgramError::InvalidArgument)?;
160+
161+
let mut keys = comps
162+
.iter()
163+
.enumerate()
164+
.map(|(i, x)| (x.pub_, i))
165+
.collect::<Vec<_>>();
166+
167+
heapsort(&mut keys);
168+
169+
for i in 0..num_publishers {
170+
// We know that the publisher with key[i].0 should be at index i in the sorted array but we
171+
// do not know where it is as we are doing an in-place sort. Normally, the publisher at
172+
// key[i].0 should be at key[i].1 but if it is swapped, we need to find the correct index.
173+
//
174+
// The way it is done is via satisfying the invariant that for the publisher at index j <
175+
// i, the publisher at index key[j].1 is the index that their publisher was at before the
176+
// swap and the elements j >= i are not swapped if they are not equal to any of the keys
177+
// 0..(i-1). This way, for the publisher at index i we can find the correct index by
178+
// following the chain of swaps and finally set the key[i].1 to the correct index to
179+
// satisfy the invariant.
180+
let mut index = keys[i].1;
181+
182+
while index < i {
183+
index = keys[index].1;
184+
}
185+
keys[i].1 = index;
186+
comps.swap(i, index);
187+
}
188+
189+
Ok(())
190+
}

program/rust/src/processor/upd_price.rs

+122-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use {
22
crate::{
33
accounts::{
44
PriceAccount,
5+
PriceComponent,
56
PriceInfo,
67
PythOracleSerialize,
78
UPD_PRICE_WRITE_SEED,
@@ -31,6 +32,7 @@ use {
3132
},
3233
program::invoke_signed,
3334
program_error::ProgramError,
35+
program_memory::sol_memcmp,
3436
pubkey::Pubkey,
3537
sysvar::Sysvar,
3638
},
@@ -127,7 +129,7 @@ pub fn upd_price(
127129
// Check clock
128130
let clock = Clock::from_account_info(clock_account)?;
129131

130-
let mut publisher_index: usize = 0;
132+
let publisher_index: usize;
131133
let latest_aggregate_price: PriceInfo;
132134

133135
// The price_data borrow happens in a scope because it must be
@@ -137,17 +139,15 @@ pub fn upd_price(
137139
// Verify that symbol account is initialized
138140
let price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
139141

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;
142+
publisher_index = match find_publisher_index(
143+
&price_data.comp_[..try_convert::<u32, usize>(price_data.num_)?],
144+
funding_account.key,
145+
) {
146+
Some(index) => index,
147+
None => {
148+
return Err(OracleError::PermissionViolation.into());
144149
}
145-
publisher_index += 1;
146-
}
147-
pyth_assert(
148-
publisher_index < try_convert::<u32, usize>(price_data.num_)?,
149-
OracleError::PermissionViolation.into(),
150-
)?;
150+
};
151151

152152
latest_aggregate_price = price_data.agg_;
153153
let latest_publisher_price = price_data.comp_[publisher_index].latest_;
@@ -281,6 +281,62 @@ pub fn upd_price(
281281
Ok(())
282282
}
283283

284+
/// Find the index of the publisher in the list of components.
285+
///
286+
/// This method first tries to binary search for the publisher's key in the list of components
287+
/// to get the result faster if the list is sorted. If the list is not sorted, it falls back to
288+
/// a linear search.
289+
#[inline(always)]
290+
fn find_publisher_index(comps: &[PriceComponent], key: &Pubkey) -> Option<usize> {
291+
// Verify that publisher is authorized by initially binary searching
292+
// for the publisher's component in the price account. The binary
293+
// search might not work if the publisher list is not sorted; therefore
294+
// we fall back to a linear search.
295+
let mut binary_search_result = None;
296+
297+
{
298+
// Binary search to find the publisher key. Rust std binary search is not used because
299+
// they guarantee valid outcome only if the array is sorted whereas we want to rely on
300+
// a Equal match if it is a result on an unsorted array. Currently the rust
301+
// implementation behaves the same but we do not want to rely on api internals.
302+
let mut left = 0;
303+
let mut right = comps.len();
304+
while left < right {
305+
let mid = left + (right - left) / 2;
306+
match sol_memcmp(comps[mid].pub_.as_ref(), key.as_ref(), 32) {
307+
i if i < 0 => {
308+
left = mid + 1;
309+
}
310+
i if i > 0 => {
311+
right = mid;
312+
}
313+
_ => {
314+
binary_search_result = Some(mid);
315+
break;
316+
}
317+
}
318+
}
319+
}
320+
321+
match binary_search_result {
322+
Some(index) => Some(index),
323+
None => {
324+
let mut index = 0;
325+
while index < comps.len() {
326+
if sol_memcmp(comps[index].pub_.as_ref(), key.as_ref(), 32) == 0 {
327+
break;
328+
}
329+
index += 1;
330+
}
331+
if index == comps.len() {
332+
None
333+
} else {
334+
Some(index)
335+
}
336+
}
337+
}
338+
}
339+
284340
#[allow(dead_code)]
285341
// Wrapper struct for the accounts required to add data to the accumulator program.
286342
struct MessageBufferAccounts<'a, 'b: 'a> {
@@ -289,3 +345,58 @@ struct MessageBufferAccounts<'a, 'b: 'a> {
289345
oracle_auth_pda: &'a AccountInfo<'b>,
290346
message_buffer_data: &'a AccountInfo<'b>,
291347
}
348+
349+
#[cfg(test)]
350+
mod test {
351+
use {
352+
super::*,
353+
crate::accounts::PriceComponent,
354+
solana_program::pubkey::Pubkey,
355+
};
356+
357+
fn dummy_price_component(pub_: Pubkey) -> PriceComponent {
358+
let dummy_price_info = PriceInfo {
359+
price_: 0,
360+
conf_: 0,
361+
status_: 0,
362+
pub_slot_: 0,
363+
corp_act_status_: 0,
364+
};
365+
366+
PriceComponent {
367+
latest_: dummy_price_info,
368+
agg_: dummy_price_info,
369+
pub_,
370+
}
371+
}
372+
373+
/// Test the find_publisher_index method works with an unordered list of components.
374+
#[test]
375+
pub fn test_find_publisher_index_unordered_comp() {
376+
let comps = (0..64)
377+
.map(|_| dummy_price_component(Pubkey::new_unique()))
378+
.collect::<Vec<_>>();
379+
380+
comps.iter().enumerate().for_each(|(idx, comp)| {
381+
assert_eq!(find_publisher_index(&comps, &comp.pub_), Some(idx));
382+
});
383+
384+
assert_eq!(find_publisher_index(&comps, &Pubkey::new_unique()), None);
385+
}
386+
387+
/// Test the find_publisher_index method works with a sorted list of components.
388+
#[test]
389+
pub fn test_find_publisher_index_ordered_comp() {
390+
let mut comps = (0..64)
391+
.map(|_| dummy_price_component(Pubkey::new_unique()))
392+
.collect::<Vec<_>>();
393+
394+
comps.sort_by_key(|comp| comp.pub_);
395+
396+
comps.iter().enumerate().for_each(|(idx, comp)| {
397+
assert_eq!(find_publisher_index(&comps, &comp.pub_), Some(idx));
398+
});
399+
400+
assert_eq!(find_publisher_index(&comps, &Pubkey::new_unique()), None);
401+
}
402+
}

0 commit comments

Comments
 (0)