Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make feed_value FreeOperational #86

Merged
merged 14 commits into from
Feb 16, 2020
4 changes: 2 additions & 2 deletions oracle/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ serde = { version = "1.0", optional = true }
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false }
sp-runtime = { git = "https://github.com/paritytech/substrate.git", default-features = false }
runtime-io = { package = "sp-io", git = "https://github.com/paritytech/substrate.git", default-features = false }
rstd = { package = "sp-std", git = "https://github.com/paritytech/substrate.git", default-features = false }
sp-std = { git = "https://github.com/paritytech/substrate.git", default-features = false }

frame-support = { git = "https://github.com/paritytech/substrate.git", default-features = false }
frame-system = { git = "https://github.com/paritytech/substrate.git", default-features = false }
Expand All @@ -30,7 +30,7 @@ std = [
"codec/std",
"sp-runtime/std",
"runtime-io/std",
"rstd/std",
"sp-std/std",
"frame-support/std",
"frame-system/std",
"orml-traits/std",
Expand Down
4 changes: 2 additions & 2 deletions oracle/src/default_combine_data.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use frame_support::traits::{Get, Time};
use orml_traits::CombineData;
use rstd::prelude::*;
use sp_std::prelude::*;

use crate::{MomentOf, TimestampedValueOf, Trait};

/// Sort by value and returns median timestamped value.
/// Returns prev_value if not enough valid values.
pub struct DefaultCombineData<T, MinimumCount, ExpiresIn>(rstd::marker::PhantomData<(T, MinimumCount, ExpiresIn)>);
pub struct DefaultCombineData<T, MinimumCount, ExpiresIn>(sp_std::marker::PhantomData<(T, MinimumCount, ExpiresIn)>);

impl<T, MinimumCount, ExpiresIn> CombineData<T::OracleKey, TimestampedValueOf<T>>
for DefaultCombineData<T, MinimumCount, ExpiresIn>
Expand Down
106 changes: 100 additions & 6 deletions oracle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,40 @@ 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, ensure, traits::Time, Parameter};
use frame_support::{
decl_error, decl_event, decl_module, decl_storage,
dispatch::Dispatchable,
ensure,
traits::Time,
weights::{DispatchClass, DispatchInfo, FunctionOf, TransactionPriority},
IsSubType, Parameter,
};
pub use operator_provider::OperatorProvider;
use rstd::{prelude::*, vec};
use sp_runtime::{traits::Member, DispatchResult};
use sp_runtime::{
traits::{Member, SignedExtension},
DispatchResult,
};
use sp_std::{prelude::*, 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 orml_traits::{CombineData, DataProvider, OnNewData};
pub use orml_traits::{CombineData, DataProvider, OnNewData, OnRedundantCall};
use sp_runtime::transaction_validity::{
InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction,
};
pub use timestamped_value::TimestampedValue;

type MomentOf<T> = <<T as Trait>::Time as Time>::Moment;
pub type TimestampedValueOf<T> = TimestampedValue<<T as Trait>::OracleValue, MomentOf<T>>;

pub trait Trait: frame_system::Trait {
type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
type OnNewData: OnNewData<Self::OracleKey, Self::OracleValue>;
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;
Expand All @@ -36,30 +52,45 @@ decl_storage! {
pub RawValues get(raw_values): double_map hasher(blake2_256) T::OracleKey, hasher(blake2_256) T::AccountId => Option<TimestampedValueOf<T>>;
pub HasUpdate get(has_update): map hasher(blake2_256) T::OracleKey => bool;
pub Values get(values): map hasher(blake2_256) T::OracleKey => Option<TimestampedValueOf<T>>;
HasDispatched: Vec<T::AccountId>;
}
}

decl_error! {
// Oracle module errors
pub enum Error for Module<T: Trait> {
NoPermission,
UpdateAlreadyDispatched,
}
}

#[repr(u8)]
pub enum ValidityError {
NoPermission,
}

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(|_: (&T::OracleKey, &T::OracleValue)| 0, DispatchClass::Operational, false)]
pub fn feed_value(origin, key: T::OracleKey, value: T::OracleValue) {
let who = ensure_signed(origin)?;
Self::_feed_values(who, vec![(key, value)])?;
}

#[weight = FunctionOf(|_: (&Vec<(T::OracleKey, T::OracleValue)>,)| 0, DispatchClass::Operational, false)]
pub fn feed_values(origin, values: Vec<(T::OracleKey, T::OracleValue)>) {
let who = ensure_signed(origin)?;
Self::_feed_values(who, values)?;
}

fn on_finalize(_n: T::BlockNumber) {
// cleanup for next block
<HasDispatched<T>>::kill();
}
}
}

Expand Down Expand Up @@ -111,6 +142,60 @@ impl<T: Trait> Module<T> {
}
}

#[derive(Encode, Decode, Clone, Eq, PartialEq)]
pub struct CheckOperator<T: Trait + Send + Sync>(sp_std::marker::PhantomData<T>);

impl<T: Trait + Send + Sync> sp_std::fmt::Debug for CheckOperator<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
write!(f, "CheckOperator")
}

#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
Ok(())
}
}

impl<T: Trait + Send + Sync> SignedExtension for CheckOperator<T> {
const IDENTIFIER: &'static str = "CheckOperator";
type AccountId = T::AccountId;
type Call = <T as Trait>::Call;
type AdditionalSigned = ();
type Pre = ();
type DispatchInfo = DispatchInfo;

fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {
Ok(())
}

fn validate(
&self,
who: &T::AccountId,
call: &Self::Call,
_info: Self::DispatchInfo,
_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()
});
}
return 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)
Expand All @@ -121,6 +206,15 @@ 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 {
Expand All @@ -131,7 +225,7 @@ impl<T: Trait> Module<T> {
<RawValues<T>>::insert(&key, &who, timestamped);
<HasUpdate<T>>::insert(&key, true);

T::OnNewData::on_new_data(&key, &value);
T::OnNewData::on_new_data(&who, &key, &value);
}

Self::deposit_event(RawEvent::NewFeedData(who, values));
Expand Down
22 changes: 15 additions & 7 deletions oracle/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use super::*;

use frame_support::{impl_outer_origin, parameter_types, weights::Weight};
use frame_support::{impl_outer_dispatch, impl_outer_origin, parameter_types, weights::Weight};
use primitives::H256;
use sp_runtime::{
testing::Header,
Expand All @@ -14,10 +14,18 @@ impl_outer_origin! {
pub enum Origin for Test {}
}

impl_outer_dispatch! {
pub enum Call for Test where origin: Origin {
oracle::ModuleOracle,
}
}

pub type OracleCall = super::Call<Test>;

// 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
// configuration traits of modules we want to use.
#[derive(Clone, Eq, PartialEq)]
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Test;
parameter_types! {
pub const BlockHashCount: u64 = 250;
Expand Down Expand Up @@ -79,20 +87,20 @@ parameter_types! {

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 OracleKey = Key;
type OracleValue = Value;
}
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() -> runtime_io::TestExternalities {
frame_system::GenesisConfig::default()
.build_storage::<Test>()
.unwrap()
.into()
let r = frame_system::GenesisConfig::default().build_storage::<Test>();

r.unwrap().into()
}
2 changes: 1 addition & 1 deletion oracle/src/operator_provider.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rstd::prelude::Vec;
use sp_std::prelude::Vec;

pub trait OperatorProvider<AccountId> {
// Make sure `who` has permission to feed data
Expand Down
64 changes: 60 additions & 4 deletions oracle/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
#![cfg(test)]

use crate::mock::{new_test_ext, ModuleOracle, Origin, Timestamp};

use crate::TimestampedValue;
use frame_support::assert_ok;
use crate::{
mock::{new_test_ext, Call, ModuleOracle, OracleCall, Origin, Test, Timestamp},
{CheckOperator, Error, TimestampedValue},
};
use frame_support::{
assert_noop, assert_ok,
weights::{DispatchClass, DispatchInfo, GetDispatchInfo, TransactionPriority},
};
use sp_runtime::{
traits::{OnFinalize, SignedExtension},
transaction_validity::ValidTransaction,
};

#[test]
fn should_feed_value() {
Expand Down Expand Up @@ -152,3 +160,51 @@ fn should_return_none() {
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() {
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: false,
}
);
});
});
}

#[test]
fn multiple_calls_should_fail() {
new_test_ext().execute_with(|| {
assert_ok!(ModuleOracle::feed_value(Origin::signed(1), 1, 1000));
assert_noop!(
ModuleOracle::feed_value(Origin::signed(1), 1, 1200),
Error::<Test>::UpdateAlreadyDispatched
);
<ModuleOracle as OnFinalize<u64>>::on_finalize(1);
assert_ok!(ModuleOracle::feed_value(Origin::signed(1), 1, 1200));
});
}
9 changes: 7 additions & 2 deletions traits/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ pub trait BasicCurrencyExtended<AccountId>: BasicCurrency<AccountId> {
}

#[impl_trait_for_tuples::impl_for_tuples(30)]
pub trait OnNewData<Key, Value> {
fn on_new_data(key: &Key, value: &Value);
pub trait OnNewData<AccountId, Key, Value> {
fn on_new_data(who: &AccountId, key: &Key, value: &Value);
}

pub trait DataProvider<Key, Value> {
Expand Down Expand Up @@ -156,3 +156,8 @@ pub trait OnDustRemoval<Balance> {
impl<Balance> OnDustRemoval<Balance> for () {
fn on_dust_removal(_: Balance) {}
}

#[impl_trait_for_tuples::impl_for_tuples(30)]
pub trait OnRedundantCall<AccountId> {
fn multiple_calls_per_block(who: &AccountId);
}