Skip to content

Commit 1e17559

Browse files
committed
feat: support zero confirmation for aggregation
1 parent 8cfa4d2 commit 1e17559

File tree

9 files changed

+240
-15
lines changed

9 files changed

+240
-15
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

program/c/src/oracle/oracle.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,9 @@ typedef struct pc_price
193193
uint8_t min_pub_; // min publishers for valid price
194194
int8_t message_sent_; // flag to indicate if the current aggregate price has been sent as a message to the message buffer, 0 if not sent, 1 if sent
195195
uint8_t max_latency_; // configurable max latency in slots between send and receive
196-
int8_t drv3_; // space for future derived values
197-
int32_t drv4_; // space for future derived values
196+
uint8_t flags; // Various bit flags. See PriceAccountFlags rust struct for more details.
197+
// 0: ACCUMULATOR_V2, 1: MESSAGE_BUFFER_CLEARED 2: ALLOW_ZERO_CI
198+
uint32_t feed_index; // Globally unique feed index for this price feed
198199
pc_pub_key_t prod_; // product id/ref-account
199200
pc_pub_key_t next_; // next price account in list
200201
uint64_t prev_slot_; // valid slot of previous aggregate with TRADING status

program/c/src/oracle/upd_aggregate.h

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ static inline bool upd_aggregate( pc_price_t *ptr, uint64_t slot, int64_t timest
155155
uint32_t numv = 0;
156156
uint32_t nprcs = (uint32_t)0;
157157
int64_t prcs[ PC_NUM_COMP * 3 ]; // ~0.75KiB for current PC_NUM_COMP (FIXME: DOUBLE CHECK THIS FITS INTO STACK FRAME LIMIT)
158+
bool allow_zero_ci = (ptr->flags & 0x4) != 0;
159+
158160
for ( uint32_t i = 0; i != ptr->num_; ++i ) {
159161
pc_price_comp_t *iptr = &ptr->comp_[i];
160162
// copy contributing price to aggregate snapshot
@@ -165,9 +167,10 @@ static inline bool upd_aggregate( pc_price_t *ptr, uint64_t slot, int64_t timest
165167
int64_t conf = ( int64_t )( iptr->agg_.conf_ );
166168
int64_t max_latency = ptr->max_latency_ ? ptr->max_latency_ : PC_MAX_SEND_LATENCY;
167169
if ( iptr->agg_.status_ == PC_STATUS_TRADING &&
168-
// No overflow for INT64_MIN+conf or INT64_MAX-conf as 0 < conf < INT64_MAX
169-
// These checks ensure that price - conf and price + conf do not overflow.
170-
(int64_t)0 < conf && (INT64_MIN + conf) <= price && price <= (INT64_MAX-conf) &&
170+
// Only accept confidence of zero if the flag is set
171+
(allow_zero_ci || conf > 0) &&
172+
// these checks ensure that price - conf and price + conf do not overflow.
173+
(INT64_MIN + conf) <= price && price <= (INT64_MAX-conf) &&
171174
// slot_diff is implicitly >= 0 due to the check in Rust code ensuring publishing_slot is always less than or equal to the current slot.
172175
slot_diff <= max_latency ) {
173176
numv += 1;
@@ -201,10 +204,9 @@ static inline bool upd_aggregate( pc_price_t *ptr, uint64_t slot, int64_t timest
201204
// use the larger of the left and right confidences
202205
agg_conf = agg_conf_right > agg_conf_left ? agg_conf_right : agg_conf_left;
203206

204-
// if the confidences end up at zero, we abort
205-
// this is paranoia as it is currently not possible when nprcs>2 and
206-
// positive confidences given the current pricing model
207-
if( agg_conf <= (int64_t)0 ) {
207+
// when zero CI is not allowed, the confidence should not be zero.
208+
// and this check is not necessary, but we do it anyway to be safe.
209+
if( (!allow_zero_ci) && agg_conf <= (int64_t)0 ) {
208210
ptr->agg_.status_ = PC_STATUS_UNKNOWN;
209211
return false;
210212
}

program/rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-oracle"
3-
version = "2.34.0"
3+
version = "2.35.0"
44
edition = "2021"
55
license = "Apache 2.0"
66
publish = false

program/rust/src/accounts/price.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ mod price_pythnet {
108108
/// If unset, the program will remove old messages from its message buffer account
109109
/// and set this flag.
110110
const MESSAGE_BUFFER_CLEARED = 0b10;
111+
/// If set, the program allows publishing of zero confidence interval updates.
112+
const ALLOW_ZERO_CI = 0b100;
111113
}
112114
}
113115

program/rust/src/processor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ mod upd_product;
4141

4242
#[cfg(test)]
4343
pub use add_publisher::{
44+
ALLOW_ZERO_CI,
4445
DISABLE_ACCUMULATOR_V2,
4546
ENABLE_ACCUMULATOR_V2,
47+
FORBID_ZERO_CI,
4648
};
4749
use solana_program::{
4850
program_error::ProgramError,

program/rust/src/processor/add_publisher.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ pub const ENABLE_ACCUMULATOR_V2: [u8; 32] = [
4040
pub const DISABLE_ACCUMULATOR_V2: [u8; 32] = [
4141
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
4242
];
43+
pub const ALLOW_ZERO_CI: [u8; 32] = [
44+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
45+
];
46+
pub const FORBID_ZERO_CI: [u8; 32] = [
47+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
48+
];
4349

4450
/// Add publisher to symbol account
4551
// account[0] funding account [signer writable]
@@ -73,18 +79,23 @@ pub fn add_publisher(
7379

7480
let mut price_data = load_checked::<PriceAccount>(price_account, cmd_args.header.version)?;
7581

82+
// Hack: we use add_publisher instruction to configure the price feeds for some operations.
83+
// This is mostly because we are constrained on contract size and can't add separate
84+
// instructions for these operations.
7685
if cmd_args.publisher == Pubkey::from(ENABLE_ACCUMULATOR_V2) {
77-
// Hack: we use add_publisher instruction to configure the `ACCUMULATOR_V2` flag. Using a new
78-
// instruction would be cleaner but it would require more work in the tooling.
79-
// These special cases can be removed along with the v1 aggregation code once the transition
80-
// is complete.
8186
price_data.flags.insert(PriceAccountFlags::ACCUMULATOR_V2);
8287
return Ok(());
8388
} else if cmd_args.publisher == Pubkey::from(DISABLE_ACCUMULATOR_V2) {
8489
price_data
8590
.flags
8691
.remove(PriceAccountFlags::ACCUMULATOR_V2 | PriceAccountFlags::MESSAGE_BUFFER_CLEARED);
8792
return Ok(());
93+
} else if cmd_args.publisher == Pubkey::from(ALLOW_ZERO_CI) {
94+
price_data.flags.insert(PriceAccountFlags::ALLOW_ZERO_CI);
95+
return Ok(());
96+
} else if cmd_args.publisher == Pubkey::from(FORBID_ZERO_CI) {
97+
price_data.flags.remove(PriceAccountFlags::ALLOW_ZERO_CI);
98+
return Ok(());
8899
}
89100

90101
if price_data.num_ >= PC_NUM_COMP {

program/rust/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod test_add_product;
44
mod test_add_publisher;
55
mod test_aggregate_v2;
66
mod test_aggregation;
7+
mod test_aggregation_zero_conf;
78
mod test_c_code;
89
mod test_check_valid_signable_account_or_permissioned_funding_account;
910
mod test_del_price;
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
use {
2+
crate::{
3+
accounts::{
4+
PermissionAccount,
5+
PriceAccount,
6+
PriceAccountFlags,
7+
PythAccount,
8+
},
9+
c_oracle_header::{
10+
PC_STATUS_TRADING,
11+
PC_STATUS_UNKNOWN,
12+
PC_VERSION,
13+
},
14+
deserialize::{
15+
load_checked,
16+
load_mut,
17+
},
18+
instruction::{
19+
AddPublisherArgs,
20+
OracleCommand,
21+
UpdPriceArgs,
22+
},
23+
processor::{
24+
process_instruction,
25+
ALLOW_ZERO_CI,
26+
FORBID_ZERO_CI,
27+
},
28+
tests::test_utils::{
29+
update_clock_slot,
30+
AccountSetup,
31+
},
32+
},
33+
bytemuck::bytes_of,
34+
solana_program::pubkey::Pubkey,
35+
std::mem::size_of,
36+
};
37+
38+
struct Accounts {
39+
program_id: Pubkey,
40+
publisher_account: AccountSetup,
41+
funding_account: AccountSetup,
42+
price_account: AccountSetup,
43+
permissions_account: AccountSetup,
44+
clock_account: AccountSetup,
45+
}
46+
47+
impl Accounts {
48+
fn new() -> Self {
49+
let program_id = Pubkey::new_unique();
50+
let publisher_account = AccountSetup::new_funding();
51+
let clock_account = AccountSetup::new_clock();
52+
let mut funding_account = AccountSetup::new_funding();
53+
let mut permissions_account = AccountSetup::new_permission(&program_id);
54+
let mut price_account = AccountSetup::new::<PriceAccount>(&program_id);
55+
56+
PriceAccount::initialize(&price_account.as_account_info(), PC_VERSION).unwrap();
57+
58+
{
59+
let permissions_account_info = permissions_account.as_account_info();
60+
let mut permissions_account_data =
61+
PermissionAccount::initialize(&permissions_account_info, PC_VERSION).unwrap();
62+
permissions_account_data.master_authority = *funding_account.as_account_info().key;
63+
permissions_account_data.data_curation_authority =
64+
*funding_account.as_account_info().key;
65+
permissions_account_data.security_authority = *funding_account.as_account_info().key;
66+
}
67+
68+
Self {
69+
program_id,
70+
publisher_account,
71+
funding_account,
72+
price_account,
73+
permissions_account,
74+
clock_account,
75+
}
76+
}
77+
}
78+
79+
fn add_publisher(accounts: &mut Accounts, publisher: Option<Pubkey>) {
80+
let args = AddPublisherArgs {
81+
header: OracleCommand::AddPublisher.into(),
82+
publisher: publisher.unwrap_or(*accounts.publisher_account.as_account_info().key),
83+
};
84+
85+
assert!(process_instruction(
86+
&accounts.program_id,
87+
&[
88+
accounts.funding_account.as_account_info(),
89+
accounts.price_account.as_account_info(),
90+
accounts.permissions_account.as_account_info(),
91+
],
92+
bytes_of::<AddPublisherArgs>(&args)
93+
)
94+
.is_ok());
95+
}
96+
97+
fn update_price(accounts: &mut Accounts, price: i64, conf: u64, slot: u64) {
98+
let instruction_data = &mut [0u8; size_of::<UpdPriceArgs>()];
99+
let mut cmd = load_mut::<UpdPriceArgs>(instruction_data).unwrap();
100+
cmd.header = OracleCommand::UpdPrice.into();
101+
cmd.status = PC_STATUS_TRADING;
102+
cmd.price = price;
103+
cmd.confidence = conf;
104+
cmd.publishing_slot = slot;
105+
cmd.unused_ = 0;
106+
107+
let mut clock = accounts.clock_account.as_account_info();
108+
clock.is_signer = false;
109+
clock.is_writable = false;
110+
111+
process_instruction(
112+
&accounts.program_id,
113+
&[
114+
accounts.publisher_account.as_account_info(),
115+
accounts.price_account.as_account_info(),
116+
clock,
117+
],
118+
instruction_data,
119+
)
120+
.unwrap();
121+
}
122+
123+
#[test]
124+
fn test_aggregate_v2_toggle() {
125+
let accounts = &mut Accounts::new();
126+
127+
// Add an initial Publisher to test with.
128+
add_publisher(accounts, None);
129+
130+
// Update the price, no aggregation will happen on the first slot.
131+
{
132+
update_clock_slot(&mut accounts.clock_account.as_account_info(), 1);
133+
update_price(accounts, 42, 2, 1);
134+
let info = accounts.price_account.as_account_info();
135+
let price_data = load_checked::<PriceAccount>(&info, PC_VERSION).unwrap();
136+
assert_eq!(price_data.last_slot_, 0);
137+
assert!(!price_data.flags.contains(PriceAccountFlags::ACCUMULATOR_V2));
138+
}
139+
140+
// Update again, component is now TRADING so aggregation should trigger.
141+
{
142+
update_clock_slot(&mut accounts.clock_account.as_account_info(), 2);
143+
update_price(accounts, 43, 3, 2);
144+
let info = accounts.price_account.as_account_info();
145+
let price_data = load_checked::<PriceAccount>(&info, PC_VERSION).unwrap();
146+
assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING);
147+
assert_eq!(price_data.last_slot_, 2);
148+
assert_eq!(price_data.agg_.price_, 42);
149+
assert_eq!(price_data.agg_.conf_, 2);
150+
}
151+
152+
// Update again, but with confidence set to 0, it should not aggregate *in the next slot*.
153+
{
154+
update_clock_slot(&mut accounts.clock_account.as_account_info(), 3);
155+
update_price(accounts, 44, 0, 3);
156+
let info = accounts.price_account.as_account_info();
157+
let price_data = load_checked::<PriceAccount>(&info, PC_VERSION).unwrap();
158+
assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING);
159+
assert_eq!(price_data.last_slot_, 3);
160+
assert_eq!(price_data.agg_.price_, 43);
161+
assert_eq!(price_data.agg_.conf_, 3);
162+
}
163+
164+
// Update again, to trigger aggregation. We should see status go to unknown
165+
{
166+
update_clock_slot(&mut accounts.clock_account.as_account_info(), 4);
167+
update_price(accounts, 45, 0, 4);
168+
let info = accounts.price_account.as_account_info();
169+
let price_data = load_checked::<PriceAccount>(&info, PC_VERSION).unwrap();
170+
println!("Price Data: {:?}", price_data.agg_);
171+
assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN);
172+
assert_eq!(price_data.last_slot_, 3);
173+
}
174+
175+
// Enable allow zero confidence bit
176+
add_publisher(accounts, Some(ALLOW_ZERO_CI.into()));
177+
178+
// Update again, with allow zero confidence bit set, aggregation should support
179+
// zero confidence values. Note that we don't need to do this twice, because the
180+
// price with ci zero was already stored in the previous slot.
181+
{
182+
update_clock_slot(&mut accounts.clock_account.as_account_info(), 5);
183+
update_price(accounts, 46, 0, 5);
184+
let info = accounts.price_account.as_account_info();
185+
let price_data = load_checked::<PriceAccount>(&info, PC_VERSION).unwrap();
186+
assert!(price_data.flags.contains(PriceAccountFlags::ALLOW_ZERO_CI));
187+
assert_eq!(price_data.agg_.status_, PC_STATUS_TRADING);
188+
assert_eq!(price_data.last_slot_, 5);
189+
assert_eq!(price_data.agg_.price_, 45);
190+
assert_eq!(price_data.agg_.conf_, 0);
191+
}
192+
193+
// Disable allow zero confidence bit
194+
add_publisher(accounts, Some(FORBID_ZERO_CI.into()));
195+
196+
// Update again, with forbid zero confidence bit set, aggregation should have status
197+
// of unknown
198+
{
199+
update_clock_slot(&mut accounts.clock_account.as_account_info(), 6);
200+
update_price(accounts, 47, 0, 6);
201+
let info = accounts.price_account.as_account_info();
202+
let price_data = load_checked::<PriceAccount>(&info, PC_VERSION).unwrap();
203+
assert!(!price_data.flags.contains(PriceAccountFlags::ALLOW_ZERO_CI));
204+
assert_eq!(price_data.agg_.status_, PC_STATUS_UNKNOWN);
205+
}
206+
}

0 commit comments

Comments
 (0)