From c46a5ee19042403c028b2f332e6b030bbc83af84 Mon Sep 17 00:00:00 2001 From: Xiliang Chen <xlchen1291@gmail.com> Date: Thu, 21 May 2020 00:04:05 +1200 Subject: [PATCH] Oracle (#169) * refactor oracle * use SignedExtension * fix tests * finish oracle tests --- oracle/Cargo.toml | 8 +- oracle/src/lib.rs | 276 +++++++++++++++++--------- oracle/src/mock.rs | 62 +++--- oracle/src/operator_provider.rs | 19 -- oracle/src/tests.rs | 342 ++++++++++++++++++++------------ utilities/src/lib.rs | 1 + utilities/src/ordered_set.rs | 2 +- 7 files changed, 440 insertions(+), 270 deletions(-) delete mode 100644 oracle/src/operator_provider.rs diff --git a/oracle/Cargo.toml b/oracle/Cargo.toml index efaf200..aff8bc4 100644 --- a/oracle/Cargo.toml +++ b/oracle/Cargo.toml @@ -10,8 +10,10 @@ edition = "2018" [dependencies] serde = { version = "1.0.101", optional = true } codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false } -sp-runtime = { version = "2.0.0-alpha.8", default-features = false } + +sp-application-crypto = { version = "2.0.0-alpha.8", default-features = false } sp-io = { version = "2.0.0-alpha.8", default-features = false } +sp-runtime = { version = "2.0.0-alpha.8", default-features = false } sp-std = { version = "2.0.0-alpha.8", default-features = false } frame-support = { version = "2.0.0-alpha.8", default-features = false } @@ -22,7 +24,6 @@ orml-utilities = { path = "../utilities", version = "0.1.0", default-features = [dev-dependencies] sp-core = { version = "2.0.0-alpha.8", default-features = false } -pallet-timestamp = { version = "2.0.0-alpha.8" } clear_on_drop = { version = "0.2.3", features = ["no_cc"] } # https://github.com/paritytech/substrate/issues/4179 @@ -31,8 +32,9 @@ default = ["std"] std = [ "serde", "codec/std", - "sp-runtime/std", + "sp-application-crypto/std", "sp-io/std", + "sp-runtime/std", "sp-std/std", "frame-support/std", "frame-system/std", diff --git a/oracle/src/lib.rs b/oracle/src/lib.rs index 8c2b7ac..9d19523 100644 --- a/oracle/src/lib.rs +++ b/oracle/src/lib.rs @@ -4,36 +4,54 @@ mod default_combine_data; mod mock; -mod operator_provider; mod tests; mod timestamped_value; use codec::{Decode, Encode}; -pub use default_combine_data::DefaultCombineData; use frame_support::{ decl_error, decl_event, decl_module, decl_storage, dispatch::Dispatchable, ensure, - traits::Time, - weights::{DispatchClass, FunctionOf, Pays, TransactionPriority}, + traits::{ChangeMembers, Get, InitializeMembers, Time}, + weights::{DispatchClass, FunctionOf, Pays}, IsSubType, IterableStorageMap, Parameter, }; -pub use operator_provider::OperatorProvider; use sp_runtime::{ traits::{DispatchInfoOf, Member, SignedExtension}, + transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionValidity, TransactionValidityError, ValidTransaction, + }, DispatchResult, }; -use sp_std::{fmt, marker, prelude::*, result, vec}; +use sp_std::{fmt, prelude::*, result, vec}; // FIXME: `pallet/frame-` prefix should be used for all pallet modules, but currently `frame_system` // would cause compiling error in `decl_module!` and `construct_runtime!` // #3295 https://github.com/paritytech/substrate/issues/3295 -use frame_system::{self as system, ensure_signed}; +pub use default_combine_data::DefaultCombineData; +use frame_system::{self as system, ensure_none, ensure_signed}; pub use orml_traits::{CombineData, DataProvider, DataProviderExtended, OnNewData, OnRedundantCall}; -use sp_runtime::transaction_validity::{ - InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, -}; +use orml_utilities::OrderedSet; pub use timestamped_value::TimestampedValue; +use sp_application_crypto::{KeyTypeId, RuntimeAppPublic}; +pub const ORACLE: KeyTypeId = KeyTypeId(*b"orac"); + +mod app_sr25519 { + use sp_application_crypto::{app_crypto, sr25519}; + app_crypto!(sr25519, super::ORACLE); +} + +sp_application_crypto::with_pair! { + /// An oracle keypair using sr25519 as its crypto. + pub type AuthorityPair = app_sr25519::Pair; +} + +/// An oracle signature using sr25519 as its crypto. +pub type AuthoritySignature = app_sr25519::Signature; + +/// An oracle identifier using sr25519 as its crypto. +pub type AuthorityId = app_sr25519::Public; + type MomentOf<T> = <<T as Trait>::Time as Time>::Moment; pub type TimestampedValueOf<T> = TimestampedValue<<T as Trait>::OracleValue, MomentOf<T>>; @@ -41,25 +59,48 @@ pub trait Trait: frame_system::Trait { type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>; type Call: Parameter + Dispatchable<Origin = <Self as frame_system::Trait>::Origin> + IsSubType<Module<Self>, Self>; type OnNewData: OnNewData<Self::AccountId, Self::OracleKey, Self::OracleValue>; - type OnRedundantCall: OnRedundantCall<Self::AccountId>; - type OperatorProvider: OperatorProvider<Self::AccountId>; type CombineData: CombineData<Self::OracleKey, TimestampedValueOf<Self>>; type Time: Time; type OracleKey: Parameter + Member; type OracleValue: Parameter + Member + Ord; + + /// A configuration for base priority of unsigned transactions. + /// + /// This is exposed so that it can be tuned for particular runtime, when + /// multiple pallets send unsigned transactions. + type UnsignedPriority: Get<TransactionPriority>; + + /// The identifier type for an authority. + type AuthorityId: Member + Parameter + RuntimeAppPublic + Default + Ord; } decl_storage! { trait Store for Module<T: Trait> as Oracle { - pub RawValues get(fn raw_values): double_map hasher(twox_64_concat) T::OracleKey, hasher(twox_64_concat) T::AccountId => Option<TimestampedValueOf<T>>; - pub HasUpdate get(fn has_update): map hasher(twox_64_concat) T::OracleKey => bool; + + /// Raw values for each oracle operators + pub RawValues get(fn raw_values): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::OracleKey => Option<TimestampedValueOf<T>>; + + /// True if Self::values(key) is up to date, otherwise the value is stale + pub IsUpdated get(fn is_updated): map hasher(twox_64_concat) T::OracleKey => bool; + + /// Combined value, may not be up to date pub Values get(fn values): map hasher(twox_64_concat) T::OracleKey => Option<TimestampedValueOf<T>>; - HasDispatched: Vec<T::AccountId>; + + /// If an oracle operator has feed a value in this block + HasDispatched: OrderedSet<T::AccountId>; + + // TODO: this shouldn't be required https://github.com/paritytech/substrate/issues/6041 + /// The current members of the collective. This is stored sorted (just by value). + pub Members get(fn members) config(): OrderedSet<T::AccountId>; + + /// Session key for oracle operators + pub SessionKeys get(fn session_keys) config(): map hasher(twox_64_concat) T::AccountId => Option<T::AuthorityId>; + + pub Nonces get(fn nonces): map hasher(twox_64_concat) T::AccountId => u32; } } decl_error! { - // Oracle module errors pub enum Error for Module<T: Trait> { NoPermission, UpdateAlreadyDispatched, @@ -72,21 +113,31 @@ pub enum ValidityError { } decl_module! { - #[derive(Encode, Decode)] pub struct Module<T: Trait> for enum Call where origin: T::Origin { type Error = Error<T>; + fn deposit_event() = default; #[weight = FunctionOf(0, DispatchClass::Operational, Pays::No)] - pub fn feed_value(origin, key: T::OracleKey, value: T::OracleValue) { - let who = ensure_signed(origin)?; - Self::_feed_values(who, vec![(key, value)])?; + pub fn feed_values( + origin, + values: Vec<(T::OracleKey, T::OracleValue)>, + #[compact] index: u32, + // since signature verification is done in `validate_unsigned` + // we can skip doing it here again. + _signature: <T::AuthorityId as RuntimeAppPublic>::Signature, + ) { + ensure_none(origin)?; + let who = Self::members().0[index as usize].clone(); + Self::_feed_values(who, values)?; } - #[weight = FunctionOf(0, DispatchClass::Operational, Pays::No)] - pub fn feed_values(origin, values: Vec<(T::OracleKey, T::OracleValue)>) { + #[weight = 10_000_000] + pub fn set_session_key(origin, key: T::AuthorityId) { let who = ensure_signed(origin)?; - Self::_feed_values(who, values)?; + ensure!(Self::members().contains(&who), Error::<T>::NoPermission); + + SessionKeys::<T>::insert(who, key); } fn on_finalize(_n: T::BlockNumber) { @@ -109,9 +160,10 @@ decl_event!( impl<T: Trait> Module<T> { pub fn read_raw_values(key: &T::OracleKey) -> Vec<TimestampedValueOf<T>> { - T::OperatorProvider::operators() + Self::members() + .0 .iter() - .filter_map(|x| <RawValues<T>>::get(key, x)) + .filter_map(|x| Self::raw_values(x, key)) .collect() } @@ -119,22 +171,24 @@ impl<T: Trait> Module<T> { /// /// Note this will update values storage if has update. pub fn get(key: &T::OracleKey) -> Option<TimestampedValueOf<T>> { - if <HasUpdate<T>>::take(key) { + if Self::is_updated(key) { + <Values<T>>::get(key) + } else { let timestamped = Self::combined(key)?; <Values<T>>::insert(key, timestamped.clone()); - return Some(timestamped); + IsUpdated::<T>::insert(key, true); + Some(timestamped) } - <Values<T>>::get(key) } /// Returns fresh combined value if has update, or latest combined value. /// /// This is a no-op function which would not change storage. pub fn get_no_op(key: &T::OracleKey) -> Option<TimestampedValueOf<T>> { - if Self::has_update(key) { - Self::combined(key) - } else { + if Self::is_updated(key) { Self::values(key) + } else { + Self::combined(key) } } @@ -149,14 +203,74 @@ impl<T: Trait> Module<T> { let values = Self::read_raw_values(key); T::CombineData::combine_data(key, values, Self::values(key)) } + + fn _feed_values(who: T::AccountId, values: Vec<(T::OracleKey, T::OracleValue)>) -> DispatchResult { + let now = T::Time::now(); + + for (key, value) in &values { + let timestamped = TimestampedValue { + value: value.clone(), + timestamp: now, + }; + RawValues::<T>::insert(&who, &key, timestamped); + IsUpdated::<T>::remove(&key); + + T::OnNewData::on_new_data(&who, &key, &value); + } + + Self::deposit_event(RawEvent::NewFeedData(who, values)); + + Ok(()) + } +} + +impl<T: Trait> InitializeMembers<T::AccountId> for Module<T> { + fn initialize_members(members: &[T::AccountId]) { + if !members.is_empty() { + assert!(Members::<T>::get().0.is_empty(), "Members are already initialized!"); + Members::<T>::put(OrderedSet::from_sorted_set(members.into())); + } + } +} + +impl<T: Trait> ChangeMembers<T::AccountId> for Module<T> { + fn change_members_sorted(_incoming: &[T::AccountId], outgoing: &[T::AccountId], new: &[T::AccountId]) { + // remove session keys and its values + for removed in outgoing { + SessionKeys::<T>::remove(removed); + RawValues::<T>::remove_prefix(removed); + Nonces::<T>::remove(removed); + } + + Members::<T>::put(OrderedSet::from_sorted_set(new.into())); + + // not bothering to track which key needs recompute, just update all + IsUpdated::<T>::remove_all(); + } + + fn set_prime(_prime: Option<T::AccountId>) { + // nothing + } +} + +impl<T: Trait> DataProvider<T::OracleKey, T::OracleValue> for Module<T> { + fn get(key: &T::OracleKey) -> Option<T::OracleValue> { + Self::get(key).map(|timestamped_value| timestamped_value.value) + } +} + +impl<T: Trait> DataProviderExtended<T::OracleKey, T::OracleValue, T::AccountId> for Module<T> { + fn feed_value(who: T::AccountId, key: T::OracleKey, value: T::OracleValue) -> DispatchResult { + Self::_feed_values(who, vec![(key, value)]) + } } #[derive(Encode, Decode, Clone, Eq, PartialEq, Default)] -pub struct CheckOperator<T: Trait + Send + Sync>(marker::PhantomData<T>); +pub struct CheckOperator<T: Trait + Send + Sync>(sp_std::marker::PhantomData<T>); impl<T: Trait + Send + Sync> CheckOperator<T> { pub fn new() -> Self { - Self(marker::PhantomData) + Self(sp_std::marker::PhantomData) } } @@ -183,73 +297,45 @@ impl<T: Trait + Send + Sync> SignedExtension for CheckOperator<T> { Ok(()) } - fn validate( - &self, - who: &T::AccountId, - call: &Self::Call, - _info: &DispatchInfoOf<Self::Call>, - _len: usize, - ) -> TransactionValidity { + fn validate_unsigned(call: &Self::Call, _info: &DispatchInfoOf<Self::Call>, _len: usize) -> TransactionValidity { let call = match call.is_sub_type() { Some(call) => call, None => return Ok(ValidTransaction::default()), }; - if let Call::<T>::feed_value(..) | Call::<T>::feed_values(..) = call { - ensure!( - T::OperatorProvider::can_feed_data(who), - TransactionValidityError::Invalid(InvalidTransaction::Custom(ValidityError::NoPermission as u8)) - ); - - return Ok(ValidTransaction { - priority: TransactionPriority::max_value(), - ..Default::default() - }); - } - Ok(ValidTransaction::default()) - } -} - -impl<T: Trait> DataProvider<T::OracleKey, T::OracleValue> for Module<T> { - fn get(key: &T::OracleKey) -> Option<T::OracleValue> { - Self::get(key).map(|timestamped_value| timestamped_value.value) - } -} - -impl<T: Trait> DataProviderExtended<T::OracleKey, T::OracleValue, T::AccountId> for Module<T> { - fn feed_value(who: T::AccountId, key: T::OracleKey, value: T::OracleValue) -> DispatchResult { - Self::_feed_values(who, vec![(key, value)]) - } -} - -impl<T: Trait> Module<T> { - fn _feed_values(who: T::AccountId, values: Vec<(T::OracleKey, T::OracleValue)>) -> DispatchResult { - ensure!(T::OperatorProvider::can_feed_data(&who), Error::<T>::NoPermission); - - // ensure account hasn't dispatched an updated yet - let mut accounts = <HasDispatched<T>>::get(); - if accounts.contains(&who) { - T::OnRedundantCall::multiple_calls_per_block(&who); - return Err(Error::<T>::UpdateAlreadyDispatched.into()); - } - accounts.push(who.clone()); - <HasDispatched<T>>::put(accounts); - - let now = T::Time::now(); - - for (key, value) in &values { - let timestamped = TimestampedValue { - value: value.clone(), - timestamp: now, - }; - <RawValues<T>>::insert(&key, &who, timestamped); - <HasUpdate<T>>::insert(&key, true); - - T::OnNewData::on_new_data(&who, &key, &value); + if let Call::feed_values(value, index, signature) = call { + let members = Module::<T>::members(); + let who = members.0.get(*index as usize); + if let Some(who) = who { + let nonce = Module::<T>::nonces(&who); + + let signature_valid = Module::<T>::session_keys(&who) + .map(|session_key| (nonce, value).using_encoded(|payload| session_key.verify(&payload, &signature))) + .unwrap_or(false); + + if !signature_valid { + return InvalidTransaction::BadProof.into(); + } + + // ensure account hasn't dispatched an updated yet + let ok = HasDispatched::<T>::mutate(|set| set.insert(who.clone())); + if !ok { + // we already received a feed for this operator + return Err(InvalidTransaction::Stale.into()); + } + + Nonces::<T>::insert(who, nonce + 1); + + ValidTransaction::with_tag_prefix("Oracle") + .priority(T::UnsignedPriority::get()) + .longevity(256) + .propagate(true) + .build() + } else { + InvalidTransaction::BadProof.into() + } + } else { + InvalidTransaction::Call.into() } - - Self::deposit_event(RawEvent::NewFeedData(who, values)); - - Ok(()) } } diff --git a/oracle/src/mock.rs b/oracle/src/mock.rs index c48ebd6..38d6e56 100644 --- a/oracle/src/mock.rs +++ b/oracle/src/mock.rs @@ -5,10 +5,11 @@ use super::*; use frame_support::{impl_outer_dispatch, impl_outer_origin, parameter_types, weights::Weight}; use sp_core::H256; use sp_runtime::{ - testing::Header, + testing::{Header, UintAuthorityId}, traits::{BlakeTwo256, IdentityLookup}, Perbill, }; +use std::cell::RefCell; impl_outer_origin! { pub enum Origin for Test {} @@ -21,6 +22,9 @@ impl_outer_dispatch! { } pub type OracleCall = super::Call<Test>; +type AccountId = u64; +type Key = u32; +type Value = u32; // For testing the module, we construct most of a mock runtime. This means // first constructing a configuration type (`Test`) which `impl`s each of the @@ -40,7 +44,7 @@ impl frame_system::Trait for Test { type BlockNumber = u64; type Hash = H256; type Hashing = BlakeTwo256; - type AccountId = u64; + type AccountId = AccountId; type Lookup = IdentityLookup<Self::AccountId>; type Header = Header; type Event = (); @@ -58,55 +62,59 @@ impl frame_system::Trait for Test { type ExtrinsicBaseWeight = (); } -type AccountId = u64; -type Key = u32; -type Value = u32; - -pub type Timestamp = pallet_timestamp::Module<Test>; - -parameter_types! { - pub const MinimumPeriod: u64 = 5; -} - -impl pallet_timestamp::Trait for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = MinimumPeriod; +thread_local! { + static TIME: RefCell<u32> = RefCell::new(0); } -pub struct MockOperatorProvider; +pub struct Timestamp; +impl Time for Timestamp { + type Moment = u32; -impl OperatorProvider<AccountId> for MockOperatorProvider { - fn can_feed_data(who: &AccountId) -> bool { - Self::operators().contains(who) + fn now() -> Self::Moment { + TIME.with(|v| *v.borrow()) } +} - fn operators() -> Vec<AccountId> { - vec![1, 2, 3] +impl Timestamp { + pub fn set_timestamp(val: u32) { + TIME.with(|v| *v.borrow_mut() = val); } } parameter_types! { pub const MinimumCount: u32 = 3; pub const ExpiresIn: u32 = 600; + pub const UnsignedPriority: TransactionPriority = 32u64; } impl Trait for Test { type Event = (); type Call = Call; type OnNewData = (); - type OnRedundantCall = (); - type OperatorProvider = MockOperatorProvider; type CombineData = DefaultCombineData<Self, MinimumCount, ExpiresIn>; - type Time = pallet_timestamp::Module<Self>; + type Time = Timestamp; type OracleKey = Key; type OracleValue = Value; + type UnsignedPriority = UnsignedPriority; + type AuthorityId = UintAuthorityId; } pub type ModuleOracle = Module<Test>; // This function basically just builds a genesis storage key/value store according to // our desired mockup. pub fn new_test_ext() -> sp_io::TestExternalities { - let r = frame_system::GenesisConfig::default().build_storage::<Test>(); + let mut storage = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap(); + + let _ = GenesisConfig::<Test> { + members: vec![1, 2, 3].into(), + session_keys: vec![(1, 10.into()), (2, 20.into()), (3, 30.into())], + } + .assimilate_storage(&mut storage); + + let mut t: sp_io::TestExternalities = storage.into(); + + t.execute_with(|| { + Timestamp::set_timestamp(12345); + }); - r.unwrap().into() + t } diff --git a/oracle/src/operator_provider.rs b/oracle/src/operator_provider.rs deleted file mode 100644 index 40cfad9..0000000 --- a/oracle/src/operator_provider.rs +++ /dev/null @@ -1,19 +0,0 @@ -use sp_std::prelude::Vec; - -pub trait OperatorProvider<AccountId> { - // Make sure `who` has permission to feed data - fn can_feed_data(who: &AccountId) -> bool; - - // return a list of operators - fn operators() -> Vec<AccountId>; -} - -impl<AccountId> OperatorProvider<AccountId> for () { - fn can_feed_data(_who: &AccountId) -> bool { - false - } - - fn operators() -> Vec<AccountId> { - Vec::new() - } -} diff --git a/oracle/src/tests.rs b/oracle/src/tests.rs index 6d3572d..5c76de1 100644 --- a/oracle/src/tests.rs +++ b/oracle/src/tests.rs @@ -2,33 +2,51 @@ use crate::{ mock::{new_test_ext, Call, ModuleOracle, OracleCall, Origin, Test, Timestamp}, - {CheckOperator, Error, TimestampedValue}, + {CheckOperator, TimestampedValue}, }; +use codec::Encode; use frame_support::{ - assert_noop, assert_ok, - traits::OnFinalize, - weights::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays, TransactionPriority}, + assert_noop, assert_ok, dispatch, + traits::{ChangeMembers, OnFinalize}, + weights::{DispatchClass, DispatchInfo, Pays}, +}; +use sp_runtime::{ + testing::UintAuthorityId, + traits::SignedExtension, + transaction_validity::{InvalidTransaction, TransactionValidityError}, + RuntimeAppPublic, }; -use sp_runtime::{traits::SignedExtension, transaction_validity::ValidTransaction}; - -#[test] -fn should_feed_value() { - new_test_ext().execute_with(|| { - let key: u32 = 1; - let account_id: u64 = 1; - - Timestamp::set_timestamp(12345); - let expected = TimestampedValue { - value: 1000, - timestamp: 12345, - }; +fn feed_values_from_session_key( + id: UintAuthorityId, + index: u32, + nonce: u32, + values: Vec<(u32, u32)>, +) -> Result<dispatch::DispatchResult, TransactionValidityError> { + let sig = id.sign(&(nonce, &values).encode()).unwrap(); + + CheckOperator::<Test>::validate_unsigned( + &Call::ModuleOracle(OracleCall::feed_values(values.clone(), index, sig.clone())), + &DispatchInfo { + weight: 0, + class: DispatchClass::Normal, + pays_fee: Pays::Yes, + }, + 0, + )?; + + Ok(ModuleOracle::feed_values(Origin::NONE, values, index, sig)) +} - assert_ok!(ModuleOracle::feed_value(Origin::signed(account_id), key, 1000)); +fn feed_values( + from: u64, + index: u32, + nonce: u32, + values: Vec<(u32, u32)>, +) -> Result<dispatch::DispatchResult, TransactionValidityError> { + let id = ModuleOracle::session_keys(from).unwrap(); - let feed_data = ModuleOracle::raw_values(&key, &account_id).unwrap(); - assert_eq!(feed_data, expected); - }); + feed_values_from_session_key(id, index, nonce, values) } #[test] @@ -36,15 +54,10 @@ fn should_feed_values() { new_test_ext().execute_with(|| { let account_id: u64 = 1; - Timestamp::set_timestamp(12345); - - assert_ok!(ModuleOracle::feed_values( - Origin::signed(account_id), - vec![(1, 1000), (2, 900), (3, 800)] - )); + assert_ok!(feed_values(account_id, 0, 0, vec![(50, 1000), (51, 900), (52, 800)])); assert_eq!( - ModuleOracle::raw_values(&1, &account_id), + ModuleOracle::raw_values(&account_id, &50), Some(TimestampedValue { value: 1000, timestamp: 12345, @@ -52,7 +65,7 @@ fn should_feed_values() { ); assert_eq!( - ModuleOracle::raw_values(&2, &account_id), + ModuleOracle::raw_values(&account_id, &51), Some(TimestampedValue { value: 900, timestamp: 12345, @@ -60,7 +73,7 @@ fn should_feed_values() { ); assert_eq!( - ModuleOracle::raw_values(&3, &account_id), + ModuleOracle::raw_values(&account_id, &52), Some(TimestampedValue { value: 800, timestamp: 12345, @@ -70,157 +83,151 @@ fn should_feed_values() { } #[test] -fn should_change_status_when_feeding() { +fn should_update_is_updated() { new_test_ext().execute_with(|| { - let key: u32 = 1; - assert_eq!(ModuleOracle::has_update(key), false); - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), key, 1000)); - assert_eq!(ModuleOracle::has_update(key), true); + let key: u32 = 50; + assert_eq!(ModuleOracle::is_updated(key), false); + assert_ok!(feed_values(1, 0, 0, vec![(key, 1000)])); + assert_ok!(feed_values(2, 1, 0, vec![(key, 1000)])); + assert_ok!(feed_values(3, 2, 0, vec![(key, 1000)])); + assert_eq!(ModuleOracle::is_updated(key), false); + assert_eq!( + ModuleOracle::get(&key).unwrap(), + TimestampedValue { + value: 1000, + timestamp: 12345 + } + ); + assert_eq!(ModuleOracle::is_updated(key), true); + ModuleOracle::on_finalize(1); + assert_ok!(feed_values(1, 0, 1, vec![(key, 1000)])); + assert_eq!(ModuleOracle::is_updated(key), false); }); } #[test] -fn should_read_raw_values() { +fn should_validate_index() { new_test_ext().execute_with(|| { - let key: u32 = 1; + assert_noop!( + feed_values(1, 1, 0, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); - let raw_values = ModuleOracle::read_raw_values(&key); - assert_eq!(raw_values, vec![]); + assert_noop!( + feed_values(2, 0, 0, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + }); +} - Timestamp::set_timestamp(12345); +#[test] +fn should_validate_nonce() { + new_test_ext().execute_with(|| { + assert_noop!( + feed_values(1, 0, 1, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); - let expected = vec![ - TimestampedValue { - value: 1000, - timestamp: 12345, - }, - TimestampedValue { - value: 1200, - timestamp: 12345, - }, - ]; + assert_ok!(feed_values(1, 0, 0, vec![(50, 1000)])); - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), key, 1000)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(2), key, 1200)); + assert_eq!(ModuleOracle::nonces(&1), 1); + ModuleOracle::on_finalize(1); - let raw_values = ModuleOracle::read_raw_values(&key); - assert_eq!(raw_values, expected); + assert_noop!( + feed_values(1, 0, 0, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + + assert_ok!(feed_values(1, 0, 1, vec![(50, 1000)])); + + assert_eq!(ModuleOracle::nonces(&1), 2); }); } #[test] -fn should_combined_data() { +fn should_read_raw_values() { new_test_ext().execute_with(|| { - Timestamp::set_timestamp(12345); + let key: u32 = 50; - let expected = Some(TimestampedValue { - value: 1200, - timestamp: 12345, - }); + let raw_values = ModuleOracle::read_raw_values(&key); + assert_eq!(raw_values, vec![]); - let key: u32 = 1; + assert_ok!(feed_values(1, 0, 0, vec![(key, 1000)])); + assert_ok!(feed_values(2, 1, 0, vec![(key, 1200)])); - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), key, 1300)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(2), key, 1000)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(3), key, 1200)); - assert_eq!(ModuleOracle::get(&key), expected); + let raw_values = ModuleOracle::read_raw_values(&key); + assert_eq!( + raw_values, + vec![ + TimestampedValue { + value: 1000, + timestamp: 12345, + }, + TimestampedValue { + value: 1200, + timestamp: 12345, + }, + ] + ); }); } #[test] -fn should_return_prev_value() { +fn should_combined_data() { new_test_ext().execute_with(|| { - Timestamp::set_timestamp(12345); + let key: u32 = 50; + + assert_ok!(feed_values(1, 0, 0, vec![(key, 1300)])); + assert_ok!(feed_values(2, 1, 0, vec![(key, 1000)])); + assert_ok!(feed_values(3, 2, 0, vec![(key, 1200)])); let expected = Some(TimestampedValue { value: 1200, timestamp: 12345, }); - let key: u32 = 1; - - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), key, 1300)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(2), key, 1000)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(3), key, 1200)); assert_eq!(ModuleOracle::get(&key), expected); Timestamp::set_timestamp(23456); - // should return prev_value assert_eq!(ModuleOracle::get(&key), expected); }); } #[test] -fn should_return_none() { - new_test_ext().execute_with(|| { - let key: u32 = 1; - assert_eq!(ModuleOracle::get(&key), None); - }); -} - -#[test] -fn should_validate() { - new_test_ext().execute_with(|| { - let call = Call::ModuleOracle(OracleCall::feed_values(vec![(1, 1)])); - let info = <Call as GetDispatchInfo>::get_dispatch_info(&call); - - assert_eq!( - CheckOperator::<Test>(Default::default()).validate(&1, &call, &info, 1), - Ok(ValidTransaction { - priority: TransactionPriority::max_value(), - ..Default::default() - }) - ); - }); -} - -#[test] -fn should_be_free_operational() { +fn should_return_none_for_non_exist_key() { new_test_ext().execute_with(|| { - let feed_value = Call::ModuleOracle(OracleCall::feed_value(1, 1)); - let feed_values = Call::ModuleOracle(OracleCall::feed_values(vec![(1, 1)])); - vec![feed_value, feed_values].iter().for_each(|f| { - let dispatch_info = <Call as GetDispatchInfo>::get_dispatch_info(&f); - assert_eq!( - dispatch_info, - DispatchInfo { - weight: 0, - class: DispatchClass::Operational, - pays_fee: Pays::No, - } - ); - }); + assert_eq!(ModuleOracle::get(&50), None); }); } #[test] fn multiple_calls_should_fail() { new_test_ext().execute_with(|| { - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), 1, 1000)); + assert_ok!(feed_values(1, 0, 0, vec![(50, 1300)])); assert_noop!( - ModuleOracle::feed_value(Origin::signed(1), 1, 1200), - Error::<Test>::UpdateAlreadyDispatched + feed_values(1, 0, 1, vec![(50, 1300)]), + TransactionValidityError::Invalid(InvalidTransaction::Stale) ); - <ModuleOracle as OnFinalize<u64>>::on_finalize(1); - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), 1, 1200)); + + ModuleOracle::on_finalize(1); + + assert_ok!(feed_values(1, 0, 1, vec![(50, 1300)])); }); } #[test] fn get_all_values_should_work() { new_test_ext().execute_with(|| { - Timestamp::set_timestamp(12345); - let eur: u32 = 1; let jpy: u32 = 2; assert_eq!(ModuleOracle::get_all_values(), vec![]); // feed eur & jpy - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), eur, 1300)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(2), eur, 1000)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(3), jpy, 9000)); + assert_ok!(feed_values(1, 0, 0, vec![(eur, 1300)])); + assert_ok!(feed_values(2, 1, 0, vec![(eur, 1000)])); + assert_ok!(feed_values(3, 2, 0, vec![(jpy, 9000)])); // not enough eur & jpy prices assert_eq!(ModuleOracle::get(&eur), None); @@ -228,11 +235,11 @@ fn get_all_values_should_work() { assert_eq!(ModuleOracle::get_all_values(), vec![]); // finalize block - <ModuleOracle as OnFinalize<u64>>::on_finalize(1); + ModuleOracle::on_finalize(1); // feed eur & jpy - assert_ok!(ModuleOracle::feed_value(Origin::signed(3), eur, 1200)); - assert_ok!(ModuleOracle::feed_value(Origin::signed(1), jpy, 8000)); + assert_ok!(feed_values(3, 2, 1, vec![(eur, 1200)])); + assert_ok!(feed_values(1, 0, 1, vec![(jpy, 8000)])); // enough eur prices let eur_price = Some(TimestampedValue { @@ -247,7 +254,7 @@ fn get_all_values_should_work() { assert_eq!(ModuleOracle::get_all_values(), vec![(eur, eur_price)]); // feed jpy - assert_ok!(ModuleOracle::feed_value(Origin::signed(2), jpy, 7000)); + assert_ok!(feed_values(2, 1, 1, vec![(jpy, 7000)])); // enough jpy prices let jpy_price = Some(TimestampedValue { @@ -259,3 +266,88 @@ fn get_all_values_should_work() { assert_eq!(ModuleOracle::get_all_values(), vec![(eur, eur_price), (jpy, jpy_price)]); }); } + +#[test] +fn bad_index() { + new_test_ext().execute_with(|| { + assert_noop!( + feed_values(1, 255, 0, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + }); +} + +#[test] +fn change_member_should_work() { + new_test_ext().execute_with(|| { + <ModuleOracle as ChangeMembers<u64>>::change_members_sorted(&[4], &[1], &[2, 3, 4]); + + assert_noop!( + feed_values_from_session_key(10.into(), 0, 0, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + + assert_ok!(feed_values(2, 0, 0, vec![(50, 1000)])); + + assert_noop!( + feed_values_from_session_key(40.into(), 2, 0, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + + assert_eq!(ModuleOracle::session_keys(&4), None); + + assert_ok!(ModuleOracle::set_session_key(Origin::signed(4), 40.into())); + + assert_ok!(feed_values(4, 2, 0, vec![(50, 1000)])); + }); +} + +#[test] +fn should_clear_is_updated_on_change_member() { + new_test_ext().execute_with(|| { + assert_ok!(feed_values(1, 0, 0, vec![(50, 1000)])); + assert_ok!(feed_values(2, 1, 0, vec![(50, 1000)])); + assert_ok!(feed_values(3, 2, 0, vec![(50, 1000)])); + + assert_eq!( + ModuleOracle::get(&50).unwrap(), + TimestampedValue { + value: 1000, + timestamp: 12345 + } + ); + assert_eq!(ModuleOracle::is_updated(50), true); + + ModuleOracle::change_members_sorted(&[4], &[1], &[2, 3, 4]); + + assert_eq!(ModuleOracle::is_updated(50), false); + }); +} + +#[test] +fn should_clear_data_for_removed_members() { + new_test_ext().execute_with(|| { + assert_ok!(feed_values(1, 0, 0, vec![(50, 1000)])); + assert_ok!(feed_values(2, 1, 0, vec![(50, 1000)])); + + ModuleOracle::change_members_sorted(&[4], &[1], &[2, 3, 4]); + + assert_eq!(ModuleOracle::raw_values(&1, 50), None); + assert_eq!(ModuleOracle::session_keys(&1), None); + assert_eq!(ModuleOracle::nonces(&1), 0); + }); +} + +#[test] +fn change_session_key() { + new_test_ext().execute_with(|| { + assert_ok!(ModuleOracle::set_session_key(Origin::signed(1), 11.into())); + + assert_noop!( + feed_values_from_session_key(10.into(), 0, 0, vec![(50, 1000)]), + TransactionValidityError::Invalid(InvalidTransaction::BadProof) + ); + + assert_ok!(feed_values_from_session_key(11.into(), 0, 0, vec![(50, 1000)])); + }); +} diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs index 910f988..7fb5b28 100644 --- a/utilities/src/lib.rs +++ b/utilities/src/lib.rs @@ -6,3 +6,4 @@ pub mod ordered_set; pub use fixed_u128::FixedU128; pub use linked_item::{LinkedItem, LinkedList}; +pub use ordered_set::OrderedSet; diff --git a/utilities/src/ordered_set.rs b/utilities/src/ordered_set.rs index 56ec866..d8ea421 100644 --- a/utilities/src/ordered_set.rs +++ b/utilities/src/ordered_set.rs @@ -3,7 +3,7 @@ use sp_runtime::RuntimeDebug; use sp_std::prelude::*; #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Default)] -pub struct OrderedSet<T>(Vec<T>); +pub struct OrderedSet<T>(pub Vec<T>); impl<T: Ord> OrderedSet<T> { pub fn new() -> Self { -- GitLab