Skip to content

Commit 8301f05

Browse files
authored
Sma3 (#288)
* Pseudocode * Pseudocode * Check * Wrote tests * some comments * Cleanup * Should pass CI * Starting tests * Update python tests * Test upd sma * Another test * More comments * New implementation * Cleanup * Add comment * Renames * Cleanup * Cleanup * New implementation * New implementation * Comments * Some cleanup * Gate everything by test flags * PR feedback * Fix comments * Feedback * Add comment * First epoch is invalid always * Remove files * Comment test flags
1 parent 21cc8ef commit 8301f05

File tree

10 files changed

+1209
-27
lines changed

10 files changed

+1209
-27
lines changed

program/c/src/oracle/oracle.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ extern "C" {
99

1010
// The size of the "time machine" account defined in the
1111
// Rust portion of the codebase.
12-
const uint64_t TIME_MACHINE_STRUCT_SIZE = 1864ULL;
12+
const uint64_t TIME_MACHINE_STRUCT_SIZE = 1200ULL;
1313

14-
const uint64_t EXTRA_PUBLISHER_SPACE = 1000ULL;
14+
const uint64_t EXTRA_PUBLISHER_SPACE = 3072ULL;
1515

1616

1717
// magic number at head of account

program/rust/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ solana-program-test = "=1.10.29"
2121
solana-sdk = "=1.10.29"
2222
tokio = "1.14.1"
2323
hex = "0.3.1"
24+
quickcheck = "1"
25+
quickcheck_macros = "1"
2426

2527
[features]
2628
debug = []

program/rust/src/tests/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ mod test_init_price;
1111
mod test_resize_account;
1212
mod test_set_min_pub;
1313
mod test_sizes;
14+
mod test_sma;
15+
mod test_sma_epoch_transition;
1416
mod test_upd_aggregate;
1517
mod test_upd_permissions;
1618
mod test_upd_price;
1719
mod test_upd_price_no_fail_on_error;
1820
mod test_upd_product;
21+
mod test_upd_sma;
1922
mod test_utils;

program/rust/src/tests/test_resize_account.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,10 @@ async fn test_resize_account() {
2828
assert!(sim.resize_price_account(&price1).await.is_ok());
2929
let price1_account = sim.get_account(price1.pubkey()).await.unwrap();
3030
assert_eq!(price1_account.data.len(), size_of::<PriceAccountWrapper>());
31+
let price1_account_data = sim
32+
.get_account_data_as::<PriceAccountWrapper>(price1.pubkey())
33+
.await
34+
.unwrap();
35+
assert_eq!(price1_account_data.time_machine.granularity, 0);
36+
assert_eq!(price1_account_data.time_machine.threshold, 0);
3137
}

program/rust/src/tests/test_sma.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use quickcheck::Arbitrary;
2+
use quickcheck_macros::quickcheck;
3+
4+
use crate::time_machine_types::{
5+
DataPoint,
6+
SmaTracker,
7+
NUM_BUCKETS_THIRTY_MIN,
8+
};
9+
10+
#[derive(Clone, Debug, Copy)]
11+
struct DataEvent {
12+
time_gap: i64,
13+
slot_gap: u64,
14+
price: i64,
15+
}
16+
17+
impl Arbitrary for DataEvent {
18+
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
19+
DataEvent {
20+
time_gap: i64::from(u8::arbitrary(g)),
21+
slot_gap: u64::from(u8::arbitrary(g)) + 1, /* Slot gap is always > 1, because there
22+
* has been a succesful aggregation */
23+
price: i64::arbitrary(g),
24+
}
25+
}
26+
}
27+
28+
/// This is a generative test for the sma struct. quickcheck will generate a series of
29+
/// vectors of DataEvents of different length. The generation is based on the arbitrary trait
30+
/// above.
31+
/// For each DataEvent :
32+
/// - time_gap is a random number between 0 and u8::MAX (255)
33+
/// - slot_gap is a random number between 1 and u8::MAX + 1 (256)
34+
/// - price is a random i64
35+
#[quickcheck]
36+
fn test_sma(input: Vec<DataEvent>) -> bool {
37+
// No gaps, no skipped epochs
38+
let mut tracker1 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero();
39+
tracker1.initialize(i64::from(u8::MAX), u64::from(u8::MAX));
40+
41+
// Skipped and gaps
42+
let mut tracker2 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero();
43+
tracker2.initialize(i64::from(u8::MAX / 5), u64::from(u8::MAX / 5));
44+
45+
// Gaps, no skips
46+
let mut tracker3 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero();
47+
tracker3.initialize(i64::from(u8::MAX), u64::from(u8::MAX / 5));
48+
49+
// No skips, gaps
50+
let mut tracker4 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero();
51+
tracker4.initialize(i64::from(u8::MAX), u64::from(u8::MAX / 5) * 4);
52+
53+
// Each epoch is 1 second
54+
let mut tracker5 = SmaTracker::<NUM_BUCKETS_THIRTY_MIN>::zero();
55+
tracker5.initialize(1, u64::from(u8::MAX / 5));
56+
57+
let mut data = Vec::<DataPoint>::new();
58+
59+
let mut current_time = 0i64;
60+
for data_event in input.clone() {
61+
let datapoint = DataPoint {
62+
previous_timestamp: current_time,
63+
current_timestamp: current_time + data_event.time_gap,
64+
slot_gap: data_event.slot_gap,
65+
price: data_event.price,
66+
};
67+
68+
tracker1.add_datapoint(&datapoint).unwrap();
69+
tracker2.add_datapoint(&datapoint).unwrap();
70+
tracker3.add_datapoint(&datapoint).unwrap();
71+
tracker4.add_datapoint(&datapoint).unwrap();
72+
tracker5.add_datapoint(&datapoint).unwrap();
73+
data.push(datapoint);
74+
current_time += data_event.time_gap;
75+
76+
tracker1.check_current_epoch_fields(&data, current_time);
77+
tracker2.check_current_epoch_fields(&data, current_time);
78+
tracker3.check_current_epoch_fields(&data, current_time);
79+
tracker4.check_current_epoch_fields(&data, current_time);
80+
tracker5.check_current_epoch_fields(&data, current_time);
81+
tracker1.check_array_fields(&data, current_time);
82+
tracker2.check_array_fields(&data, current_time);
83+
tracker3.check_array_fields(&data, current_time);
84+
tracker4.check_array_fields(&data, current_time);
85+
tracker5.check_array_fields(&data, current_time);
86+
}
87+
88+
return true;
89+
}
90+
91+
92+
impl<const NUM_ENTRIES: usize> SmaTracker<NUM_ENTRIES> {
93+
pub fn zero() -> Self {
94+
return SmaTracker::<NUM_ENTRIES> {
95+
granularity: 0,
96+
threshold: 0,
97+
current_epoch_denominator: 0,
98+
current_epoch_is_valid: false,
99+
current_epoch_numerator: 0,
100+
running_valid_epoch_counter: [0u64; NUM_ENTRIES],
101+
running_sum_of_price_averages: [0i128; NUM_ENTRIES],
102+
};
103+
}
104+
105+
pub fn check_current_epoch_fields(&self, data: &Vec<DataPoint>, time: i64) {
106+
let curent_epoch = self.time_to_epoch(time).unwrap();
107+
108+
let result = self.compute_epoch_expected_values(data, curent_epoch);
109+
assert_eq!(self.current_epoch_denominator, result.0);
110+
assert_eq!(self.current_epoch_numerator, result.1);
111+
assert_eq!(self.current_epoch_is_valid, result.2);
112+
}
113+
114+
pub fn check_array_fields(&self, data: &Vec<DataPoint>, time: i64) {
115+
let current_epoch = self.time_to_epoch(time).unwrap();
116+
let mut values = vec![];
117+
118+
// Compute all epoch averages
119+
for i in 0..current_epoch {
120+
values.push(self.compute_epoch_expected_values(data, i));
121+
}
122+
123+
// Get running sums
124+
let running_sum_price_iter = values.iter().scan((0, 0), |res, &y| {
125+
res.0 = res.0 + y.1 / i128::from(y.0);
126+
res.1 = res.1 + u64::from(y.2);
127+
Some(*res)
128+
});
129+
130+
// Compare to running_sum_of_price_averages
131+
let mut i = (current_epoch + NUM_ENTRIES - 1).rem_euclid(NUM_ENTRIES);
132+
for x in running_sum_price_iter
133+
.collect::<Vec<(i128, u64)>>()
134+
.iter()
135+
.rev()
136+
.take(NUM_ENTRIES)
137+
{
138+
assert_eq!(self.running_sum_of_price_averages[i], x.0);
139+
assert_eq!(self.running_valid_epoch_counter[i], x.1);
140+
i = (i + NUM_ENTRIES - 1).rem_euclid(NUM_ENTRIES);
141+
}
142+
}
143+
144+
pub fn compute_epoch_expected_values(
145+
&self,
146+
data: &Vec<DataPoint>,
147+
epoch_number: usize,
148+
) -> (u64, i128, bool) {
149+
let left_bound = self
150+
.granularity
151+
.checked_mul(epoch_number.try_into().unwrap())
152+
.unwrap();
153+
154+
let right_bound = self
155+
.granularity
156+
.checked_mul((epoch_number + 1).try_into().unwrap())
157+
.unwrap();
158+
159+
160+
let mut result = data.iter().fold((0, 0, true), |x: (u64, i128, bool), y| {
161+
if !((left_bound > y.current_timestamp) || (right_bound <= y.previous_timestamp))
162+
//Check interval intersection
163+
{
164+
let is_valid = y.slot_gap <= self.threshold;
165+
return (
166+
x.0 + y.slot_gap,
167+
x.1 + i128::from(y.slot_gap) * i128::from(y.price),
168+
x.2 && is_valid,
169+
);
170+
}
171+
return x;
172+
});
173+
174+
if epoch_number == 0 {
175+
result.2 = false;
176+
}
177+
return result;
178+
}
179+
}

0 commit comments

Comments
 (0)