[hermes] Add storage tests + refactor (#907)

* [hermes] Add storage tests + refactor

* Bump pythnet_sdk version + update cosmwasm

* Address review feedbacks
This commit is contained in:
Ali Behjati 2023-06-22 14:38:56 +01:00 committed by GitHub
parent c9c20310d3
commit d07cc9d1ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 619 additions and 247 deletions

204
hermes/Cargo.lock generated
View File

@ -674,12 +674,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bytecount"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.13.1" version = "1.13.1"
@ -712,15 +706,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "camino"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "caps" name = "caps"
version = "0.5.5" version = "0.5.5"
@ -731,28 +716,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "cargo-platform"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa"
dependencies = [
"camino",
"cargo-platform",
"semver 1.0.17",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.79" version = "1.0.79"
@ -1367,15 +1330,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "error-chain"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.3" version = "2.5.3"
@ -1639,12 +1593,6 @@ dependencies = [
"polyval", "polyval",
] ]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "gloo-timers" name = "gloo-timers"
version = "0.2.6" version = "0.2.6"
@ -1711,7 +1659,7 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hermes" name = "hermes"
version = "0.1.2" version = "0.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1730,7 +1678,6 @@ dependencies = [
"libc", "libc",
"libp2p", "libp2p",
"log", "log",
"moka",
"pyth-sdk", "pyth-sdk",
"pythnet-sdk", "pythnet-sdk",
"rand 0.8.5", "rand 0.8.5",
@ -2861,15 +2808,6 @@ dependencies = [
"linked-hash-map", "linked-hash-map",
] ]
[[package]]
name = "mach2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "match_cfg" name = "match_cfg"
version = "0.1.0" version = "0.1.0"
@ -2966,32 +2904,6 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "moka"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "934030d03f6191edbb4ba16835ccdb80d560788ac686570a8e2986a0fb59ded8"
dependencies = [
"async-io",
"async-lock",
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"futures-util",
"num_cpus",
"once_cell",
"parking_lot 0.12.1",
"quanta",
"rustc_version 0.4.0",
"scheduled-thread-pool",
"skeptic",
"smallvec",
"tagptr",
"thiserror",
"triomphe",
"uuid",
]
[[package]] [[package]]
name = "multiaddr" name = "multiaddr"
version = "0.13.0" version = "0.13.0"
@ -3776,17 +3688,6 @@ dependencies = [
"prost", "prost",
] ]
[[package]]
name = "pulldown-cmark"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
dependencies = [
"bitflags",
"memchr",
"unicase",
]
[[package]] [[package]]
name = "pyth-sdk" name = "pyth-sdk"
version = "0.7.0" version = "0.7.0"
@ -3802,7 +3703,7 @@ dependencies = [
[[package]] [[package]]
name = "pythnet-sdk" name = "pythnet-sdk"
version = "1.13.6" version = "2.0.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"borsh", "borsh",
@ -3827,22 +3728,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "quanta"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cc73c42f9314c4bdce450c77e6f09ecbddefbeddb1b5979ded332a3913ded33"
dependencies = [
"crossbeam-utils",
"libc",
"mach2",
"once_cell",
"raw-cpuid",
"wasi 0.11.0+wasi-snapshot-preview1",
"web-sys",
"winapi",
]
[[package]] [[package]]
name = "quick-error" name = "quick-error"
version = "1.2.3" version = "1.2.3"
@ -4011,15 +3896,6 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "raw-cpuid"
version = "10.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.7.0" version = "1.7.0"
@ -4323,15 +4199,6 @@ dependencies = [
"cipher 0.4.4", "cipher 0.4.4",
] ]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.21" version = "0.1.21"
@ -4341,15 +4208,6 @@ dependencies = [
"windows-sys 0.42.0", "windows-sys 0.42.0",
] ]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
dependencies = [
"parking_lot 0.12.1",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.8.12" version = "0.8.12"
@ -4447,9 +4305,6 @@ name = "semver"
version = "1.0.17" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "semver-parser" name = "semver-parser"
@ -4681,21 +4536,6 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "skeptic"
version = "0.13.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8"
dependencies = [
"bytecount",
"cargo_metadata",
"error-chain",
"glob",
"pulldown-cmark",
"tempfile",
"walkdir",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.8" version = "0.4.8"
@ -5543,12 +5383,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.5.0" version = "3.5.0"
@ -5881,12 +5715,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "triomphe"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1ee9bd9239c339d714d657fac840c6d2a4f9c45f4f9ec7b0975113458be78db"
[[package]] [[package]]
name = "trust-dns-proto" name = "trust-dns-proto"
version = "0.20.4" version = "0.20.4"
@ -6001,15 +5829,6 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.13" version = "0.3.13"
@ -6110,15 +5929,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
dependencies = [
"getrandom 0.2.9",
]
[[package]] [[package]]
name = "value-bag" name = "value-bag"
version = "1.0.0-alpha.9" version = "1.0.0-alpha.9"
@ -6159,16 +5969,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.0" version = "0.3.0"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "hermes" name = "hermes"
version = "0.1.2" version = "0.1.3"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -35,11 +35,10 @@ libp2p = { version = "0.42.2", features = [
]} ]}
log = { version = "0.4.17" } log = { version = "0.4.17" }
moka = { version = "0.11.0", features = ["future"] }
pyth-sdk = { version = "0.7.0" } pyth-sdk = { version = "0.7.0" }
# Parse Wormhole attester price attestations. # Parse Wormhole attester price attestations.
pythnet-sdk = { path = "../pythnet/pythnet_sdk/", version = "=1.13.6", features = ["strum"] } pythnet-sdk = { path = "../pythnet/pythnet_sdk/", version = "2.0.0", features = ["strum"] }
rand = { version = "0.8.5" } rand = { version = "0.8.5" }
reqwest = { version = "0.11.14", features = ["blocking", "json"] } reqwest = { version = "0.11.14", features = ["blocking", "json"] }

View File

@ -70,7 +70,7 @@ impl RpcPriceFeed {
let price_feed_message = price_feed_update.price_feed; let price_feed_message = price_feed_update.price_feed;
Self { Self {
id: PriceIdentifier::new(price_feed_message.id), id: PriceIdentifier::new(price_feed_message.feed_id),
price: Price { price: Price {
price: price_feed_message.price, price: price_feed_message.price,
conf: price_feed_message.conf, conf: price_feed_message.conf,

View File

@ -165,7 +165,7 @@ impl Subscriber {
{ {
let config = self let config = self
.price_feeds_with_config .price_feeds_with_config
.get(&PriceIdentifier::new(update.price_feed.id)) .get(&PriceIdentifier::new(update.price_feed.feed_id))
.ok_or(anyhow::anyhow!( .ok_or(anyhow::anyhow!(
"Config missing, price feed list was poisoned during iteration." "Config missing, price feed list was poisoned during iteration."
))?; ))?;

View File

@ -33,7 +33,6 @@ use {
anyhow, anyhow,
Result, Result,
}, },
moka::future::Cache,
pyth_sdk::PriceIdentifier, pyth_sdk::PriceIdentifier,
pythnet_sdk::{ pythnet_sdk::{
messages::{ messages::{
@ -48,6 +47,7 @@ use {
std::{ std::{
collections::{ collections::{
BTreeMap, BTreeMap,
BTreeSet,
HashSet, HashSet,
}, },
sync::Arc, sync::Arc,
@ -78,9 +78,11 @@ pub mod storage;
pub mod types; pub mod types;
pub mod wormhole; pub mod wormhole;
const OBSERVED_CACHE_SIZE: usize = 1000;
pub struct Store { pub struct Store {
pub storage: StorageInstance, pub storage: StorageInstance,
pub observed_vaa_seqs: Cache<u64, bool>, pub observed_vaa_seqs: RwLock<BTreeSet<u64>>,
pub guardian_set: RwLock<BTreeMap<u32, GuardianSet>>, pub guardian_set: RwLock<BTreeMap<u32, GuardianSet>>,
pub update_tx: Sender<()>, pub update_tx: Sender<()>,
pub last_completed_update_at: RwLock<Option<Instant>>, pub last_completed_update_at: RwLock<Option<Instant>>,
@ -90,10 +92,7 @@ impl Store {
pub fn new_with_local_cache(update_tx: Sender<()>, cache_size: u64) -> Arc<Self> { pub fn new_with_local_cache(update_tx: Sender<()>, cache_size: u64) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
storage: storage::local_storage::LocalStorage::new_instance(cache_size), storage: storage::local_storage::LocalStorage::new_instance(cache_size),
observed_vaa_seqs: Cache::builder() observed_vaa_seqs: RwLock::new(Default::default()),
.max_capacity(cache_size)
.time_to_live(Duration::from_secs(60 * 5))
.build(),
guardian_set: RwLock::new(Default::default()), guardian_set: RwLock::new(Default::default()),
update_tx, update_tx,
last_completed_update_at: RwLock::new(None), last_completed_update_at: RwLock::new(None),
@ -113,7 +112,7 @@ impl Store {
return Ok(()); // Ignore VAA from other emitters return Ok(()); // Ignore VAA from other emitters
} }
if self.observed_vaa_seqs.get(&vaa.sequence).is_some() { if self.observed_vaa_seqs.read().await.contains(&vaa.sequence) {
return Ok(()); // Ignore VAA if we have already seen it return Ok(()); // Ignore VAA if we have already seen it
} }
@ -127,7 +126,13 @@ impl Store {
} }
}; };
self.observed_vaa_seqs.insert(vaa.sequence, true).await; {
let mut observed_vaa_seqs = self.observed_vaa_seqs.write().await;
observed_vaa_seqs.insert(vaa.sequence);
while observed_vaa_seqs.len() > OBSERVED_CACHE_SIZE {
observed_vaa_seqs.pop_first();
}
}
match WormholeMessage::try_from_bytes(vaa.payload)?.payload { match WormholeMessage::try_from_bytes(vaa.payload)?.payload {
WormholePayload::Merkle(proof) => { WormholePayload::Merkle(proof) => {
@ -232,7 +237,10 @@ impl Store {
let messages = self let messages = self
.storage .storage
.fetch_message_states( .fetch_message_states(
price_ids, price_ids
.iter()
.map(|price_id| price_id.to_bytes())
.collect(),
request_time, request_time,
MessageStateFilter::Only(MessageType::PriceFeedMessage), MessageStateFilter::Only(MessageType::PriceFeedMessage),
) )
@ -267,7 +275,7 @@ impl Store {
.message_state_keys() .message_state_keys()
.await .await
.iter() .iter()
.map(|key| PriceIdentifier::new(key.id)) .map(|key| PriceIdentifier::new(key.feed_id))
.collect() .collect()
} }

View File

@ -14,8 +14,8 @@ use {
Result, Result,
}, },
async_trait::async_trait, async_trait::async_trait,
pyth_sdk::PriceIdentifier,
pythnet_sdk::messages::{ pythnet_sdk::messages::{
FeedId,
Message, Message,
MessageType, MessageType,
}, },
@ -57,8 +57,8 @@ impl TryFrom<AccumulatorState> for CompletedAccumulatorState {
#[derive(Clone, PartialEq, Eq, Debug, Hash)] #[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub struct MessageStateKey { pub struct MessageStateKey {
pub id: [u8; 32], pub feed_id: FeedId,
pub type_: MessageType, pub type_: MessageType,
} }
#[derive(Clone, PartialEq, Eq, Debug, PartialOrd, Ord)] #[derive(Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
@ -85,8 +85,8 @@ impl MessageState {
pub fn key(&self) -> MessageStateKey { pub fn key(&self) -> MessageStateKey {
MessageStateKey { MessageStateKey {
id: self.message.id(), feed_id: self.message.feed_id(),
type_: self.message.into(), type_: self.message.into(),
} }
} }
@ -125,13 +125,99 @@ pub trait Storage: Send + Sync {
async fn store_message_states(&self, message_states: Vec<MessageState>) -> Result<()>; async fn store_message_states(&self, message_states: Vec<MessageState>) -> Result<()>;
async fn fetch_message_states( async fn fetch_message_states(
&self, &self,
ids: Vec<PriceIdentifier>, ids: Vec<FeedId>,
request_time: RequestTime, request_time: RequestTime,
filter: MessageStateFilter, filter: MessageStateFilter,
) -> Result<Vec<MessageState>>; ) -> Result<Vec<MessageState>>;
async fn store_accumulator_state(&self, state: AccumulatorState) -> Result<()>; async fn store_accumulator_state(&self, state: AccumulatorState) -> Result<()>;
async fn fetch_accumulator_state(&self, slot: u64) -> Result<Option<AccumulatorState>>; async fn fetch_accumulator_state(&self, slot: Slot) -> Result<Option<AccumulatorState>>;
} }
pub type StorageInstance = Box<dyn Storage>; pub type StorageInstance = Box<dyn Storage>;
#[cfg(test)]
mod test {
use {
super::*,
pythnet_sdk::wire::v1::WormholeMerkleRoot,
};
#[test]
pub fn test_complete_accumulator_state_try_from_accumulator_state_works() {
let accumulator_state = AccumulatorState {
slot: 1,
accumulator_messages: None,
wormhole_merkle_state: None,
};
assert!(CompletedAccumulatorState::try_from(accumulator_state.clone()).is_err());
let accumulator_state = AccumulatorState {
slot: 1,
accumulator_messages: Some(AccumulatorMessages {
slot: 1,
magic: [0; 4],
ring_size: 10,
messages: vec![],
}),
wormhole_merkle_state: None,
};
assert!(CompletedAccumulatorState::try_from(accumulator_state.clone()).is_err());
let accumulator_state = AccumulatorState {
slot: 1,
accumulator_messages: None,
wormhole_merkle_state: Some(WormholeMerkleState {
vaa: vec![],
root: WormholeMerkleRoot {
slot: 1,
ring_size: 10,
root: [0; 20],
},
}),
};
assert!(CompletedAccumulatorState::try_from(accumulator_state.clone()).is_err());
let accumulator_state = AccumulatorState {
slot: 1,
accumulator_messages: Some(AccumulatorMessages {
slot: 1,
magic: [0; 4],
ring_size: 10,
messages: vec![],
}),
wormhole_merkle_state: Some(WormholeMerkleState {
vaa: vec![],
root: WormholeMerkleRoot {
slot: 1,
ring_size: 10,
root: [0; 20],
},
}),
};
assert_eq!(
CompletedAccumulatorState::try_from(accumulator_state.clone()).unwrap(),
CompletedAccumulatorState {
slot: 1,
accumulator_messages: AccumulatorMessages {
slot: 1,
magic: [0; 4],
ring_size: 10,
messages: vec![],
},
wormhole_merkle_state: WormholeMerkleState {
vaa: vec![],
root: WormholeMerkleRoot {
slot: 1,
ring_size: 10,
root: [0; 20],
},
},
}
);
}
}

View File

@ -16,20 +16,22 @@ use {
}, },
async_trait::async_trait, async_trait::async_trait,
dashmap::DashMap, dashmap::DashMap,
moka::sync::Cache, pythnet_sdk::messages::{
pyth_sdk::PriceIdentifier, FeedId,
pythnet_sdk::messages::MessageType, MessageType,
},
std::{ std::{
collections::VecDeque, collections::VecDeque,
sync::Arc, sync::Arc,
}, },
strum::IntoEnumIterator, strum::IntoEnumIterator,
tokio::sync::RwLock,
}; };
#[derive(Clone)] #[derive(Clone)]
pub struct LocalStorage { pub struct LocalStorage {
message_cache: Arc<DashMap<MessageStateKey, VecDeque<MessageState>>>, message_cache: Arc<DashMap<MessageStateKey, VecDeque<MessageState>>>,
accumulator_cache: Cache<Slot, AccumulatorState>, accumulator_cache: Arc<RwLock<VecDeque<AccumulatorState>>>,
cache_size: u64, cache_size: u64,
} }
@ -37,7 +39,7 @@ impl LocalStorage {
pub fn new_instance(cache_size: u64) -> StorageInstance { pub fn new_instance(cache_size: u64) -> StorageInstance {
Box::new(Self { Box::new(Self {
message_cache: Arc::new(DashMap::new()), message_cache: Arc::new(DashMap::new()),
accumulator_cache: Cache::builder().max_capacity(cache_size).build(), accumulator_cache: Arc::new(RwLock::new(VecDeque::new())),
cache_size, cache_size,
}) })
} }
@ -123,7 +125,7 @@ impl Storage for LocalStorage {
async fn fetch_message_states( async fn fetch_message_states(
&self, &self,
ids: Vec<PriceIdentifier>, ids: Vec<FeedId>,
request_time: RequestTime, request_time: RequestTime,
filter: MessageStateFilter, filter: MessageStateFilter,
) -> Result<Vec<MessageState>> { ) -> Result<Vec<MessageState>> {
@ -137,8 +139,8 @@ impl Storage for LocalStorage {
message_types.into_iter().map(move |message_type| { message_types.into_iter().map(move |message_type| {
let key = MessageStateKey { let key = MessageStateKey {
id: id.to_bytes(), feed_id: id,
type_: message_type, type_: message_type,
}; };
self.retrieve_message_state(key, request_time.clone()) self.retrieve_message_state(key, request_time.clone())
.ok_or(anyhow!("Message not found")) .ok_or(anyhow!("Message not found"))
@ -155,12 +157,488 @@ impl Storage for LocalStorage {
} }
async fn store_accumulator_state(&self, state: super::AccumulatorState) -> Result<()> { async fn store_accumulator_state(&self, state: super::AccumulatorState) -> Result<()> {
let key = state.slot; let mut accumulator_cache = self.accumulator_cache.write().await;
self.accumulator_cache.insert(key, state); accumulator_cache.push_back(state);
let mut i = accumulator_cache.len().saturating_sub(1);
while i > 0 && accumulator_cache[i - 1].slot > accumulator_cache[i].slot {
accumulator_cache.swap(i - 1, i);
i -= 1;
}
if accumulator_cache.len() > self.cache_size as usize {
accumulator_cache.pop_front();
}
Ok(()) Ok(())
} }
async fn fetch_accumulator_state(&self, slot: Slot) -> Result<Option<super::AccumulatorState>> { async fn fetch_accumulator_state(&self, slot: Slot) -> Result<Option<super::AccumulatorState>> {
Ok(self.accumulator_cache.get(&slot)) let accumulator_cache = self.accumulator_cache.read().await;
match accumulator_cache.binary_search_by_key(&slot, |state| state.slot) {
Ok(idx) => Ok(accumulator_cache.get(idx).cloned()),
Err(_) => Ok(None),
}
}
}
#[cfg(test)]
mod test {
use {
super::*,
crate::store::{
proof::wormhole_merkle::WormholeMerkleMessageProof,
types::{
AccumulatorMessages,
ProofSet,
},
},
pyth_sdk::UnixTimestamp,
pythnet_sdk::{
accumulators::merkle::MerklePath,
hashers::keccak256_160::Keccak160,
messages::{
Message,
PriceFeedMessage,
},
},
};
pub fn create_dummy_price_feed_message_state(
feed_id: FeedId,
publish_time: i64,
slot: Slot,
) -> MessageState {
MessageState {
slot,
message: Message::PriceFeedMessage(PriceFeedMessage {
feed_id,
publish_time,
price: 1,
conf: 2,
exponent: 3,
ema_price: 4,
ema_conf: 5,
prev_publish_time: 6,
}),
received_at: publish_time,
proof_set: ProofSet {
wormhole_merkle_proof: WormholeMerkleMessageProof {
vaa: vec![],
proof: MerklePath::<Keccak160>::new(vec![]),
},
},
}
}
pub async fn create_and_store_dummy_price_feed_message_state(
storage: &StorageInstance,
feed_id: FeedId,
publish_time: UnixTimestamp,
slot: Slot,
) -> MessageState {
let message_state = create_dummy_price_feed_message_state(feed_id, publish_time, slot);
storage
.store_message_states(vec![message_state.clone()])
.await
.unwrap();
message_state
}
#[tokio::test]
pub async fn test_store_and_retrieve_latest_message_state_works() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
let message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// The latest message state should be the one we just stored.
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::Latest,
MessageStateFilter::Only(MessageType::PriceFeedMessage),
)
.await
.unwrap(),
vec![message_state]
);
}
#[tokio::test]
pub async fn test_store_and_retrieve_latest_message_state_with_multiple_update_works() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
let _old_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Create and store a message state with feed id [1....] and publish time 20 at slot 10.
let new_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 20, 10).await;
// The latest message state should be the one with publish time 20.
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::Latest,
MessageStateFilter::Only(MessageType::PriceFeedMessage)
)
.await
.unwrap(),
vec![new_message_state]
);
}
#[tokio::test]
pub async fn test_store_and_retrieve_latest_message_state_with_out_of_order_update_works() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 20 at slot 10.
let new_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 20, 10).await;
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
let _old_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// The latest message state should be the one with publish time 20.
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::Latest,
MessageStateFilter::Only(MessageType::PriceFeedMessage)
)
.await
.unwrap(),
vec![new_message_state]
);
}
#[tokio::test]
pub async fn test_store_and_retrieve_first_after_message_state_works() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
let old_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Create and store a message state with feed id [1....] and publish time 13 at slot 10.
let new_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
// The first message state after time 10 should be the old message state.
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::FirstAfter(10),
MessageStateFilter::Only(MessageType::PriceFeedMessage)
)
.await
.unwrap(),
vec![old_message_state]
);
// Querying the first after pub time 11, 12, 13 should all return the new message state.
for request_time in 11..14 {
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::FirstAfter(request_time),
MessageStateFilter::Only(MessageType::PriceFeedMessage)
)
.await
.unwrap(),
vec![new_message_state.clone()]
);
}
}
#[tokio::test]
pub async fn test_store_and_retrieve_latest_message_state_with_same_pubtime_works() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
let slightly_older_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Create and store a message state with feed id [1....] and publish time 10 at slot 7.
let slightly_newer_message_state =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 7).await;
// The latest message state should be the one with the higher slot.
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::Latest,
MessageStateFilter::Only(MessageType::PriceFeedMessage),
)
.await
.unwrap(),
vec![slightly_newer_message_state]
);
// Querying the first message state after time 10 should return the one with the lower slot.
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::FirstAfter(10),
MessageStateFilter::Only(MessageType::PriceFeedMessage),
)
.await
.unwrap(),
vec![slightly_older_message_state]
);
}
#[tokio::test]
pub async fn test_store_and_retrieve_first_after_message_state_fails_for_past_time() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Create and store a message state with feed id [1....] and publish time 13 at slot 10.
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
// Query the message state before the available times should return an error.
// This is because we are not sure that the first available message is really the first.
assert!(storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::FirstAfter(9),
MessageStateFilter::Only(MessageType::PriceFeedMessage)
)
.await
.is_err());
}
#[tokio::test]
pub async fn test_store_and_retrieve_first_after_message_state_fails_for_future_time() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Create and store a message state with feed id [1....] and publish time 13 at slot 10.
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
// Query the message state after the available times should return an error.
assert!(storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::FirstAfter(14),
MessageStateFilter::Only(MessageType::PriceFeedMessage)
)
.await
.is_err());
}
#[tokio::test]
pub async fn test_store_more_message_states_than_cache_size_evicts_old_messages() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Create and store a message state with feed id [1....] and publish time 13 at slot 10.
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
// Create and store a message state with feed id [1....] and publish time 20 at slot 14.
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 20, 14).await;
// The message at time 10 should be evicted and querying for it should return an error.
assert!(storage
.fetch_message_states(
vec![[1; 32]],
RequestTime::FirstAfter(10),
MessageStateFilter::Only(MessageType::PriceFeedMessage)
)
.await
.is_err());
}
#[tokio::test]
pub async fn test_store_and_receive_multiple_message_feed_ids_works() {
// Initialize a storage with a cache size of 1 per key.
let storage = LocalStorage::new_instance(1);
// Create and store a message state with feed id [1....] and publish time 10 at slot 5.
let message_state_1 =
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Create and store a message state with feed id [2....] and publish time 13 at slot 10.
let message_state_2 =
create_and_store_dummy_price_feed_message_state(&storage, [2; 32], 10, 5).await;
// Check both message states can be retrieved.
assert_eq!(
storage
.fetch_message_states(
vec![[1; 32], [2; 32]],
RequestTime::Latest,
MessageStateFilter::Only(MessageType::PriceFeedMessage),
)
.await
.unwrap(),
vec![message_state_1, message_state_2]
);
}
#[tokio::test]
pub async fn test_receive_not_existent_message_fails() {
// Initialize a storage with a cache size of 2 per key.
let storage = LocalStorage::new_instance(2);
create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
// Check both message states can be retrieved.
assert!(storage
.fetch_message_states(
vec![[2; 32]],
RequestTime::Latest,
MessageStateFilter::Only(MessageType::PriceFeedMessage),
)
.await
.is_err());
}
pub fn create_empty_accumulator_state_at_slot(slot: Slot) -> AccumulatorState {
AccumulatorState {
slot,
accumulator_messages: None,
wormhole_merkle_state: None,
}
}
pub async fn create_and_store_empty_accumulator_state_at_slot(
storage: &StorageInstance,
slot: Slot,
) -> AccumulatorState {
let accumulator_state = create_empty_accumulator_state_at_slot(slot);
storage
.store_accumulator_state(accumulator_state.clone())
.await
.unwrap();
accumulator_state
}
#[tokio::test]
pub async fn test_store_and_receive_accumulator_state_works() {
// Initialize a storage with a cache size of 2 per key and the accumulator state.
let storage = LocalStorage::new_instance(2);
// Create and store an accumulator state with slot 10.
let accumulator_state =
create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
// Make sure the retrieved accumulator state is what we stored.
assert_eq!(
storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
accumulator_state
);
}
#[tokio::test]
pub async fn test_store_and_receive_accumulator_state_works_on_overwrite() {
// Initialize a storage with a cache size of 2 per key and the accumulator state.
let storage = LocalStorage::new_instance(2);
// Create and store an accumulator state with slot 10.
let mut accumulator_state =
create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
// Retrieve the accumulator state and make sure it is what we stored.
assert_eq!(
storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
accumulator_state
);
// Change the state to have accumulator messages
// We mutate the existing state because the normal flow is like this.
accumulator_state.accumulator_messages = Some(AccumulatorMessages {
magic: [0; 4],
slot: 10,
ring_size: 3,
messages: vec![],
});
// Store the accumulator state again.
storage
.store_accumulator_state(accumulator_state.clone())
.await
.unwrap();
// Make sure the retrieved accumulator state is what we stored.
assert_eq!(
storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
accumulator_state
);
}
#[tokio::test]
pub async fn test_store_and_receive_multiple_accumulator_state_works() {
// Initialize a storage with a cache size of 2 per key and the accumulator state.
let storage = LocalStorage::new_instance(2);
let accumulator_state_at_slot_10 =
create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
let accumulator_state_at_slot_20 =
create_and_store_empty_accumulator_state_at_slot(&storage, 20).await;
// Retrieve the accumulator states and make sure it is what we stored.
assert_eq!(
storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
accumulator_state_at_slot_10
);
assert_eq!(
storage.fetch_accumulator_state(20).await.unwrap().unwrap(),
accumulator_state_at_slot_20
);
}
#[tokio::test]
pub async fn test_store_and_receive_accumulator_state_evicts_cache() {
// Initialize a storage with a cache size of 2 per key and the accumulator state.
let storage = LocalStorage::new_instance(2);
let _accumulator_state_at_slot_10 =
create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
let accumulator_state_at_slot_20 =
create_and_store_empty_accumulator_state_at_slot(&storage, 20).await;
let accumulator_state_at_slot_30 =
create_and_store_empty_accumulator_state_at_slot(&storage, 30).await;
// The accumulator state at slot 10 should be evicted from the cache.
assert_eq!(storage.fetch_accumulator_state(10).await.unwrap(), None);
// Retrieve the rest of accumulator states and make sure it is what we stored.
assert_eq!(
storage.fetch_accumulator_state(20).await.unwrap().unwrap(),
accumulator_state_at_slot_20
);
assert_eq!(
storage.fetch_accumulator_state(30).await.unwrap().unwrap(),
accumulator_state_at_slot_30
);
} }
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "pythnet-sdk" name = "pythnet-sdk"
version = "1.13.6" version = "2.0.0"
description = "Pyth Runtime for Solana" description = "Pyth Runtime for Solana"
authors = ["Pyth Data Association"] authors = ["Pyth Data Association"]
repository = "https://github.com/pyth-network/pythnet" repository = "https://github.com/pyth-network/pythnet"

View File

@ -17,7 +17,6 @@ use serde::{
/// some of the methods for PriceFeedMessage and TwapMessage are not used by the oracle /// some of the methods for PriceFeedMessage and TwapMessage are not used by the oracle
/// for the same reason. Rust compiler doesn't include the unused methods in the contract. /// for the same reason. Rust compiler doesn't include the unused methods in the contract.
/// Once we start using the unused structs and methods, the contract size will increase. /// Once we start using the unused structs and methods, the contract size will increase.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr( #[cfg_attr(
feature = "strum", feature = "strum",
@ -47,10 +46,10 @@ impl Message {
} }
} }
pub fn id(&self) -> [u8; 32] { pub fn feed_id(&self) -> FeedId {
match self { match self {
Self::PriceFeedMessage(msg) => msg.id, Self::PriceFeedMessage(msg) => msg.feed_id,
Self::TwapMessage(msg) => msg.id, Self::TwapMessage(msg) => msg.feed_id,
} }
} }
} }
@ -65,11 +64,13 @@ impl Arbitrary for Message {
} }
} }
/// Id of a feed producing the message. One feed produces one or more messages.
pub type FeedId = [u8; 32];
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct PriceFeedMessage { pub struct PriceFeedMessage {
pub id: [u8; 32], pub feed_id: FeedId,
pub price: i64, pub price: i64,
pub conf: u64, pub conf: u64,
pub exponent: i32, pub exponent: i32,
@ -119,7 +120,7 @@ impl Arbitrary for PriceFeedMessage {
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct TwapMessage { pub struct TwapMessage {
pub id: [u8; 32], pub feed_id: FeedId,
pub cumulative_price: i128, pub cumulative_price: i128,
pub cumulative_conf: u128, pub cumulative_conf: u128,
pub num_down_slots: u64, pub num_down_slots: u64,

View File

@ -1490,7 +1490,7 @@ dependencies = [
[[package]] [[package]]
name = "pythnet-sdk" name = "pythnet-sdk"
version = "1.13.6" version = "2.0.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"borsh", "borsh",

View File

@ -542,7 +542,7 @@ fn parse_accumulator(deps: &Deps, env: &Env, data: &[u8]) -> StdResult<Vec<Price
match msg { match msg {
Message::PriceFeedMessage(price_feed_message) => { Message::PriceFeedMessage(price_feed_message) => {
let price_feed = PriceFeed::new( let price_feed = PriceFeed::new(
PriceIdentifier::new(price_feed_message.id), PriceIdentifier::new(price_feed_message.feed_id),
Price { Price {
price: price_feed_message.price, price: price_feed_message.price,
conf: price_feed_message.conf, conf: price_feed_message.conf,
@ -1186,7 +1186,7 @@ mod test {
let mut dummy_id = [0; 32]; let mut dummy_id = [0; 32];
dummy_id[0] = value as u8; dummy_id[0] = value as u8;
let msg = PriceFeedMessage { let msg = PriceFeedMessage {
id: dummy_id, feed_id: dummy_id,
price: value, price: value,
conf: value as u64, conf: value as u64,
exponent: value as i32, exponent: value as i32,
@ -1265,7 +1265,7 @@ mod test {
match msg { match msg {
Message::PriceFeedMessage(feed_msg) => { Message::PriceFeedMessage(feed_msg) => {
let feed = price_feed_read_bucket(&deps.storage) let feed = price_feed_read_bucket(&deps.storage)
.load(&feed_msg.id) .load(&feed_msg.feed_id)
.unwrap(); .unwrap();
let price = feed.get_price_unchecked(); let price = feed.get_price_unchecked();
let ema_price = feed.get_ema_price_unchecked(); let ema_price = feed.get_ema_price_unchecked();
@ -1517,7 +1517,7 @@ mod test {
// Although Twap Message is a valid message but it won't get stored on-chain via // Although Twap Message is a valid message but it won't get stored on-chain via
// `update_price_feeds` and (will be) used in other methods // `update_price_feeds` and (will be) used in other methods
let feed1 = Message::TwapMessage(TwapMessage { let feed1 = Message::TwapMessage(TwapMessage {
id: [0; 32], feed_id: [0; 32],
cumulative_price: 0, cumulative_price: 0,
cumulative_conf: 0, cumulative_conf: 0,
num_down_slots: 0, num_down_slots: 0,