Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 2 additions & 25 deletions oracle/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,7 @@ pub fn load_all_assets(e: &Env) -> Vec<Asset> {

// Load asset index
pub fn resolve_asset_index(e: &Env, asset: &Asset) -> Option<u32> {
let index: Option<u32>;
match asset {
Asset::Stellar(address) => {
index = e.storage().instance().get(&address);
}
Asset::Other(symbol) => {
index = e.storage().instance().get(&symbol);
}
}
index
load_all_assets(e).first_index_of(asset)
}

// Add assets to the oracle
Expand All @@ -49,10 +40,9 @@ pub fn add_assets(e: &Env, assets: Vec<Asset>, initial_expiration_period: u32) {
//for each new asset
for asset in assets.iter() {
//check if the asset has been already added
if resolve_asset_index(e, &asset).is_some() {
if asset_list.first_index_of(&asset).is_some() {
panic_with_error!(&e, Error::AssetAlreadyExists);
}
set_asset_index(e, &asset, asset_list.len());
asset_list.push_back(asset);
//update expiration records
expiration.push_back(expiration_timestamp);
Expand Down Expand Up @@ -158,16 +148,3 @@ fn load_expiration_records(e: &Env) -> Vec<u64> {
fn set_expirations_records(e: &Env, expiration: &Vec<u64>) {
e.storage().instance().set(&EXPIRATION_KEY, expiration)
}

// Store asset index
#[inline]
fn set_asset_index(e: &Env, asset: &Asset, index: u32) {
match asset {
Asset::Stellar(address) => {
e.storage().instance().set(&address, &index);
}
Asset::Other(symbol) => {
e.storage().instance().set(&symbol, &index);
}
}
}
88 changes: 49 additions & 39 deletions oracle/src/mapping.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,67 @@
use soroban_sdk::{Bytes, Env, Vec, U256};
use soroban_sdk::{Bytes, Vec};

// Each history record occupies 32 bytes in history mask, allowing to store information for up to 256 recent periods
const RECORD_SIZE: u32 = 32;
const URECORD_SIZE: usize = 32;
const MAX_HISTORY_SIZE: usize = 256 * URECORD_SIZE; // 256 assets * 32 bytes

// Update history records containing a bitmask of all prices recorded within the last update period
pub fn update_history_mask(
e: &Env,
mut history_mask: Bytes,
history_mask: Bytes,
updates: &Vec<i128>,
mut updates_delta: u32,
) -> Bytes {
let one = U256::from_u32(e, 1);
//wipe entire history if the gap between updates is too large
if updates_delta > 255 {
history_mask = Bytes::new(e); //start with an empty mask
updates_delta = 1;
}
//create a buffer that can hold the entire history mask
let mut buffer = [0u8; MAX_HISTORY_SIZE];
let mask_length = history_mask.len() as usize;

if updates_delta < 1 {
updates_delta = 1; //this should never happen, but just in case
}
//iterate through all updates
if updates_delta > 255 {
//entire history is obsolete - ignore
updates_delta = 1; //reset delta to 1
} else {
//copy existing history mask into buffer
history_mask.copy_into_slice(&mut buffer[..mask_length]);
}
//iterate through all updates and update corresponding history records in the buffer
for (asset_index, price) in updates.iter().enumerate() {
//locate particular asset mask slice position within entire history record
let from = asset_index as u32 * RECORD_SIZE;
let to = from + RECORD_SIZE;
//retrieve previous asset mask
let mut bitmask = if history_mask.len() >= to {
let encoded = history_mask.slice(from..to);
U256::from_be_bytes(e, &encoded)
} else {
U256::from_u32(e, 0) //no previous records for this asset found
};
//shift existing mask to the left by the number of periods since the last update
//all mask bits older than 256 periods get evicted
bitmask = bitmask.shl(updates_delta);
//set corresponding bit if price found
if price > 0 {
bitmask = bitmask.add(&one);
//position in the mask
let offset = asset_index * URECORD_SIZE;

//256 bits as two 128 parts
let mut hi = u128::from_be_bytes(buffer[offset..offset + 16].try_into().unwrap());
let mut lo = u128::from_be_bytes(buffer[offset + 16..offset + 32].try_into().unwrap());

if lo > 0 || hi > 0 {
//shift left by the number of skipped periods
(hi, lo) = if updates_delta < 128 {
(
(hi << updates_delta) | (lo >> (128 - updates_delta)),
lo << updates_delta,
)
} else {
(lo << (updates_delta & 0x7f), 0)
};
}
//encode into bytes again
let encoded = bitmask.to_be_bytes();
//write to the history
if history_mask.len() <= from {
//that's new asset, add to the mask
history_mask.append(&encoded);
} else {
//replace bytes
for i in 0..RECORD_SIZE {
history_mask.set(from + i, encoded.get(i).unwrap());

//set lowest bit if price found
if price > 0 {
let (new_lo, carry) = lo.overflowing_add(1);
lo = new_lo;
if carry {
(hi, _) = hi.overflowing_add(1);
}
}
//write back to buffer
buffer[offset..offset + 16].copy_from_slice(&hi.to_be_bytes());
buffer[offset + 16..offset + 32].copy_from_slice(&lo.to_be_bytes());
}
history_mask //return updated history

//get total size of updated history mask based on the number of assets and return as Bytes
let updates_length = mask_length.max(updates.len() as usize * URECORD_SIZE);
Bytes::from_slice(history_mask.env(), &buffer[..updates_length])
}

// Check whether asset price has been quoted for a certain period based on history records bitmask
Expand All @@ -61,9 +71,9 @@ pub fn check_history_updated(history_mask: &Bytes, asset_index: u32, period: u32
//and calculate specific bit that we need to check
let bit = 1 << (period % 8);
//retrieve byte from array
let bytemask = history_mask.get(from).unwrap_or_default();
let encoded_byte = history_mask.get(from).unwrap_or_default();
//compare with bit mask
bytemask & bit == bit
encoded_byte & bit == bit
}

// Check whether price update record contains update for given asset by its index
Expand Down
2 changes: 1 addition & 1 deletion oracle/src/price_oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ impl PriceOracleContractBase {
//prepare and publish update event
events::publish_update_event(e, &asset_prices, &all, timestamp);
//store new prices
prices::store_prices(e, &update, timestamp, &asset_prices);
prices::store_prices(e, update, timestamp, asset_prices);
}

// Update contract source code
Expand Down
9 changes: 5 additions & 4 deletions oracle/src/prices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub fn update_history_mask(e: &Env, prices: &Vec<i128>, timestamp: u64) {
}

//update the position mask
history_map = mapping::update_history_mask(e, history_map, prices, update_delta as u32);
history_map = mapping::update_history_mask(history_map, prices, update_delta as u32);
//store updated timestamps
e.storage().instance().set(&HISTORY_KEY, &history_map);
}
Expand All @@ -150,7 +150,7 @@ pub fn load_history_record(e: &Env, timestamp: u64) -> Option<PriceUpdate> {
}

// Update prices stored in the oracle
pub fn store_prices(e: &Env, update: &PriceUpdate, timestamp: u64, update_v1: &Vec<i128>) {
pub fn store_prices(e: &Env, update: PriceUpdate, timestamp: u64, update_v1: Vec<i128>) {
//validate timestamp
let ledger_timestamp = timestamps::ledger_timestamp(&e);
let last_timestamp = get_last_timestamp(e);
Expand All @@ -172,7 +172,7 @@ pub fn store_prices(e: &Env, update: &PriceUpdate, timestamp: u64, update_v1: &V
if cache_size > 0 {
//if cache size is non-empty, store it in the instance
let mut cache = load_price_records_cache(e).unwrap_or(Vec::new(&e));
cache.push_front((timestamp, update.clone()));
cache.push_front((timestamp, update));
while cache.len() > cache_size {
cache.pop_back(); //remove the oldest record if cache size exceeded
}
Expand Down Expand Up @@ -216,6 +216,7 @@ pub fn load_prices(e: &Env, asset_index: u32, records: u32) -> Option<Vec<PriceD
//continue to iterate until the last record with ts<=lower_boundary found
//(required for further interpolation if the value at lower_boundary is not available)
while last_included > lower_boundary {
//TODO: Load `history_map` and `cache` once outside the loop
//invoke price fetch callback for each record
if let Some(price) = retrieve_asset_price_data(e, asset_index, timestamp) {
prices.push_back(price);
Expand All @@ -240,7 +241,7 @@ fn load_price_records_cache(e: &Env) -> Option<Vec<(u64, PriceUpdate)>> {
}

// Update price in legacy format (deprecated)
pub fn store_price_v1(e: &Env, updates: &Vec<i128>, timestamp: u64, ledgers_to_live: u32) {
pub fn store_price_v1(e: &Env, updates: Vec<i128>, timestamp: u64, ledgers_to_live: u32) {
//iterate over the updates
for (i, price) in updates.iter().enumerate() {
//ignore zero prices
Expand Down
7 changes: 5 additions & 2 deletions oracle/src/tests/fetch_prices_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn store_prices_test(
set_ledger_timestamp(&e, 600_000);

let mut assets = Vec::new(&e);
for i in 0..10 {
for i in 0..255 {
assets.push_back(types::Asset::Other(Symbol::new(
&e,
&("ASSET_".to_string() + &i.to_string()),
Expand All @@ -65,9 +65,10 @@ fn store_prices_test(
fn set_price(e: &Env, timestamp: u64, assets: &Vec<types::Asset>) {
let updates = generate_updates(e, &assets, 100);
let asset_prices = prices::extract_update_record_prices(e, &updates, assets.len());
let legacy_update = updates.prices.clone();
//store history timestamps for all assets
prices::update_history_mask(e, &asset_prices, timestamp);
prices::store_prices(e, &updates, timestamp, &updates.prices.clone());
prices::store_prices(e, updates, timestamp, legacy_update);
}

let mut timestamp = first_timestamp;
Expand All @@ -93,4 +94,6 @@ fn store_prices_test(
expected_first_price_ts / 1000
);
});

e.cost_estimate().budget().print();
}
12 changes: 9 additions & 3 deletions oracle/src/tests/prices_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@ fn invalid_timestamp_update_test(ts: u64) {
&e,
types::PriceUpdate {
prices: vec![&e, 12345678i128],
mask: generate_update_record_mask(&e, &vec![&e, 12345678i128]),
mask: generate_update_record_mask(
&e,
&std::collections::VecDeque::from([12345678i128]),
),
},
ts,
);
});
}

#[test]
fn price_update_test() {
fn single_price_update_test() {
let e = Env::default();
//register contract to have storage available
let contract = e.register_stellar_asset_contract_v2(Address::generate(&e));
Expand Down Expand Up @@ -79,7 +82,10 @@ fn price_update_test() {
&e,
types::PriceUpdate {
prices: vec![&e, 12345678i128],
mask: generate_update_record_mask(&e, &vec![&e, 12345678i128]),
mask: generate_update_record_mask(
&e,
&std::collections::VecDeque::from([12345678i128]),
),
},
900_000,
);
Expand Down
8 changes: 4 additions & 4 deletions oracle/src/tests/util_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn position_encoding_bitmask_test() {
};
updates.push_back(price);
}
mask = mapping::update_history_mask(&e, mask, &updates, 1);
mask = mapping::update_history_mask(mask, &updates, 1);
}
log!(&e, "entire mask", mask);

Expand All @@ -70,21 +70,21 @@ fn update_record_bitmask_test() {
let e = Env::default();
let iterations = 70;

let mut updates = Vec::from_array(&e, [0i128; 254]);
let mut updates = std::collections::VecDeque::from([0i128; 254]);
for i in 0..iterations {
for asset_index in 0..updates.len() {
let price = match i & asset_index == 0 {
true => 1,
_ => 0,
};
updates.set(asset_index, price);
updates[asset_index] = price;
}
let mask = generate_update_record_mask(&e, &updates);
//log!(&e, "entire mask", mask);
for (asset_index, price) in updates.iter().enumerate() {
assert_eq!(
mapping::check_period_updated(&mask, asset_index as u32),
price > 0
price > &0
);
}
}
Expand Down
Loading