diff --git a/Cargo.lock b/Cargo.lock index 93a8e5e988305..11b5cc603fb9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6227,7 +6227,24 @@ dependencies = [ ] [[package]] -name = "pallet-example-offchain-worker" +name = "pallet-example-offchain-worker-ping-pong" +version = "4.0.0-dev" +dependencies = [ + "frame-support", + "frame-system", + "lite-json", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-example-offchain-worker-price-oracle" version = "4.0.0-dev" dependencies = [ "frame-support", @@ -6266,7 +6283,8 @@ dependencies = [ "pallet-dev-mode", "pallet-example-basic", "pallet-example-kitchensink", - "pallet-example-offchain-worker", + "pallet-example-offchain-worker-ping-pong", + "pallet-example-offchain-worker-price-oracle", "pallet-example-split", ] diff --git a/Cargo.toml b/Cargo.toml index bbbb8563f5b28..fc82b601ead84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,8 @@ members = [ "frame/election-provider-support/solution-type/fuzzer", "frame/examples", "frame/examples/basic", - "frame/examples/offchain-worker", + "frame/examples/offchain-worker-ping-pong", + "frame/examples/offchain-worker-price-oracle", "frame/examples/kitchensink", "frame/examples/dev-mode", "frame/examples/split", diff --git a/frame/examples/Cargo.toml b/frame/examples/Cargo.toml index af67bef792b6f..3307527f3edf9 100644 --- a/frame/examples/Cargo.toml +++ b/frame/examples/Cargo.toml @@ -14,7 +14,8 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] pallet-example-basic = { default-features = false, path = "./basic" } pallet-default-config-example = { default-features = false, path = "./default-config" } -pallet-example-offchain-worker = { default-features = false, path = "./offchain-worker" } +pallet-example-offchain-worker-ping-pong = { default-features = false, path = "./offchain-worker-ping-pong" } +pallet-example-offchain-worker-price-oracle = { default-features = false, path = "./offchain-worker-price-oracle" } pallet-example-kitchensink = { default-features = false, path = "./kitchensink" } pallet-dev-mode = { default-features = false, path = "./dev-mode" } pallet-example-split = { default-features = false, path = "./split" } @@ -24,7 +25,8 @@ default = [ "std" ] std = [ "pallet-example-basic/std", "pallet-default-config-example/std", - "pallet-example-offchain-worker/std", + "pallet-example-offchain-worker-ping-pong/std", + "pallet-example-offchain-worker-price-oracle/std", "pallet-example-kitchensink/std", "pallet-dev-mode/std", "pallet-example-split/std", @@ -32,7 +34,8 @@ std = [ try-runtime = [ "pallet-example-basic/try-runtime", "pallet-default-config-example/try-runtime", - "pallet-example-offchain-worker/try-runtime", + "pallet-example-offchain-worker-ping-pong/try-runtime", + "pallet-example-offchain-worker-price-oracle/try-runtime", "pallet-example-kitchensink/try-runtime", "pallet-dev-mode/try-runtime", "pallet-example-split/try-runtime", diff --git a/frame/examples/offchain-worker-ping-pong/Cargo.toml b/frame/examples/offchain-worker-ping-pong/Cargo.toml new file mode 100644 index 0000000000000..ffc7b7e808858 --- /dev/null +++ b/frame/examples/offchain-worker-ping-pong/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "pallet-example-offchain-worker-ping-pong" +version = "1.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "MIT-0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME example pallet for offchain worker (ping-pong)" +readme = "README.md" +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } +lite-json = { version = "0.2.0", default-features = false } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../../system" } +sp-core = { version = "21.0.0", default-features = false, path = "../../../primitives/core" } +sp-io = { version = "23.0.0", default-features = false, path = "../../../primitives/io" } +sp-keystore = { version = "0.27.0", optional = true, path = "../../../primitives/keystore" } +sp-runtime = { version = "24.0.0", default-features = false, path = "../../../primitives/runtime" } +sp-std = { version = "8.0.0", default-features = false, path = "../../../primitives/std" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "lite-json/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-keystore", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md new file mode 100644 index 0000000000000..56526fd5ba325 --- /dev/null +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -0,0 +1,52 @@ +# Ping-Pong Offchain Worker Example Pallet + +A simple pallet demonstrating concepts, APIs and structures common to most offchain workers. + +Run `cargo doc --package pallet-example-offchain-worker-ping-pong --open` to view this module's +documentation. + +This is a simple example pallet to showcase how the runtime can and should interact with an offchain worker asynchronously. +It also showcases the potential pitfalls and security considerations that come with it. + +It is based on [this example by `gnunicorn`](https://gnunicorn.github.io/substrate-offchain-cb/), +although an updated version with a few modifications. + +The example plays simple ping-pong with off-chain workers: +Once a signed transaction to `ping` is submitted (by any user), Ping request is written into Storage. +Each ping request has a `nonce`, which is arbitrarily chosen by the user (not necessarily unique). + +After every block, the offchain worker is triggered. If it sees a Ping request in the current +block, it reacts by sending a transaction to send a Pong with the corresponding `nonce`. When `pong_*` extrinsics are executed, +they emit an `PongAck*` event so we can track with existing UIs. + +The `PongAck*` events come in two different flavors: +- `PongAckAuthenticated`: emitted when the call was made by an **authenticated** offchain worker (whitelisted via `Authorities` storage) +- `PongAckUnauthenticated`: emitted when the call was made by an **unauthenticated** offchain worker (or potentially malicious actors) + +The security implications of `PongAckUnauthenticated` should be obvious: not **ONLY** offchain workers can +call `pong_unsigned_*`. **ANYONE** can do it, and they can actually use a different `nonce` +from the original ping (try it yourself!). If the `nonce` actually had an important meaning to the state of our chain, this would be a **VULNERABILITY**! + +This is meant to highlight the importance of solid security assumptions when using unsigned transactions. +In other words: + +⚠️ **DO NOT USE UNSIGNED TRANSACTIONS UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!** ⚠️ + +Here's an example of how a node admin can inject some keys into the keystore, so that the OCW +can call `pong_signed`: + +```bash +$ curl --location --request POST 'http://localhost:9944' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc": "2.0", + "method": "author_insertKey", + "params": ["pong","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], + "id": 1 +}' +``` + +Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) has funds and is added to `Authorities` in the runtime by adding it via `add_authority` extrinsic (from `root`). + +More complex management models and session +based key rotations should be considered, but that's outside the scope of this example. \ No newline at end of file diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs new file mode 100644 index 0000000000000..b9664dded852d --- /dev/null +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -0,0 +1,655 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +//! # Offchain Worker Example Pallet +//! +//! The Ping-Pong Offchain Worker Example: A simple pallet demonstrating concepts, APIs and +//! structures common to most offchain workers. +//! +//! Run `cargo doc --package pallet-example-offchain-worker-ping-pong --open` to view this module's +//! documentation. +//! +//! **This pallet serves as an example showcasing Substrate off-chain worker and is not meant to be +//! used in production.** +//! +//! ## Overview +//! +//! This is a simple example pallet to showcase how the runtime can and should interact with an +//! offchain worker asynchronously. It also showcases the potential pitfalls and security +//! considerations that come with it. +//! +//! It is based on [this example by +//! `gnunicorn`](https://gnunicorn.github.io/substrate-offchain-cb/), although an updated version +//! with a few modifications. +//! +//! The example plays simple ping-pong with off-chain workers: Once a signed transaction to `ping` +//! is submitted (by any user), a Ping request is written into Storage. Each Ping request has a +//! `nonce`, which is arbitrarily chosen by the user (not necessarily unique). +//! +//! After every block, the offchain worker is triggered. If it sees a Ping request in the current +//! block, it reacts by sending a transaction to send a Pong with the corresponding `nonce`. When +//! `pong_*` extrinsics are executed, they emit an `PongAck*` event so we can track with existing +//! UIs. +//! +//! The `PongAck*` events come in two different flavors: +//! - `PongAckAuthenticated`: emitted when the call was made by an **authenticated** offchain worker +//! (whitelisted via `Authorities` storage) +//! - `PongAckUnauthenticated`: emitted when the call was made by an **unauthenticated** offchain +//! worker (or potentially malicious actors +//! +//! The security implications from `PongAckUnauthenticated` should be obvious: not **ONLY** offchain +//! workers can call `pong_unsigned*`. **ANYONE** can do it, and they can actually use a different +//! `nonce` from the original ping (try it yourself!). If the `nonce` actually had some important +//! meaning to the state of our chain, this would be a **VULNERABILITY**. +//! +//! Also, unsigned transactions can easily become a vector for DoS attacks! +//! +//! This is meant to highlight the importance of solid security assumptions when using unsigned +//! transactions. In other words: +//! +//! ⚠️ **DO NOT USE UNSIGNED TRANSACTIONS UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!** ⚠️ +//! +//! Here's an example of how a node admin can inject some keys into the keystore, so that the OCW +//! can call `pong_signed`: +//! +//! ```bash +//! $ curl --location --request POST 'http://localhost:9944' \ +//! --header 'Content-Type: application/json' \ +//! --data-raw '{ +//! "jsonrpc": "2.0", +//! "method": "author_insertKey", +//! "params": [ +//! "pong", +//! "bread tongue spell stadium clean grief coin rent spend total practice document", +//! "0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30" +//! ], +//! "id": 1 +//! }' +//! ``` +//! +//! Then make sure that the corresponding address +//! (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) has funds and is added to `Authorities` in +//! the runtime by adding it via `add_authority` extrinsic (from `root`). +//! +//! More complex management models and session based key rotations should be considered, but that's +//! outside the scope of this example. + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use frame_support::traits::Get; +use frame_system::{ + self as system, + offchain::{ + AppCrypto, CreateSignedTransaction, SendSignedTransaction, SendUnsignedTransaction, + SignedPayload, Signer, SigningTypes, SubmitTransaction, + }, + pallet_prelude::*, +}; +use sp_core::crypto::KeyTypeId; +use sp_runtime::{ + traits::Zero, + transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, + RuntimeDebug, +}; + +#[cfg(test)] +mod tests; + +/// Defines application identifier for crypto keys of this module. +/// +/// Every module that deals with signatures needs to declare its unique identifier for its crypto +/// keys. When offchain worker is signing transactions it's going to request keys of type +/// `KeyTypeId` from the keystore and use the ones it finds to sign the transaction. The keys can be +/// inserted manually via RPC (see `author_insertKey`). +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"pong"); + +/// Based on the above `KeyTypeId` we need to generate a pallet-specific crypto type wrappers. We +/// can use from supported crypto kinds (`sr25519`, `ed25519` and `ecdsa`) and augment the types +/// with this pallet-specific identifier. +pub mod crypto { + use super::KEY_TYPE; + use sp_core::sr25519::Signature as Sr25519Signature; + use sp_runtime::{ + app_crypto::{app_crypto, sr25519}, + traits::Verify, + MultiSignature, MultiSigner, + }; + app_crypto!(sr25519, KEY_TYPE); + + pub struct TestAuthId; + + impl frame_system::offchain::AppCrypto for TestAuthId { + type RuntimeAppPublic = Public; + type GenericSignature = sp_core::sr25519::Signature; + type GenericPublic = sp_core::sr25519::Public; + } + + // implemented for mock runtime in test + impl frame_system::offchain::AppCrypto<::Signer, Sr25519Signature> + for TestAuthId + { + type RuntimeAppPublic = Public; + type GenericSignature = sp_core::sr25519::Signature; + type GenericPublic = sp_core::sr25519::Public; + } +} + +pub use pallet::*; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + + /// This pallet's configuration trait + #[pallet::config(with_default)] + pub trait Config: CreateSignedTransaction> + frame_system::Config { + /// The identifier type for an offchain worker. + #[pallet::no_default] + type AuthorityId: AppCrypto; + + /// The overarching event type. + #[pallet::no_default] + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + // Configuration parameters + + /// Number of blocks of cooldown after unsigned transaction is included. + /// + /// This ensures that we only accept unsigned transactions once, every `UnsignedInterval` + /// blocks. + #[pallet::no_default] + #[pallet::constant] + type UnsignedInterval: Get>; + + /// 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. + #[pallet::constant] + type UnsignedPriority: Get; + + /// Maximum number of pings on the same block. + #[pallet::constant] + type MaxPings: Get; + + /// Maximum number of authorities. + #[pallet::constant] + type MaxAuthorities: Get; + } + + #[pallet::error] + pub enum Error { + NotAuthority, + AlreadyAuthority, + TooManyAuthorities, + TooManyPings, + } + + #[derive(Clone, Debug, Encode, Decode, PartialEq, Eq, scale_info::TypeInfo)] + pub enum UnsignedType { + UnsignedWithSignedPayload, + RawUnsigned, + } + + /// Events for the pallet. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Event generated when new ping is received. + Ping { nonce: u32 }, + /// Event generated when new pong_signed transaction is accepted. + PongAckAuthenticated { nonce: u32 }, + /// Event generated when new pong_unsigned* transaction is accepted. + PongAckUnauthenticated { nonce: u32, unsigned_type: UnsignedType }, + /// Event generated when a new authority is added. + AuthorityAdded { authority: T::AccountId }, + /// Event generated when an authority is removed. + AuthorityRemoved { authority: T::AccountId }, + } + + /// A struct for wrapping the ping nonce. + #[derive(Encode, Decode, MaxEncodedLen, TypeInfo)] + pub struct Ping(pub u32); + + /// A vector of recently submitted pings. + #[pallet::storage] + #[pallet::getter(fn pings)] + pub(super) type Pings = StorageValue<_, BoundedVec, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn authorities)] + pub(super) type Authorities = + StorageValue<_, BoundedVec, ValueQuery>; + + /// Defines the block when next unsigned transaction will be accepted. + /// + /// To prevent spam of unsigned (and unpaid!) transactions on the network, we only allow one + /// transaction every `T::UnsignedInterval` blocks. This storage entry defines when new + /// transaction is going to be accepted. + #[pallet::storage] + #[pallet::getter(fn next_unsigned_at)] + pub(super) type NextUnsignedAt = StorageValue<_, BlockNumberFor, ValueQuery>; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Offchain Worker entry point. + /// + /// By implementing `fn offchain_worker` you declare a new offchain worker. This function + /// will be called when the node is fully synced and a new best block is successfully + /// imported. Note that it's not guaranteed for offchain workers to run on EVERY block, + /// there might be cases where some blocks are skipped, or for some the worker runs twice + /// (re-orgs), so the code should be able to handle that. You can use `Local Storage` API to + /// coordinate runs of the worker. + fn offchain_worker(block_number: BlockNumberFor) { + // Note that having logs compiled to WASM may cause the size of the blob to increase + // significantly. You can use `RuntimeDebug` custom derive to hide details of the types + // in WASM. The `sp-api` crate also provides a feature `disable-logging` to disable all + // logging and thus, remove any logging from the WASM. + log::info!("Hello World from offchain workers!"); + + // Since off-chain workers are just part of the runtime code, they have direct access to + // the storage and other included pallets. + // + // We can easily import `frame_system` and retrieve a block hash of the parent block. + let parent_hash = >::block_hash(block_number - 1u32.into()); + log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash); + + // if `NextUnsignedAt` allows, try to send some unsigned pong + let next_unsigned_at = >::get(); + if next_unsigned_at <= block_number { + // we choose which kind of unsigned pong based on block_number + let unsigned_type = block_number % 3u32.into(); + if unsigned_type == Zero::zero() { + if let Err(e) = Self::ocw_pong_raw_unsigned(block_number) { + log::error!("Error: {}", e); + } + } else if unsigned_type == BlockNumberFor::::from(1u32) { + // node needs to be loaded with keys as the payload will be signed + if let Err(e) = Self::ocw_pong_unsigned_for_any_account(block_number) { + log::error!("Error: {}", e); + } + } else if unsigned_type == BlockNumberFor::::from(2u32) { + // node needs to be loaded with keys as the payload will be signed + if let Err(e) = Self::ocw_pong_unsigned_for_all_accounts(block_number) { + log::error!("Error: {}", e); + } + } + } + + // try to send a pong_signed (node needs to be loaded with keys, account needs to be + // funded) + if let Err(e) = Self::ocw_pong_signed() { + log::error!("Error: {}", e); + } + } + + /// clean Pings + fn on_initialize(_: BlockNumberFor) -> Weight { + Pings::::kill(); + Weight::zero() + } + } + + /// A public part of the pallet. + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + pub fn ping(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { + let _who = ensure_signed(origin)?; + + let mut pings = >::get(); + match pings.try_push(pallet::Ping(nonce)) { + Ok(()) => (), + Err(_) => return Err(Error::::TooManyPings.into()), + }; + + Pings::::set(pings); + + Self::deposit_event(Event::Ping { nonce }); + + Ok(().into()) + } + + #[pallet::call_index(1)] + pub fn pong_signed(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + match Self::is_authority(&who) { + true => Self::deposit_event(Event::PongAckAuthenticated { nonce }), + false => return Err(Error::::NotAuthority.into()), + } + + // Authorized OCWs don't need to pay fees + Ok(Pays::No.into()) + } + + #[pallet::call_index(2)] + pub fn pong_unsigned( + origin: OriginFor, + _block_number: BlockNumberFor, + nonce: u32, + ) -> DispatchResultWithPostInfo { + // This ensures that the function can only be called via unsigned transaction. + ensure_none(origin)?; + + // Emit the PongAckUnauthenticated event + Self::deposit_event(Event::PongAckUnauthenticated { + nonce, + unsigned_type: UnsignedType::RawUnsigned, + }); + + // now increment the block number at which we expect next unsigned transaction. + let current_block = >::block_number(); + >::put(current_block + T::UnsignedInterval::get()); + Ok(().into()) + } + + #[pallet::call_index(3)] + pub fn pong_unsigned_with_signed_payload( + origin: OriginFor, + pong_payload: PongPayload>, + _signature: T::Signature, + ) -> DispatchResultWithPostInfo { + // This ensures that the function can only be called via unsigned transaction. + ensure_none(origin)?; + + Self::deposit_event(Event::PongAckUnauthenticated { + nonce: pong_payload.nonce, + unsigned_type: UnsignedType::UnsignedWithSignedPayload, + }); + + // now increment the block number at which we expect next unsigned transaction. + let current_block = >::block_number(); + >::put(current_block + T::UnsignedInterval::get()); + Ok(().into()) + } + + #[pallet::call_index(4)] + pub fn add_authority( + origin: OriginFor, + authority: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + ensure!(!Self::is_authority(&authority), Error::::AlreadyAuthority); + + let mut authorities = >::get(); + match authorities.try_push(authority.clone()) { + Ok(()) => (), + Err(_) => return Err(Error::::TooManyAuthorities.into()), + }; + + Authorities::::set(authorities); + + Self::deposit_event(Event::AuthorityAdded { authority }); + + Ok(().into()) + } + + #[pallet::call_index(5)] + pub fn remove_authority( + origin: OriginFor, + authority: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + ensure!(Self::is_authority(&authority), Error::::NotAuthority); + + let mut authorities = >::get(); + match authorities.iter().position(|a| a == &authority) { + Some(index) => authorities.swap_remove(index), + None => return Err(Error::::NotAuthority.into()), + }; + + Authorities::::set(authorities); + + Self::deposit_event(Event::AuthorityAdded { authority }); + + Ok(().into()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + /// Validate unsigned calls to this module. + /// + /// By default, unsigned transactions are disallowed, but implementing this function we make + /// sure that some particular calls are being whitelisted and marked as valid. + /// + /// ⚠ WARNING ⚠ Anyone could be sending these unsigned transactions, not only OCWs! + /// + /// When it comes to signed payloads, **we only check if the signature is coherent with the + /// signer, but we don't really check if the signer is an authorized OCW**! + /// + /// You should not interpret signed payloads as a filter that only allows transactions from + /// authorized OCWs. Anyone could have signed those payloads, even malicious actors trying + /// to "impersonate" an OCW. + /// + /// There are NO implicit security assumptions here! + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + // Firstly let's check that we call the right function. + if let Call::pong_unsigned_with_signed_payload { + pong_payload: ref payload, + ref signature, + } = call + { + // ⚠ WARNING ⚠ this is nothing but a "sanity check" on the signature it only checks + // if the signature is coherent with the public key of `SignedPayload` whoever that + // might be (not necessarily an authorized OCW) + let signature_valid = + SignedPayload::::verify::(payload, signature.clone()); + if !signature_valid { + return InvalidTransaction::BadProof.into(); + } + Self::validate_transaction_parameters(&payload.block_number) + } else if let Call::pong_unsigned { block_number, nonce: _n } = call { + Self::validate_transaction_parameters(block_number) + } else { + InvalidTransaction::Call.into() + } + } + } + + /// Container for different types that implement [`DefaultConfig`]` of this pallet. + pub mod config_preludes { + // This will help use not need to disambiguate anything when using `derive_impl`. + use super::*; + + /// A type providing default configurations for this pallet in testing environment. + pub struct TestDefaultConfig; + const UNSIGNED_PRIORITY: u64 = 1 << 20; + + #[frame_support::register_default_impl(TestDefaultConfig)] + impl DefaultConfig for TestDefaultConfig { + type UnsignedPriority = frame_support::traits::ConstU64; + type MaxPings = frame_support::traits::ConstU32<64>; + type MaxAuthorities = frame_support::traits::ConstU32<64>; + } + } +} + +/// Payload used by this example crate to hold pong response data required to submit an unsigned +/// transaction. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, scale_info::TypeInfo)] +pub struct PongPayload { + block_number: BlockNumber, + nonce: u32, + public: Public, +} + +impl SignedPayload for PongPayload> { + fn public(&self) -> T::Public { + self.public.clone() + } +} + +impl Pallet { + fn is_authority(who: &T::AccountId) -> bool { + >::get().contains(who) + } + + fn validate_transaction_parameters(block_number: &BlockNumberFor) -> TransactionValidity { + // Now let's check if the transaction has any chance to succeed. + let next_unsigned_at = >::get(); + if &next_unsigned_at > block_number { + return InvalidTransaction::Stale.into(); + } + // Let's make sure to reject transactions from the future. + let current_block = >::block_number(); + if ¤t_block < block_number { + return InvalidTransaction::Future.into(); + } + + ValidTransaction::with_tag_prefix("ExampleOffchainWorker") + // We set base priority to 2**20 and hope it's included before any other transactions in + // the pool. + .priority(T::UnsignedPriority::get()) + // This transaction does not require anything else to go before into the pool. In theory + // we could require `previous_unsigned_at` transaction to go first, but it's not + // necessary in our case. .and_requires() We set the `provides` tag to be the same as + //`next_unsigned_at`. This makes sure only one transaction produced after + // `next_unsigned_at` will ever get to the transaction pool and will end up in the + // block. We can still have multiple transactions compete for the same "spot", and the + // one with higher priority will replace other one in the pool. + .and_provides(next_unsigned_at) + // The transaction is only valid for next 5 blocks. After that it's going to be + // revalidated by the pool. + .longevity(5) + // It's fine to propagate that transaction to other peers, which means it can be created + // even by nodes that don't produce blocks. Note that sometimes it's better to keep it + // for yourself (if you are the block producer), since for instance in some schemes + // others may copy your solution and claim a reward. + .propagate(true) + .build() + } + + /// A helper function to send a signed pong transaction from the OCW. + fn ocw_pong_signed() -> Result<(), &'static str> { + let signer = Signer::::all_accounts(); + if !signer.can_sign() { + return Err( + "No local accounts available. Consider adding one via `author_insertKey` RPC.", + ); + } + + let pings = >::get(); + for p in pings { + let Ping(nonce) = p; + + // Using `send_signed_transaction` associated type we create and submit a transaction + // representing the call, we've just created. Submit signed will return a vector of + // results for all accounts that were found in the local keystore with expected + // `KEY_TYPE`. + let results = signer.send_signed_transaction(|_account| { + // nonce is wrapped into a call to `pong_signed` public function of this pallet. + // This means that the transaction, when executed, will simply call that function + // passing `nonce` as an argument. + Call::pong_signed { nonce } + }); + + for (acc, res) in &results { + match res { + Ok(()) => log::info!("[{:?}] Submitted pong with nonce {}", acc.id, nonce), + Err(e) => { + log::error!("[{:?}] Failed to submit transaction: {:?}", acc.id, e); + return Err("Failed to submit transaction"); + }, + } + } + } + + Ok(()) + } + + /// A helper function to sign payload and send an unsigned pong transaction + fn ocw_pong_unsigned_for_any_account( + block_number: BlockNumberFor, + ) -> Result<(), &'static str> { + let pings = >::get(); + for p in pings { + let Ping(nonce) = p; + + // -- Sign using any account + let (_, result) = Signer::::any_account() + .send_unsigned_transaction( + |account| PongPayload { nonce, block_number, public: account.public.clone() }, + |payload, signature| Call::pong_unsigned_with_signed_payload { + pong_payload: payload, + signature, + }, + ) + .ok_or("No local accounts accounts available.")?; + result.map_err(|()| "Unable to submit transaction")?; + } + + Ok(()) + } + + /// A helper function to sign payload and send an unsigned pong transaction + fn ocw_pong_unsigned_for_all_accounts( + block_number: BlockNumberFor, + ) -> Result<(), &'static str> { + let pings = >::get(); + for p in pings { + let Ping(nonce) = p; + + // -- Sign using all accounts + let transaction_results = Signer::::all_accounts() + .send_unsigned_transaction( + |account| PongPayload { nonce, block_number, public: account.public.clone() }, + |payload, signature| Call::pong_unsigned_with_signed_payload { + pong_payload: payload, + signature, + }, + ); + for (_account_id, result) in transaction_results.into_iter() { + if result.is_err() { + return Err("Unable to submit transaction"); + } + } + } + + Ok(()) + } + + /// A helper function to send a raw unsigned pong transaction. + fn ocw_pong_raw_unsigned(block_number: BlockNumberFor) -> Result<(), &'static str> { + let pings = >::get(); + for Ping(nonce) in pings { + // nonce is wrapped into a call to `pong_unsigned` public function of this pallet. This + // means that the transaction, when executed, will simply call that function passing + // `nonce` as an argument. + let call = Call::pong_unsigned { block_number, nonce }; + + // Now let's create a transaction out of this call and submit it to the pool. Here we + // showcase two ways to send an unsigned transaction / unsigned payload (raw) + // + // By default unsigned transactions are disallowed, so we need to whitelist this case by + // writing our implementation for the `ValidateUnsigned` trait. Note that it's EXTREMELY + // important to carefully implement unsigned validation logic, as any mistakes can lead + // to opening DoS or spam attack vectors. See validation logic docs for more details. + SubmitTransaction::>::submit_unsigned_transaction(call.into()) + .map_err(|()| "Unable to submit unsigned transaction.")?; + } + + Ok(()) + } +} diff --git a/frame/examples/offchain-worker/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs similarity index 59% rename from frame/examples/offchain-worker/src/tests.rs rename to frame/examples/offchain-worker-ping-pong/src/tests.rs index 203a59a8af03c..99e98cc1a0660 100644 --- a/frame/examples/offchain-worker/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -19,15 +19,18 @@ use crate as example_offchain_worker; use crate::*; use codec::Decode; use frame_support::{ - assert_ok, parameter_types, + assert_ok, derive_impl, traits::{ConstU32, ConstU64}, }; +use pallet::config_preludes::*; use sp_core::{ offchain::{testing, OffchainWorkerExt, TransactionPoolExt}, sr25519::Signature, H256, }; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_api_hidden_includes_construct_runtime::hidden_include::traits::Hooks; use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; use sp_runtime::{ testing::TestXt, @@ -42,7 +45,7 @@ frame_support::construct_runtime!( pub enum Test { System: frame_system::{Pallet, Call, Config, Storage, Event}, - Example: example_offchain_worker::{Pallet, Call, Storage, Event, ValidateUnsigned}, + PingPongOcwExample: example_offchain_worker::{Pallet, Call, Storage, Event, ValidateUnsigned}, } ); @@ -102,102 +105,56 @@ where } } -parameter_types! { - pub const UnsignedPriority: u64 = 1 << 20; -} - +#[derive_impl(TestDefaultConfig as pallet::DefaultConfig)] impl Config for Test { type RuntimeEvent = RuntimeEvent; type AuthorityId = crypto::TestAuthId; - type GracePeriod = ConstU64<5>; - type UnsignedInterval = ConstU64<128>; - type UnsignedPriority = UnsignedPriority; - type MaxPrices = ConstU32<64>; + type UnsignedInterval = ConstU64<16>; } -fn test_pub() -> sp_core::sr25519::Public { +fn user_pub() -> sp_core::sr25519::Public { sp_core::sr25519::Public::from_raw([1u8; 32]) } -#[test] -fn it_aggregates_the_price() { - sp_io::TestExternalities::default().execute_with(|| { - assert_eq!(Example::average_price(), None); - - assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 27)); - assert_eq!(Example::average_price(), Some(27)); - - assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 43)); - assert_eq!(Example::average_price(), Some(35)); - }); +pub fn run_to_block(n: u64) { + while System::block_number() < n { + if System::block_number() > 1 { + PingPongOcwExample::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + } + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + PingPongOcwExample::on_initialize(System::block_number()); + } } #[test] -fn should_make_http_call_and_parse_result() { - let (offchain, state) = testing::TestOffchainExt::new(); - let mut t = sp_io::TestExternalities::default(); - t.register_extension(OffchainWorkerExt::new(offchain)); +fn it_aggregates_pings() { + sp_io::TestExternalities::default().execute_with(|| { + System::set_block_number(1); - price_oracle_response(&mut state.write()); + assert_eq!(PingPongOcwExample::pings().len(), 0); - t.execute_with(|| { - // when - let price = Example::fetch_price().unwrap(); - // then - assert_eq!(price, 15523); - }); -} + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), 69)); + assert_eq!(PingPongOcwExample::pings().len(), 1); -#[test] -fn knows_how_to_mock_several_http_calls() { - let (offchain, state) = testing::TestOffchainExt::new(); - let mut t = sp_io::TestExternalities::default(); - t.register_extension(OffchainWorkerExt::new(offchain)); + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), 42)); + assert_eq!(PingPongOcwExample::pings().len(), 2); - { - let mut state = state.write(); - state.expect_request(testing::PendingRequest { - method: "GET".into(), - uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), - response: Some(br#"{"USD": 1}"#.to_vec()), - sent: true, - ..Default::default() - }); - - state.expect_request(testing::PendingRequest { - method: "GET".into(), - uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), - response: Some(br#"{"USD": 2}"#.to_vec()), - sent: true, - ..Default::default() - }); - - state.expect_request(testing::PendingRequest { - method: "GET".into(), - uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), - response: Some(br#"{"USD": 3}"#.to_vec()), - sent: true, - ..Default::default() - }); - } + // advance the block number so that the ping is no longer valid + run_to_block(System::block_number() + 1); - t.execute_with(|| { - let price1 = Example::fetch_price().unwrap(); - let price2 = Example::fetch_price().unwrap(); - let price3 = Example::fetch_price().unwrap(); - - assert_eq!(price1, 100); - assert_eq!(price2, 200); - assert_eq!(price3, 300); - }) + assert_eq!(PingPongOcwExample::pings().len(), 0); + }); } #[test] fn should_submit_signed_transaction_on_chain() { + const NONCE: u32 = 69; const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; - let (offchain, offchain_state) = testing::TestOffchainExt::new(); + let (offchain, _offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = MemoryKeystore::new(); keystore @@ -209,25 +166,29 @@ fn should_submit_signed_transaction_on_chain() { t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt::new(keystore)); - price_oracle_response(&mut offchain_state.write()); - t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); // when - Example::fetch_price_and_send_signed().unwrap(); + PingPongOcwExample::ocw_pong_signed().unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature.unwrap().0, 0); - assert_eq!(tx.call, RuntimeCall::Example(crate::Call::submit_price { price: 15523 })); + assert_eq!( + tx.call, + RuntimeCall::PingPongOcwExample(crate::Call::pong_signed { nonce: NONCE }) + ); }); } #[test] fn should_submit_unsigned_transaction_on_chain_for_any_account() { + const NONCE: u32 = 69; const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; - let (offchain, offchain_state) = testing::TestOffchainExt::new(); + let (offchain, _offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = MemoryKeystore::new(); @@ -243,34 +204,33 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt::new(keystore)); - price_oracle_response(&mut offchain_state.write()); - - let price_payload = PricePayload { + let pong_payload = PongPayload { block_number: 1, - price: 15523, + nonce: NONCE, public: ::Public::from(public_key), }; - // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); // when - Example::fetch_price_and_send_unsigned_for_any_account(1).unwrap(); + PingPongOcwExample::ocw_pong_unsigned_for_any_account(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature, None); - if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { - price_payload: body, + if let RuntimeCall::PingPongOcwExample(crate::Call::pong_unsigned_with_signed_payload { + pong_payload: body, signature, }) = tx.call { - assert_eq!(body, price_payload); + assert_eq!(body, pong_payload); let signature_valid = - ::Public, - frame_system::pallet_prelude::BlockNumberFor, - > as SignedPayload>::verify::(&price_payload, signature); + BlockNumberFor, + > as SignedPayload>::verify::(&pong_payload, signature); assert!(signature_valid); } @@ -279,9 +239,10 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { #[test] fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { + const NONCE: u32 = 69; const PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; - let (offchain, offchain_state) = testing::TestOffchainExt::new(); + let (offchain, _offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = MemoryKeystore::new(); @@ -297,34 +258,33 @@ fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt::new(keystore)); - price_oracle_response(&mut offchain_state.write()); - - let price_payload = PricePayload { + let pong_payload = PongPayload { block_number: 1, - price: 15523, + nonce: NONCE, public: ::Public::from(public_key), }; - // let signature = price_payload.sign::().unwrap(); t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); // when - Example::fetch_price_and_send_unsigned_for_all_accounts(1).unwrap(); + PingPongOcwExample::ocw_pong_unsigned_for_all_accounts(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); let tx = Extrinsic::decode(&mut &*tx).unwrap(); assert_eq!(tx.signature, None); - if let RuntimeCall::Example(crate::Call::submit_price_unsigned_with_signed_payload { - price_payload: body, + if let RuntimeCall::PingPongOcwExample(crate::Call::pong_unsigned_with_signed_payload { + pong_payload: body, signature, }) = tx.call { - assert_eq!(body, price_payload); + assert_eq!(body, pong_payload); let signature_valid = - ::Public, - frame_system::pallet_prelude::BlockNumberFor, - > as SignedPayload>::verify::(&price_payload, signature); + BlockNumberFor, + > as SignedPayload>::verify::(&pong_payload, signature); assert!(signature_valid); } @@ -333,7 +293,9 @@ fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { #[test] fn should_submit_raw_unsigned_transaction_on_chain() { - let (offchain, offchain_state) = testing::TestOffchainExt::new(); + const NONCE: u32 = 69; + + let (offchain, _offchain_state) = testing::TestOffchainExt::new(); let (pool, pool_state) = testing::TestTransactionPoolExt::new(); let keystore = MemoryKeystore::new(); @@ -343,11 +305,11 @@ fn should_submit_raw_unsigned_transaction_on_chain() { t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt::new(keystore)); - price_oracle_response(&mut offchain_state.write()); - t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); // when - Example::fetch_price_and_send_raw_unsigned(1).unwrap(); + PingPongOcwExample::ocw_pong_raw_unsigned(1).unwrap(); // then let tx = pool_state.write().transactions.pop().unwrap(); assert!(pool_state.read().transactions.is_empty()); @@ -355,36 +317,10 @@ fn should_submit_raw_unsigned_transaction_on_chain() { assert_eq!(tx.signature, None); assert_eq!( tx.call, - RuntimeCall::Example(crate::Call::submit_price_unsigned { + RuntimeCall::PingPongOcwExample(crate::Call::pong_unsigned { block_number: 1, - price: 15523 + nonce: NONCE }) ); }); } - -fn price_oracle_response(state: &mut testing::OffchainState) { - state.expect_request(testing::PendingRequest { - method: "GET".into(), - uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), - response: Some(br#"{"USD": 155.23}"#.to_vec()), - sent: true, - ..Default::default() - }); -} - -#[test] -fn parse_price_works() { - let test_data = vec![ - ("{\"USD\":6536.92}", Some(653692)), - ("{\"USD\":65.92}", Some(6592)), - ("{\"USD\":6536.924565}", Some(653692)), - ("{\"USD\":6536}", Some(653600)), - ("{\"USD2\":6536}", None), - ("{\"USD\":\"6432\"}", None), - ]; - - for (json, expected) in test_data { - assert_eq!(expected, Example::parse_price(json)); - } -} diff --git a/frame/examples/offchain-worker/Cargo.toml b/frame/examples/offchain-worker-price-oracle/Cargo.toml similarity index 90% rename from frame/examples/offchain-worker/Cargo.toml rename to frame/examples/offchain-worker-price-oracle/Cargo.toml index c0cd16bf19a6e..6060dce2630ea 100644 --- a/frame/examples/offchain-worker/Cargo.toml +++ b/frame/examples/offchain-worker-price-oracle/Cargo.toml @@ -1,13 +1,14 @@ [package] -name = "pallet-example-offchain-worker" -version = "4.0.0-dev" +name = "pallet-example-offchain-worker-price-oracle" +version = "1.0.0" authors = ["Parity Technologies "] edition = "2021" license = "MIT-0" homepage = "https://substrate.io" repository = "https://github.com/paritytech/substrate/" -description = "FRAME example pallet for offchain worker" +description = "FRAME example pallet for offchain worker (price oracle)" readme = "README.md" +publish = false [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md new file mode 100644 index 0000000000000..13872548142bf --- /dev/null +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -0,0 +1,40 @@ +# Price Oracle Offchain Worker Example Pallet + +A simple pallet demonstrating concepts, APIs and structures common to most offchain workers. + +Run `cargo doc --package pallet-example-offchain-worker-price-oracle --open` to view this module's +documentation. + +**This pallet serves as an example showcasing Substrate off-chain worker and is not meant to be +used in production.** + +## Overview + +In this example we are going to build a very simplistic, naive and definitely NOT +production-ready oracle for BTC/USD price. The main goal is to showcase how to use +offchain workers to fetch data from external sources via HTTP and feed it back on-chain. + +The OCW will be triggered after every block, fetch the current price +and prepare either signed or unsigned transaction to feed the result back on chain. +The on-chain logic will simply aggregate the results and store the last `64` values to compute +the average price. + +Only authorized keys are allowed to submit the price. The authorization key should be rotated. + +Here's an example of how a node admin can inject some keys into the keystore: + +```bash +$ curl --location --request POST 'http://localhost:9944' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "jsonrpc": "2.0", + "method": "author_insertKey", + "params": ["btc!","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], + "id": 1 +}' +``` + +Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) has funds and is added to `Authorities` in the runtime by adding it via `add_authority` extrinsic (from `root`). + +More complex management models and session +based key rotations should be considered, but that's outside the scope of this example. diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs new file mode 100644 index 0000000000000..90425237ebd9c --- /dev/null +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -0,0 +1,500 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +//! # Price Oracle Offchain Worker Example Pallet +//! +//! The Price Oracle Offchain Worker Example: A simple pallet demonstrating concepts, APIs and +//! structures common to most offchain workers. +//! +//! Run `cargo doc --package pallet-example-offchain-worker-price-oracle --open` to view this +//! module's documentation. +//! +//! **This pallet serves as an example showcasing Substrate off-chain worker and is not meant to be +//! used in production.** +//! +//! ## Overview +//! +//! In this example we are going to build a very simplistic, naive and definitely NOT +//! production-ready oracle for BTC/USD price. The main goal is to showcase how to use off-chain +//! workers to fetch data from external sources via HTTP and feed it back on-chain. +//! +//! The OCW will be triggered after every block, fetch the current price and prepare either signed +//! or unsigned transaction to feed the result back on chain. The on-chain logic will simply +//! aggregate the results and store last `64` values to compute the average price. +//! +//! Only authorized keys are allowed to submit the price. The authorization key should be rotated. +//! +//! Here's an example of how a node admin can inject some keys into the keystore: +//! +//! ```bash +//! $ curl --location --request POST 'http://localhost:9944' \ +//! --header 'Content-Type: application/json' \ +//! --data-raw '{ +//! "jsonrpc": "2.0", +//! "method": "author_insertKey", +//! "params": [ +//! "btc!", +//! "bread tongue spell stadium clean grief coin rent spend total practice document", +//! "0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30" +//! ], +//! "id": 1 +//! }' +//! ``` +//! +//! Then make sure that the corresponding address +//! (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) has funds and is added to `Authorities` in +//! the runtime by adding it via `add_authority` extrinsic (from `root`). +//! +//! More complex management models and session based key rotations should be considered, but that’s +//! outside the scope of this example. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::Get; +use frame_system::{ + self as system, + offchain::{AppCrypto, CreateSignedTransaction, SendSignedTransaction, Signer}, +}; +use lite_json::json::JsonValue; +use sp_core::crypto::KeyTypeId; +use sp_runtime::offchain::{ + http, + storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, + Duration, +}; +use sp_std::vec::Vec; + +#[cfg(test)] +mod tests; + +/// Defines application identifier for crypto keys of this module. +/// +/// Every module that deals with signatures needs to declare its unique identifier for its crypto +/// keys. +/// +/// When offchain worker is signing transactions it's going to request keys of type `KeyTypeId` from +/// the keystore and use the ones it finds to sign the transaction. The keys can be inserted +/// manually via RPC (see `author_insertKey`). +pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"btc!"); + +/// Based on the above `KeyTypeId` we need to generate a pallet-specific crypto type wrappers. We +/// can use from supported crypto kinds (`sr25519`, `ed25519` and `ecdsa`) and augment the types +/// with this pallet-specific identifier. +pub mod crypto { + use super::KEY_TYPE; + use sp_core::sr25519::Signature as Sr25519Signature; + use sp_runtime::{ + app_crypto::{app_crypto, sr25519}, + traits::Verify, + MultiSignature, MultiSigner, + }; + app_crypto!(sr25519, KEY_TYPE); + + pub struct TestAuthId; + + impl frame_system::offchain::AppCrypto for TestAuthId { + type RuntimeAppPublic = Public; + type GenericSignature = sp_core::sr25519::Signature; + type GenericPublic = sp_core::sr25519::Public; + } + + // implemented for mock runtime in test + impl frame_system::offchain::AppCrypto<::Signer, Sr25519Signature> + for TestAuthId + { + type RuntimeAppPublic = Public; + type GenericSignature = sp_core::sr25519::Signature; + type GenericPublic = sp_core::sr25519::Public; + } +} + +pub use pallet::*; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// This pallet's configuration trait + #[pallet::config(with_default)] + pub trait Config: CreateSignedTransaction> + frame_system::Config { + /// The identifier type for an offchain worker. + #[pallet::no_default] + type AuthorityId: AppCrypto; + + /// The overarching event type. + #[pallet::no_default] + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// A grace period after we send transaction. + /// + /// To avoid sending too many transactions, we only attempt to send one every `GRACE_PERIOD` + /// blocks. We use Local Storage to coordinate sending between distinct runs of this + /// offchain worker. + #[pallet::no_default] + #[pallet::constant] + type GracePeriod: Get>; + + /// Maximum number of prices. + #[pallet::constant] + type MaxPrices: Get; + + /// Maximum number of authorities. + #[pallet::constant] + type MaxAuthorities: Get; + } + + /// Events for the pallet. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Event generated when new price is accepted to contribute to the average. + NewPrice { price: u32, maybe_who: Option }, + /// Event generated when a new authority is added. + AuthorityAdded { authority: T::AccountId }, + /// Event generated when an authority is removed. + AuthorityRemoved { authority: T::AccountId }, + } + + /// A vector of recently submitted prices. + /// + /// This is used to calculate average price, should have bounded size. + #[pallet::storage] + #[pallet::getter(fn prices)] + pub(super) type Prices = StorageValue<_, BoundedVec, ValueQuery>; + + /// Authorities allowed to submit the price. + #[pallet::storage] + #[pallet::getter(fn authorities)] + pub(super) type Authorities = + StorageValue<_, BoundedVec, ValueQuery>; + + #[pallet::error] + pub enum Error { + NotAuthority, + AlreadyAuthority, + TooManyAuthorities, + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Offchain Worker entry point. + /// + /// By implementing `fn offchain_worker` you declare a new offchain worker. This function + /// will be called when the node is fully synced and a new best block is successfully + /// imported. + /// + /// Note that it's not guaranteed for offchain workers to run on EVERY block, there might be + /// cases where some blocks are skipped, or for some the worker runs twice (re-orgs), so the + /// code should be able to handle that. + /// + /// You can use `Local Storage` API to coordinate runs of the worker. + fn offchain_worker(block_number: BlockNumberFor) { + // Note that having logs compiled to WASM may cause the size of the blob to increase + // significantly. You can use `RuntimeDebug` custom derive to hide details of the types + // in WASM. The `sp-api` crate also provides a feature `disable-logging` to disable all + // logging and thus, remove any logging from the WASM. + log::info!("Hello World from offchain workers!"); + + // Since off-chain workers are just part of the runtime code, they have direct access to + // the storage and other included pallets. + // + // We can easily import `frame_system` and retrieve a block hash of the parent block. + let parent_hash = >::block_hash(block_number - 1u32.into()); + log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash); + + // It's a good practice to keep `fn offchain_worker()` function minimal, and move most + // of the code to separate `impl` block. Here we call a helper function to calculate + // current average price. This function reads storage entries of the current state. + let average: Option = Self::average_price(); + log::debug!("Current price: {:?}", average); + + // Start off by creating a reference to Local Storage value. Since the local storage is + // common for all offchain workers, it's a good practice to prepend your entry with the + // module name. + let val = StorageValueRef::persistent(b"example_ocw::last_send"); + // The Local Storage is persisted and shared between runs of the offchain workers, and + // offchain workers may run concurrently. We can use the `mutate` function, to write a + // storage entry in an atomic fashion. Under the hood it uses `compare_and_set` + // low-level method of local storage API, which means that only one worker will be able + // to "acquire a lock" and send a transaction if multiple workers happen to be executed + // concurrently. + let res = val.mutate( + |last_send: Result>, StorageRetrievalError>| { + match last_send { + // If we already have a value in storage and the block number is recent + // enough we avoid sending another transaction at this time. + Ok(Some(block)) if block_number < block + T::GracePeriod::get() => Err(()), + // In every other case we attempt to acquire the lock and send a + // transaction. + _ => Ok(block_number), + } + }, + ); + + // The result of `mutate` call will give us a nested `Result` type. The first one + // matches the return of the closure passed to `mutate`, i.e. if we return `Err` from + // the closure, we get an `Err` here. In case we return `Ok`, here we will have another + // (inner) `Result` that indicates if the value has been set to the storage correctly - + // i.e. if it wasn't written to in the meantime. + match res { + // The value has been set correctly, which means we can safely send a transaction + // now. + Ok(_) => { + if let Err(e) = Self::fetch_price_and_send_signed() { + log::error!("Error: {}", e); + } + }, + // We are in the grace period, we should not send a transaction this time. + Err(MutateStorageError::ValueFunctionFailed(())) => { + log::info!("Sent transaction too recently, waiting for grace period.") + }, + // We wanted to send a transaction, but failed to write the block number (acquire a + // lock). This indicates that another offchain worker that was running concurrently + // most likely executed the same logic and succeeded at writing to storage. Thus we + // don't really want to send the transaction, knowing that the other run already + // did. + Err(MutateStorageError::ConcurrentModification(_)) => { + log::error!("OCW failed to acquire a lock.") + }, + } + } + } + + /// A public part of the pallet. + #[pallet::call] + impl Pallet { + /// Submit new price to the list. + /// + /// This method is a public function of the module and can be called from within a + /// transaction. It appends given `price` to current list of prices. In our example the + /// `offchain worker` will create, sign & submit a transaction that calls this function + /// passing the price. + /// + /// This only works if the caller is in `Authorities`. + #[pallet::call_index(0)] + pub fn submit_price(origin: OriginFor, price: u32) -> DispatchResultWithPostInfo { + // Retrieve sender of the transaction. + let who = ensure_signed(origin)?; + + match Self::is_authority(&who) { + true => Self::add_price(Some(who), price), + false => return Err(Error::::NotAuthority.into()), + } + + // Authorized OCWs don't need to pay fees + Ok(Pays::No.into()) + } + + #[pallet::call_index(1)] + pub fn add_authority( + origin: OriginFor, + authority: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + ensure!(!Self::is_authority(&authority), Error::::AlreadyAuthority); + + let mut authorities = >::get(); + match authorities.try_push(authority.clone()) { + Ok(()) => (), + Err(_) => return Err(Error::::TooManyAuthorities.into()), + }; + + Authorities::::set(authorities); + + Self::deposit_event(Event::AuthorityAdded { authority }); + + Ok(().into()) + } + + #[pallet::call_index(2)] + pub fn remove_authority( + origin: OriginFor, + authority: T::AccountId, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + ensure!(Self::is_authority(&authority), Error::::NotAuthority); + + let mut authorities = >::get(); + match authorities.iter().position(|a| a == &authority) { + Some(index) => authorities.swap_remove(index), + None => return Err(Error::::NotAuthority.into()), + }; + + Authorities::::set(authorities); + + Self::deposit_event(Event::AuthorityAdded { authority }); + + Ok(().into()) + } + } + + /// Container for different types that implement [`DefaultConfig`]` of this pallet. + pub mod config_preludes { + // This will help use not need to disambiguate anything when using `derive_impl`. + use super::*; + + /// A type providing default configurations for this pallet in testing environment. + pub struct TestDefaultConfig; + + #[frame_support::register_default_impl(TestDefaultConfig)] + impl DefaultConfig for TestDefaultConfig { + type MaxPrices = frame_support::traits::ConstU32<64>; + type MaxAuthorities = frame_support::traits::ConstU32<64>; + } + } +} + +impl Pallet { + fn is_authority(who: &T::AccountId) -> bool { + >::get().contains(who) + } + + /// A helper function to fetch the price and send signed transaction. + fn fetch_price_and_send_signed() -> Result<(), &'static str> { + let signer = Signer::::all_accounts(); + if !signer.can_sign() { + return Err( + "No local accounts available. Consider adding one via `author_insertKey` RPC.", + ); + } + // Make an external HTTP request to fetch the current price. Note this call will block until + // response is received. + let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; + + // Using `send_signed_transaction` associated type we create and submit a transaction + // representing the call, we've just created. Submit signed will return a vector of results + // for all accounts that were found in the local keystore with expected `KEY_TYPE`. + let results = signer.send_signed_transaction(|_account| { + // Received price is wrapped into a call to `submit_price` public function of this + // pallet. This means that the transaction, when executed, will simply call that + // function passing `price` as an argument. + Call::submit_price { price } + }); + + for (acc, res) in &results { + match res { + Ok(()) => log::info!("[{:?}] Submitted price of {} cents", acc.id, price), + Err(e) => log::error!("[{:?}] Failed to submit transaction: {:?}", acc.id, e), + } + } + + Ok(()) + } + + /// Fetch current price and return the result in cents. + fn fetch_price() -> Result { + // We want to keep the offchain worker execution time reasonable, so we set a hard-coded + // deadline to 2s to complete the external call. You can also wait indefinitely for the + // response, however you may still get a timeout coming from the host machine. + let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(2_000)); + // Initiate an external HTTP GET request. This is using high-level wrappers from + // `sp_runtime`, for the low-level calls that you can find in `sp_io`. The API is trying to + // be similar to `request`, but since we are running in a custom WASM execution environment + // we can't simply import the library here. + let request = + http::Request::get("https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD"); + // We set the deadline for sending of the request, note that awaiting response can have a + // separate deadline. Next we send the request, before that it's also possible to alter + // request headers or stream body content in case of non-GET requests. + let pending = request.deadline(deadline).send().map_err(|_| http::Error::IoError)?; + + // The request is already being processed by the host, we are free to do anything else in + // the worker (we can send multiple concurrent requests too). At some point however we + // probably want to check the response though, so we can block current thread and wait for + // it to finish. Note that since the request is being driven by the host, we don't have to + // wait for the request to have it complete, we will just not read the response. + let response = pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??; + // Let's check the status code before we proceed to reading the response. + if response.code != 200 { + log::warn!("Unexpected status code: {}", response.code); + return Err(http::Error::Unknown); + } + + // Next we want to fully read the response body and collect it to a vector of bytes. Note + // that the return object allows you to read the body in chunks as well with a way to + // control the deadline. + let body = response.body().collect::>(); + + // Create a str slice from the body. + let body_str = sp_std::str::from_utf8(&body).map_err(|_| { + log::warn!("No UTF8 body"); + http::Error::Unknown + })?; + + let price = match Self::parse_price(body_str) { + Some(price) => Ok(price), + None => { + log::warn!("Unable to extract price from the response: {:?}", body_str); + Err(http::Error::Unknown) + }, + }?; + + log::warn!("Got price: {} cents", price); + + Ok(price) + } + + /// Parse the price from the given JSON string using `lite-json`. + /// + /// Returns `None` when parsing failed or `Some(price in cents)` when parsing is successful. + fn parse_price(price_str: &str) -> Option { + let val = lite_json::parse_json(price_str); + let price = match val.ok()? { + JsonValue::Object(obj) => { + let (_, v) = obj.into_iter().find(|(k, _)| k.iter().copied().eq("USD".chars()))?; + match v { + JsonValue::Number(number) => number, + _ => return None, + } + }, + _ => return None, + }; + + let exp = price.fraction_length.saturating_sub(2); + Some(price.integer as u32 * 100 + (price.fraction / 10_u64.pow(exp)) as u32) + } + + /// Add new price to the list. + fn add_price(maybe_who: Option, price: u32) { + log::info!("Adding to the average: {}", price); + >::mutate(|prices| { + if prices.try_push(price).is_err() { + prices[(price % T::MaxPrices::get()) as usize] = price; + } + }); + + let average = Self::average_price() + .expect("The average is not empty, because it was just mutated; qed"); + log::info!("Current average price is: {}", average); + // here we are raising the NewPrice event + Self::deposit_event(Event::NewPrice { price, maybe_who }); + } + + /// Calculate current average price. + fn average_price() -> Option { + let prices = >::get(); + if prices.is_empty() { + None + } else { + Some(prices.iter().fold(0_u32, |a, b| a.saturating_add(*b)) / prices.len() as u32) + } + } +} diff --git a/frame/examples/offchain-worker-price-oracle/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs new file mode 100644 index 0000000000000..65c047bd2c176 --- /dev/null +++ b/frame/examples/offchain-worker-price-oracle/src/tests.rs @@ -0,0 +1,261 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate as example_offchain_worker; +use crate::*; +use codec::Decode; +use frame_support::{assert_noop, assert_ok, derive_impl, parameter_types, traits::ConstU64}; +use pallet::config_preludes::*; +use sp_core::{ + offchain::{testing, OffchainWorkerExt, TransactionPoolExt}, + sr25519::Signature, + H256, +}; + +use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; +use sp_runtime::{ + testing::TestXt, + traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, + RuntimeAppPublic, +}; + +type Block = frame_system::mocking::MockBlock; + +// For testing the module, we construct a mock runtime. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + PriceOracleOcwExample: example_offchain_worker::{Pallet, Call, Storage, Event}, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type Hash = H256; + type RuntimeCall = RuntimeCall; + type Hashing = BlakeTwo256; + type AccountId = sp_core::sr25519::Public; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +type Extrinsic = TestXt; +type AccountId = <::Signer as IdentifyAccount>::AccountId; + +impl frame_system::offchain::SigningTypes for Test { + type Public = ::Signer; + type Signature = Signature; +} + +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type OverarchingCall = RuntimeCall; + type Extrinsic = Extrinsic; +} + +impl frame_system::offchain::CreateSignedTransaction for Test +where + RuntimeCall: From, +{ + fn create_transaction>( + call: RuntimeCall, + _public: ::Signer, + _account: AccountId, + nonce: u64, + ) -> Option<(RuntimeCall, ::SignaturePayload)> { + Some((call, (nonce, ()))) + } +} + +parameter_types! { + pub const UnsignedPriority: u64 = 1 << 20; +} + +#[derive_impl(TestDefaultConfig as pallet::DefaultConfig)] +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type AuthorityId = crypto::TestAuthId; + type GracePeriod = ConstU64<5>; +} + +fn test_pub() -> sp_core::sr25519::Public { + sp_core::sr25519::Public::from_raw([1u8; 32]) +} + +#[test] +fn it_aggregates_the_price() { + sp_io::TestExternalities::default().execute_with(|| { + assert_eq!(PriceOracleOcwExample::average_price(), None); + + assert_noop!( + PriceOracleOcwExample::submit_price(RuntimeOrigin::signed(test_pub()), 27), + Error::::NotAuthority + ); + + assert_ok!(PriceOracleOcwExample::add_authority(RuntimeOrigin::root(), test_pub())); + assert_ok!(PriceOracleOcwExample::submit_price(RuntimeOrigin::signed(test_pub()), 27)); + assert_eq!(PriceOracleOcwExample::average_price(), Some(27)); + + assert_ok!(PriceOracleOcwExample::submit_price(RuntimeOrigin::signed(test_pub()), 43)); + assert_eq!(PriceOracleOcwExample::average_price(), Some(35)); + + assert_ok!(PriceOracleOcwExample::remove_authority(RuntimeOrigin::root(), test_pub())); + assert_noop!( + PriceOracleOcwExample::submit_price(RuntimeOrigin::signed(test_pub()), 27), + Error::::NotAuthority + ); + }); +} + +#[test] +fn should_make_http_call_and_parse_result() { + let (offchain, state) = testing::TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + price_oracle_response(&mut state.write()); + + t.execute_with(|| { + // when + let price = PriceOracleOcwExample::fetch_price().unwrap(); + // then + assert_eq!(price, 15523); + }); +} + +#[test] +fn knows_how_to_mock_several_http_calls() { + let (offchain, state) = testing::TestOffchainExt::new(); + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + + { + let mut state = state.write(); + state.expect_request(testing::PendingRequest { + method: "GET".into(), + uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), + response: Some(br#"{"USD": 1}"#.to_vec()), + sent: true, + ..Default::default() + }); + + state.expect_request(testing::PendingRequest { + method: "GET".into(), + uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), + response: Some(br#"{"USD": 2}"#.to_vec()), + sent: true, + ..Default::default() + }); + + state.expect_request(testing::PendingRequest { + method: "GET".into(), + uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), + response: Some(br#"{"USD": 3}"#.to_vec()), + sent: true, + ..Default::default() + }); + } + + t.execute_with(|| { + let price1 = PriceOracleOcwExample::fetch_price().unwrap(); + let price2 = PriceOracleOcwExample::fetch_price().unwrap(); + let price3 = PriceOracleOcwExample::fetch_price().unwrap(); + + assert_eq!(price1, 100); + assert_eq!(price2, 200); + assert_eq!(price3, 300); + }) +} + +#[test] +fn should_submit_signed_transaction_on_chain() { + const PHRASE: &str = + "news slush supreme milk chapter athlete soap sausage put clutch what kitten"; + + let (offchain, offchain_state) = testing::TestOffchainExt::new(); + let (pool, pool_state) = testing::TestTransactionPoolExt::new(); + let keystore = MemoryKeystore::new(); + keystore + .sr25519_generate_new(crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE))) + .unwrap(); + + let mut t = sp_io::TestExternalities::default(); + t.register_extension(OffchainWorkerExt::new(offchain)); + t.register_extension(TransactionPoolExt::new(pool)); + t.register_extension(KeystoreExt::new(keystore)); + + price_oracle_response(&mut offchain_state.write()); + + t.execute_with(|| { + // when + PriceOracleOcwExample::fetch_price_and_send_signed().unwrap(); + // then + let tx = pool_state.write().transactions.pop().unwrap(); + assert!(pool_state.read().transactions.is_empty()); + let tx = Extrinsic::decode(&mut &*tx).unwrap(); + assert_eq!(tx.signature.unwrap().0, 0); + assert_eq!( + tx.call, + RuntimeCall::PriceOracleOcwExample(crate::Call::submit_price { price: 15523 }) + ); + }); +} + +fn price_oracle_response(state: &mut testing::OffchainState) { + state.expect_request(testing::PendingRequest { + method: "GET".into(), + uri: "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD".into(), + response: Some(br#"{"USD": 155.23}"#.to_vec()), + sent: true, + ..Default::default() + }); +} + +#[test] +fn parse_price_works() { + let test_data = vec![ + ("{\"USD\":6536.92}", Some(653692)), + ("{\"USD\":65.92}", Some(6592)), + ("{\"USD\":6536.924565}", Some(653692)), + ("{\"USD\":6536}", Some(653600)), + ("{\"USD2\":6536}", None), + ("{\"USD\":\"6432\"}", None), + ]; + + for (json, expected) in test_data { + assert_eq!(expected, PriceOracleOcwExample::parse_price(json)); + } +} diff --git a/frame/examples/offchain-worker/README.md b/frame/examples/offchain-worker/README.md deleted file mode 100644 index 7b8905cda3074..0000000000000 --- a/frame/examples/offchain-worker/README.md +++ /dev/null @@ -1,29 +0,0 @@ - -# Offchain Worker Example Pallet - -The Offchain Worker Example: A simple pallet demonstrating -concepts, APIs and structures common to most offchain workers. - -Run `cargo doc --package pallet-example-offchain-worker --open` to view this module's -documentation. - -- [`pallet_example_offchain_worker::Trait`](./trait.Trait.html) -- [`Call`](./enum.Call.html) -- [`Module`](./struct.Module.html) - -**This pallet serves as an example showcasing Substrate off-chain worker and is not meant to be -used in production.** - -## Overview - -In this example we are going to build a very simplistic, naive and definitely NOT -production-ready oracle for BTC/USD price. -Offchain Worker (OCW) will be triggered after every block, fetch the current price -and prepare either signed or unsigned transaction to feed the result back on chain. -The on-chain logic will simply aggregate the results and store last `64` values to compute -the average price. -Additional logic in OCW is put in place to prevent spamming the network with both signed -and unsigned transactions, and custom `UnsignedValidator` makes sure that there is only -one unsigned transaction floating in the network. - -License: MIT-0 diff --git a/frame/examples/offchain-worker/src/lib.rs b/frame/examples/offchain-worker/src/lib.rs deleted file mode 100644 index 6c1fa6ea8ec42..0000000000000 --- a/frame/examples/offchain-worker/src/lib.rs +++ /dev/null @@ -1,726 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! -//! # Offchain Worker Example Pallet -//! -//! The Offchain Worker Example: A simple pallet demonstrating -//! concepts, APIs and structures common to most offchain workers. -//! -//! Run `cargo doc --package pallet-example-offchain-worker --open` to view this module's -//! documentation. -//! -//! - [`Config`] -//! - [`Call`] -//! - [`Pallet`] -//! -//! **This pallet serves as an example showcasing Substrate off-chain worker and is not meant to -//! be used in production.** -//! -//! ## Overview -//! -//! In this example we are going to build a very simplistic, naive and definitely NOT -//! production-ready oracle for BTC/USD price. -//! Offchain Worker (OCW) will be triggered after every block, fetch the current price -//! and prepare either signed or unsigned transaction to feed the result back on chain. -//! The on-chain logic will simply aggregate the results and store last `64` values to compute -//! the average price. -//! Additional logic in OCW is put in place to prevent spamming the network with both signed -//! and unsigned transactions, and custom `UnsignedValidator` makes sure that there is only -//! one unsigned transaction floating in the network. - -#![cfg_attr(not(feature = "std"), no_std)] - -use codec::{Decode, Encode}; -use frame_support::traits::Get; -use frame_system::{ - self as system, - offchain::{ - AppCrypto, CreateSignedTransaction, SendSignedTransaction, SendUnsignedTransaction, - SignedPayload, Signer, SigningTypes, SubmitTransaction, - }, - pallet_prelude::BlockNumberFor, -}; -use lite_json::json::JsonValue; -use sp_core::crypto::KeyTypeId; -use sp_runtime::{ - offchain::{ - http, - storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, - Duration, - }, - traits::Zero, - transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, - RuntimeDebug, -}; -use sp_std::vec::Vec; - -#[cfg(test)] -mod tests; - -/// Defines application identifier for crypto keys of this module. -/// -/// Every module that deals with signatures needs to declare its unique identifier for -/// its crypto keys. -/// When offchain worker is signing transactions it's going to request keys of type -/// `KeyTypeId` from the keystore and use the ones it finds to sign the transaction. -/// The keys can be inserted manually via RPC (see `author_insertKey`). -pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"btc!"); - -/// Based on the above `KeyTypeId` we need to generate a pallet-specific crypto type wrappers. -/// We can use from supported crypto kinds (`sr25519`, `ed25519` and `ecdsa`) and augment -/// the types with this pallet-specific identifier. -pub mod crypto { - use super::KEY_TYPE; - use sp_core::sr25519::Signature as Sr25519Signature; - use sp_runtime::{ - app_crypto::{app_crypto, sr25519}, - traits::Verify, - MultiSignature, MultiSigner, - }; - app_crypto!(sr25519, KEY_TYPE); - - pub struct TestAuthId; - - impl frame_system::offchain::AppCrypto for TestAuthId { - type RuntimeAppPublic = Public; - type GenericSignature = sp_core::sr25519::Signature; - type GenericPublic = sp_core::sr25519::Public; - } - - // implemented for mock runtime in test - impl frame_system::offchain::AppCrypto<::Signer, Sr25519Signature> - for TestAuthId - { - type RuntimeAppPublic = Public; - type GenericSignature = sp_core::sr25519::Signature; - type GenericPublic = sp_core::sr25519::Public; - } -} - -pub use pallet::*; - -#[frame_support::pallet] -pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - /// This pallet's configuration trait - #[pallet::config] - pub trait Config: CreateSignedTransaction> + frame_system::Config { - /// The identifier type for an offchain worker. - type AuthorityId: AppCrypto; - - /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - // Configuration parameters - - /// A grace period after we send transaction. - /// - /// To avoid sending too many transactions, we only attempt to send one - /// every `GRACE_PERIOD` blocks. We use Local Storage to coordinate - /// sending between distinct runs of this offchain worker. - #[pallet::constant] - type GracePeriod: Get>; - - /// Number of blocks of cooldown after unsigned transaction is included. - /// - /// This ensures that we only accept unsigned transactions once, every `UnsignedInterval` - /// blocks. - #[pallet::constant] - type UnsignedInterval: Get>; - - /// 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. - #[pallet::constant] - type UnsignedPriority: Get; - - /// Maximum number of prices. - #[pallet::constant] - type MaxPrices: Get; - } - - #[pallet::pallet] - pub struct Pallet(_); - - #[pallet::hooks] - impl Hooks> for Pallet { - /// Offchain Worker entry point. - /// - /// By implementing `fn offchain_worker` you declare a new offchain worker. - /// This function will be called when the node is fully synced and a new best block is - /// successfully imported. - /// Note that it's not guaranteed for offchain workers to run on EVERY block, there might - /// be cases where some blocks are skipped, or for some the worker runs twice (re-orgs), - /// so the code should be able to handle that. - /// You can use `Local Storage` API to coordinate runs of the worker. - fn offchain_worker(block_number: BlockNumberFor) { - // Note that having logs compiled to WASM may cause the size of the blob to increase - // significantly. You can use `RuntimeDebug` custom derive to hide details of the types - // in WASM. The `sp-api` crate also provides a feature `disable-logging` to disable - // all logging and thus, remove any logging from the WASM. - log::info!("Hello World from offchain workers!"); - - // Since off-chain workers are just part of the runtime code, they have direct access - // to the storage and other included pallets. - // - // We can easily import `frame_system` and retrieve a block hash of the parent block. - let parent_hash = >::block_hash(block_number - 1u32.into()); - log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash); - - // It's a good practice to keep `fn offchain_worker()` function minimal, and move most - // of the code to separate `impl` block. - // Here we call a helper function to calculate current average price. - // This function reads storage entries of the current state. - let average: Option = Self::average_price(); - log::debug!("Current price: {:?}", average); - - // For this example we are going to send both signed and unsigned transactions - // depending on the block number. - // Usually it's enough to choose one or the other. - let should_send = Self::choose_transaction_type(block_number); - let res = match should_send { - TransactionType::Signed => Self::fetch_price_and_send_signed(), - TransactionType::UnsignedForAny => - Self::fetch_price_and_send_unsigned_for_any_account(block_number), - TransactionType::UnsignedForAll => - Self::fetch_price_and_send_unsigned_for_all_accounts(block_number), - TransactionType::Raw => Self::fetch_price_and_send_raw_unsigned(block_number), - TransactionType::None => Ok(()), - }; - if let Err(e) = res { - log::error!("Error: {}", e); - } - } - } - - /// A public part of the pallet. - #[pallet::call] - impl Pallet { - /// Submit new price to the list. - /// - /// This method is a public function of the module and can be called from within - /// a transaction. It appends given `price` to current list of prices. - /// In our example the `offchain worker` will create, sign & submit a transaction that - /// calls this function passing the price. - /// - /// The transaction needs to be signed (see `ensure_signed`) check, so that the caller - /// pays a fee to execute it. - /// This makes sure that it's not easy (or rather cheap) to attack the chain by submitting - /// excessive transactions, but note that it doesn't ensure the price oracle is actually - /// working and receives (and provides) meaningful data. - /// This example is not focused on correctness of the oracle itself, but rather its - /// purpose is to showcase offchain worker capabilities. - #[pallet::call_index(0)] - #[pallet::weight({0})] - pub fn submit_price(origin: OriginFor, price: u32) -> DispatchResultWithPostInfo { - // Retrieve sender of the transaction. - let who = ensure_signed(origin)?; - // Add the price to the on-chain list. - Self::add_price(Some(who), price); - Ok(().into()) - } - - /// Submit new price to the list via unsigned transaction. - /// - /// Works exactly like the `submit_price` function, but since we allow sending the - /// transaction without a signature, and hence without paying any fees, - /// we need a way to make sure that only some transactions are accepted. - /// This function can be called only once every `T::UnsignedInterval` blocks. - /// Transactions that call that function are de-duplicated on the pool level - /// via `validate_unsigned` implementation and also are rendered invalid if - /// the function has already been called in current "session". - /// - /// It's important to specify `weight` for unsigned calls as well, because even though - /// they don't charge fees, we still don't want a single block to contain unlimited - /// number of such transactions. - /// - /// This example is not focused on correctness of the oracle itself, but rather its - /// purpose is to showcase offchain worker capabilities. - #[pallet::call_index(1)] - #[pallet::weight({0})] - pub fn submit_price_unsigned( - origin: OriginFor, - _block_number: BlockNumberFor, - price: u32, - ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction. - ensure_none(origin)?; - // Add the price to the on-chain list, but mark it as coming from an empty address. - Self::add_price(None, price); - // now increment the block number at which we expect next unsigned transaction. - let current_block = >::block_number(); - >::put(current_block + T::UnsignedInterval::get()); - Ok(().into()) - } - - #[pallet::call_index(2)] - #[pallet::weight({0})] - pub fn submit_price_unsigned_with_signed_payload( - origin: OriginFor, - price_payload: PricePayload>, - _signature: T::Signature, - ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction. - ensure_none(origin)?; - // Add the price to the on-chain list, but mark it as coming from an empty address. - Self::add_price(None, price_payload.price); - // now increment the block number at which we expect next unsigned transaction. - let current_block = >::block_number(); - >::put(current_block + T::UnsignedInterval::get()); - Ok(().into()) - } - } - - /// Events for the pallet. - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// Event generated when new price is accepted to contribute to the average. - NewPrice { price: u32, maybe_who: Option }, - } - - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; - - /// Validate unsigned call to this module. - /// - /// By default unsigned transactions are disallowed, but implementing the validator - /// here we make sure that some particular calls (the ones produced by offchain worker) - /// are being whitelisted and marked as valid. - fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - // Firstly let's check that we call the right function. - if let Call::submit_price_unsigned_with_signed_payload { - price_payload: ref payload, - ref signature, - } = call - { - let signature_valid = - SignedPayload::::verify::(payload, signature.clone()); - if !signature_valid { - return InvalidTransaction::BadProof.into() - } - Self::validate_transaction_parameters(&payload.block_number, &payload.price) - } else if let Call::submit_price_unsigned { block_number, price: new_price } = call { - Self::validate_transaction_parameters(block_number, new_price) - } else { - InvalidTransaction::Call.into() - } - } - } - - /// A vector of recently submitted prices. - /// - /// This is used to calculate average price, should have bounded size. - #[pallet::storage] - #[pallet::getter(fn prices)] - pub(super) type Prices = StorageValue<_, BoundedVec, ValueQuery>; - - /// Defines the block when next unsigned transaction will be accepted. - /// - /// To prevent spam of unsigned (and unpaid!) transactions on the network, - /// we only allow one transaction every `T::UnsignedInterval` blocks. - /// This storage entry defines when new transaction is going to be accepted. - #[pallet::storage] - #[pallet::getter(fn next_unsigned_at)] - pub(super) type NextUnsignedAt = StorageValue<_, BlockNumberFor, ValueQuery>; -} - -/// Payload used by this example crate to hold price -/// data required to submit a transaction. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, scale_info::TypeInfo)] -pub struct PricePayload { - block_number: BlockNumber, - price: u32, - public: Public, -} - -impl SignedPayload for PricePayload> { - fn public(&self) -> T::Public { - self.public.clone() - } -} - -enum TransactionType { - Signed, - UnsignedForAny, - UnsignedForAll, - Raw, - None, -} - -impl Pallet { - /// Chooses which transaction type to send. - /// - /// This function serves mostly to showcase `StorageValue` helper - /// and local storage usage. - /// - /// Returns a type of transaction that should be produced in current run. - fn choose_transaction_type(block_number: BlockNumberFor) -> TransactionType { - /// A friendlier name for the error that is going to be returned in case we are in the grace - /// period. - const RECENTLY_SENT: () = (); - - // Start off by creating a reference to Local Storage value. - // Since the local storage is common for all offchain workers, it's a good practice - // to prepend your entry with the module name. - let val = StorageValueRef::persistent(b"example_ocw::last_send"); - // The Local Storage is persisted and shared between runs of the offchain workers, - // and offchain workers may run concurrently. We can use the `mutate` function, to - // write a storage entry in an atomic fashion. Under the hood it uses `compare_and_set` - // low-level method of local storage API, which means that only one worker - // will be able to "acquire a lock" and send a transaction if multiple workers - // happen to be executed concurrently. - let res = - val.mutate(|last_send: Result>, StorageRetrievalError>| { - match last_send { - // If we already have a value in storage and the block number is recent enough - // we avoid sending another transaction at this time. - Ok(Some(block)) if block_number < block + T::GracePeriod::get() => - Err(RECENTLY_SENT), - // In every other case we attempt to acquire the lock and send a transaction. - _ => Ok(block_number), - } - }); - - // The result of `mutate` call will give us a nested `Result` type. - // The first one matches the return of the closure passed to `mutate`, i.e. - // if we return `Err` from the closure, we get an `Err` here. - // In case we return `Ok`, here we will have another (inner) `Result` that indicates - // if the value has been set to the storage correctly - i.e. if it wasn't - // written to in the meantime. - match res { - // The value has been set correctly, which means we can safely send a transaction now. - Ok(block_number) => { - // We will send different transactions based on a random number. - // Note that this logic doesn't really guarantee that the transactions will be sent - // in an alternating fashion (i.e. fairly distributed). Depending on the execution - // order and lock acquisition, we may end up for instance sending two `Signed` - // transactions in a row. If a strict order is desired, it's better to use - // the storage entry for that. (for instance store both block number and a flag - // indicating the type of next transaction to send). - let transaction_type = block_number % 4u32.into(); - if transaction_type == Zero::zero() { - TransactionType::Signed - } else if transaction_type == BlockNumberFor::::from(1u32) { - TransactionType::UnsignedForAny - } else if transaction_type == BlockNumberFor::::from(2u32) { - TransactionType::UnsignedForAll - } else { - TransactionType::Raw - } - }, - // We are in the grace period, we should not send a transaction this time. - Err(MutateStorageError::ValueFunctionFailed(RECENTLY_SENT)) => TransactionType::None, - // We wanted to send a transaction, but failed to write the block number (acquire a - // lock). This indicates that another offchain worker that was running concurrently - // most likely executed the same logic and succeeded at writing to storage. - // Thus we don't really want to send the transaction, knowing that the other run - // already did. - Err(MutateStorageError::ConcurrentModification(_)) => TransactionType::None, - } - } - - /// A helper function to fetch the price and send signed transaction. - fn fetch_price_and_send_signed() -> Result<(), &'static str> { - let signer = Signer::::all_accounts(); - if !signer.can_sign() { - return Err( - "No local accounts available. Consider adding one via `author_insertKey` RPC.", - ) - } - // Make an external HTTP request to fetch the current price. - // Note this call will block until response is received. - let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - - // Using `send_signed_transaction` associated type we create and submit a transaction - // representing the call, we've just created. - // Submit signed will return a vector of results for all accounts that were found in the - // local keystore with expected `KEY_TYPE`. - let results = signer.send_signed_transaction(|_account| { - // Received price is wrapped into a call to `submit_price` public function of this - // pallet. This means that the transaction, when executed, will simply call that - // function passing `price` as an argument. - Call::submit_price { price } - }); - - for (acc, res) in &results { - match res { - Ok(()) => log::info!("[{:?}] Submitted price of {} cents", acc.id, price), - Err(e) => log::error!("[{:?}] Failed to submit transaction: {:?}", acc.id, e), - } - } - - Ok(()) - } - - /// A helper function to fetch the price and send a raw unsigned transaction. - fn fetch_price_and_send_raw_unsigned( - block_number: BlockNumberFor, - ) -> Result<(), &'static str> { - // Make sure we don't fetch the price if unsigned transaction is going to be rejected - // anyway. - let next_unsigned_at = >::get(); - if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") - } - - // Make an external HTTP request to fetch the current price. - // Note this call will block until response is received. - let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - - // Received price is wrapped into a call to `submit_price_unsigned` public function of this - // pallet. This means that the transaction, when executed, will simply call that function - // passing `price` as an argument. - let call = Call::submit_price_unsigned { block_number, price }; - - // Now let's create a transaction out of this call and submit it to the pool. - // Here we showcase two ways to send an unsigned transaction / unsigned payload (raw) - // - // By default unsigned transactions are disallowed, so we need to whitelist this case - // by writing `UnsignedValidator`. Note that it's EXTREMELY important to carefuly - // implement unsigned validation logic, as any mistakes can lead to opening DoS or spam - // attack vectors. See validation logic docs for more details. - // - SubmitTransaction::>::submit_unsigned_transaction(call.into()) - .map_err(|()| "Unable to submit unsigned transaction.")?; - - Ok(()) - } - - /// A helper function to fetch the price, sign payload and send an unsigned transaction - fn fetch_price_and_send_unsigned_for_any_account( - block_number: BlockNumberFor, - ) -> Result<(), &'static str> { - // Make sure we don't fetch the price if unsigned transaction is going to be rejected - // anyway. - let next_unsigned_at = >::get(); - if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") - } - - // Make an external HTTP request to fetch the current price. - // Note this call will block until response is received. - let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - - // -- Sign using any account - let (_, result) = Signer::::any_account() - .send_unsigned_transaction( - |account| PricePayload { price, block_number, public: account.public.clone() }, - |payload, signature| Call::submit_price_unsigned_with_signed_payload { - price_payload: payload, - signature, - }, - ) - .ok_or("No local accounts accounts available.")?; - result.map_err(|()| "Unable to submit transaction")?; - - Ok(()) - } - - /// A helper function to fetch the price, sign payload and send an unsigned transaction - fn fetch_price_and_send_unsigned_for_all_accounts( - block_number: BlockNumberFor, - ) -> Result<(), &'static str> { - // Make sure we don't fetch the price if unsigned transaction is going to be rejected - // anyway. - let next_unsigned_at = >::get(); - if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") - } - - // Make an external HTTP request to fetch the current price. - // Note this call will block until response is received. - let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?; - - // -- Sign using all accounts - let transaction_results = Signer::::all_accounts() - .send_unsigned_transaction( - |account| PricePayload { price, block_number, public: account.public.clone() }, - |payload, signature| Call::submit_price_unsigned_with_signed_payload { - price_payload: payload, - signature, - }, - ); - for (_account_id, result) in transaction_results.into_iter() { - if result.is_err() { - return Err("Unable to submit transaction") - } - } - - Ok(()) - } - - /// Fetch current price and return the result in cents. - fn fetch_price() -> Result { - // We want to keep the offchain worker execution time reasonable, so we set a hard-coded - // deadline to 2s to complete the external call. - // You can also wait indefinitely for the response, however you may still get a timeout - // coming from the host machine. - let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(2_000)); - // Initiate an external HTTP GET request. - // This is using high-level wrappers from `sp_runtime`, for the low-level calls that - // you can find in `sp_io`. The API is trying to be similar to `request`, but - // since we are running in a custom WASM execution environment we can't simply - // import the library here. - let request = - http::Request::get("https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD"); - // We set the deadline for sending of the request, note that awaiting response can - // have a separate deadline. Next we send the request, before that it's also possible - // to alter request headers or stream body content in case of non-GET requests. - let pending = request.deadline(deadline).send().map_err(|_| http::Error::IoError)?; - - // The request is already being processed by the host, we are free to do anything - // else in the worker (we can send multiple concurrent requests too). - // At some point however we probably want to check the response though, - // so we can block current thread and wait for it to finish. - // Note that since the request is being driven by the host, we don't have to wait - // for the request to have it complete, we will just not read the response. - let response = pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??; - // Let's check the status code before we proceed to reading the response. - if response.code != 200 { - log::warn!("Unexpected status code: {}", response.code); - return Err(http::Error::Unknown) - } - - // Next we want to fully read the response body and collect it to a vector of bytes. - // Note that the return object allows you to read the body in chunks as well - // with a way to control the deadline. - let body = response.body().collect::>(); - - // Create a str slice from the body. - let body_str = sp_std::str::from_utf8(&body).map_err(|_| { - log::warn!("No UTF8 body"); - http::Error::Unknown - })?; - - let price = match Self::parse_price(body_str) { - Some(price) => Ok(price), - None => { - log::warn!("Unable to extract price from the response: {:?}", body_str); - Err(http::Error::Unknown) - }, - }?; - - log::warn!("Got price: {} cents", price); - - Ok(price) - } - - /// Parse the price from the given JSON string using `lite-json`. - /// - /// Returns `None` when parsing failed or `Some(price in cents)` when parsing is successful. - fn parse_price(price_str: &str) -> Option { - let val = lite_json::parse_json(price_str); - let price = match val.ok()? { - JsonValue::Object(obj) => { - let (_, v) = obj.into_iter().find(|(k, _)| k.iter().copied().eq("USD".chars()))?; - match v { - JsonValue::Number(number) => number, - _ => return None, - } - }, - _ => return None, - }; - - let exp = price.fraction_length.saturating_sub(2); - Some(price.integer as u32 * 100 + (price.fraction / 10_u64.pow(exp)) as u32) - } - - /// Add new price to the list. - fn add_price(maybe_who: Option, price: u32) { - log::info!("Adding to the average: {}", price); - >::mutate(|prices| { - if prices.try_push(price).is_err() { - prices[(price % T::MaxPrices::get()) as usize] = price; - } - }); - - let average = Self::average_price() - .expect("The average is not empty, because it was just mutated; qed"); - log::info!("Current average price is: {}", average); - // here we are raising the NewPrice event - Self::deposit_event(Event::NewPrice { price, maybe_who }); - } - - /// Calculate current average price. - fn average_price() -> Option { - let prices = >::get(); - if prices.is_empty() { - None - } else { - Some(prices.iter().fold(0_u32, |a, b| a.saturating_add(*b)) / prices.len() as u32) - } - } - - fn validate_transaction_parameters( - block_number: &BlockNumberFor, - new_price: &u32, - ) -> TransactionValidity { - // Now let's check if the transaction has any chance to succeed. - let next_unsigned_at = >::get(); - if &next_unsigned_at > block_number { - return InvalidTransaction::Stale.into() - } - // Let's make sure to reject transactions from the future. - let current_block = >::block_number(); - if ¤t_block < block_number { - return InvalidTransaction::Future.into() - } - - // We prioritize transactions that are more far away from current average. - // - // Note this doesn't make much sense when building an actual oracle, but this example - // is here mostly to show off offchain workers capabilities, not about building an - // oracle. - let avg_price = Self::average_price() - .map(|price| if &price > new_price { price - new_price } else { new_price - price }) - .unwrap_or(0); - - ValidTransaction::with_tag_prefix("ExampleOffchainWorker") - // We set base priority to 2**20 and hope it's included before any other - // transactions in the pool. Next we tweak the priority depending on how much - // it differs from the current average. (the more it differs the more priority it - // has). - .priority(T::UnsignedPriority::get().saturating_add(avg_price as _)) - // This transaction does not require anything else to go before into the pool. - // In theory we could require `previous_unsigned_at` transaction to go first, - // but it's not necessary in our case. - //.and_requires() - // We set the `provides` tag to be the same as `next_unsigned_at`. This makes - // sure only one transaction produced after `next_unsigned_at` will ever - // get to the transaction pool and will end up in the block. - // We can still have multiple transactions compete for the same "spot", - // and the one with higher priority will replace other one in the pool. - .and_provides(next_unsigned_at) - // The transaction is only valid for next 5 blocks. After that it's - // going to be revalidated by the pool. - .longevity(5) - // It's fine to propagate that transaction to other peers, which means it can be - // created even by nodes that don't produce blocks. - // Note that sometimes it's better to keep it for yourself (if you are the block - // producer), since for instance in some schemes others may copy your solution and - // claim a reward. - .propagate(true) - .build() - } -} diff --git a/scripts/ci/gitlab/pipeline/test.yml b/scripts/ci/gitlab/pipeline/test.yml index 69d53012f79e7..e6b197f997059 100644 --- a/scripts/ci/gitlab/pipeline/test.yml +++ b/scripts/ci/gitlab/pipeline/test.yml @@ -321,7 +321,9 @@ test-frame-examples-compile-to-wasm: RUST_BACKTRACE: 1 script: - rusty-cachier snapshot create - - cd ./frame/examples/offchain-worker/ + - cd ./frame/examples/offchain-worker-ping-pong/ + - cargo build --locked --target=wasm32-unknown-unknown --no-default-features + - cd ../offchain-worker-price-oracle/ - cargo build --locked --target=wasm32-unknown-unknown --no-default-features - cd ../basic - cargo build --locked --target=wasm32-unknown-unknown --no-default-features