From 3e8740333a5bfaea590bf332cf58d36e481d10fb Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Mon, 29 May 2023 20:28:50 -0300 Subject: [PATCH 01/72] bootstrap ocw-ping-pong example --- .../offchain-worker-ping-pong/Cargo.toml | 32 + .../offchain-worker-ping-pong/README.md | 41 + .../offchain-worker-ping-pong/src/lib.rs | 706 ++++++++++++++++++ 3 files changed, 779 insertions(+) create mode 100644 frame/examples/offchain-worker-ping-pong/Cargo.toml create mode 100644 frame/examples/offchain-worker-ping-pong/README.md create mode 100644 frame/examples/offchain-worker-ping-pong/src/lib.rs 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..523b6b4145359 --- /dev/null +++ b/frame/examples/offchain-worker-ping-pong/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "pallet-example-offchain-worker-ping-pong" +version = "4.0.0-dev" +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" + +[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 = "8.0.0", default-features = false, path = "../../../primitives/core" } +sp-io = { version = "8.0.0", default-features = false, path = "../../../primitives/io" } +sp-keystore = { version = "0.14.0", optional = true, path = "../../../primitives/keystore" } +sp-runtime = { version = "8.0.0", default-features = false, path = "../../../primitives/runtime" } +sp-std = { version = "6.0.0", default-features = false, path = "../../../primitives/std" } +cargo-watch = "8.4.0" + +[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..eca68a45f0cb7 --- /dev/null +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -0,0 +1,41 @@ + +# Ping-Pong 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 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 +- `PongAckUnauthenticated`: emitted when the call was made by an **unauthenticated** offchain worker + +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!** ⚠️ + +For `pong_signed`, more complex management models and session +based key rotations should be considered, but that's outside the scope of this example. + +The logic around block numbers only helps create different conditions to trigger the different +kinds of transactions. It is not strictly necessary when implementing offchain workers. \ 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..044447f077f8a --- /dev/null +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -0,0 +1,706 @@ +// 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. +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Pallet`] +//! +//! **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 +//! - `PongAckUnauthenticated`: emitted when the call was made by an **unauthenticated** offchain worker +//! +//! 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**. +//! +//! 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!** ⚠️ +//! +//! For `pong_signed`, more complex management models and session +//! based key rotations should be considered, but that's outside the scope of this example. +//! +//! The logic around block numbers only helps create different conditions to trigger the different +//! kinds of transactions. It is not strictly necessary when implementing offchain workers. + +#![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, + }, +}; +use sp_core::crypto::KeyTypeId; +use sp_runtime::{ + offchain::{ + storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, + }, + 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] +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 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, + } + + #[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: T::BlockNumber) { + // 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); + + // 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::ocw_pong_signed(), + TransactionType::UnsignedForAny => + Self::ocw_pong_unsigned_for_any_account(block_number), + TransactionType::UnsignedForAll => + Self::ocw_pong_unsigned_for_all_accounts(block_number), + TransactionType::Raw => Self::ocw_pong_raw_unsigned(block_number), + TransactionType::None => Ok(()), + }; + if let Err(e) = res { + log::error!("Error: {}", e); + } + } + + /// clean Pings + fn on_initialize(_: T::BlockNumber) -> Weight { + Pings::::kill(); + Weight::zero() + } + } + + /// A public part of the pallet. + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight({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)] + #[pallet::weight({0})] + pub fn pong_signed(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + if Self::is_authority(&who) { + Self::deposit_event(Event::PongAckAuthenticated { nonce }); + } + + Ok(().into()) + } + + #[pallet::call_index(2)] + #[pallet::weight({0})] + pub fn pong_unsigned( + origin: OriginFor, + _block_number: T::BlockNumber, + 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 }); + + // 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)] + #[pallet::weight({0})] + 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 }); + + // 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)] + #[pallet::weight({0})] + 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); + + Ok(().into()) + } + + #[pallet::call_index(5)] + #[pallet::weight({0})] + 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); + + Ok(().into()) + } + } + + /// 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_authenticated transaction is accepted. + PongAckAuthenticated { nonce: u32 }, + /// Event generated when new pong_unauthenticated transaction is accepted. + PongAckUnauthenticated { nonce: u32 }, + } + + #[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 about 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() + } + } + } + + /// 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<_, T::BlockNumber, ValueQuery>; +} + +/// Payload used by this example crate to hold pong response +/// data required to submit a 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() + } +} + +enum TransactionType { + Signed, + UnsignedForAny, + UnsignedForAll, + Raw, + None, +} + +impl Pallet { + fn is_authority(who: &T::AccountId) -> bool { + >::get().contains(who) + } + + /// 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: T::BlockNumber) -> 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 == T::BlockNumber::from(1u32) { + TransactionType::UnsignedForAny + } else if transaction_type == T::BlockNumber::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, + } + } + + fn validate_transaction_parameters( + block_number: &T::BlockNumber, + ) -> 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. 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(0)) + // 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), + } + } + } + + Ok(()) + } + + /// A helper function to sign payload and send an unsigned pong transaction + fn ocw_pong_unsigned_for_any_account( + block_number: T::BlockNumber, + ) -> Result<(), &'static str> { + // Make sure we don't read the pings 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") + } + + 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: T::BlockNumber, + ) -> Result<(), &'static str> { + // Make sure we don't read the pings 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") + } + + 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: T::BlockNumber) -> Result<(), &'static str> { + // Make sure we don't read the ping 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") + } + + let pings = >::get(); + for p in pings { + let Ping(nonce) = p; + // 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 `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(()) + } +} From 0220b59bb6284043b53b8920417247a438abd684 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 15:06:30 -0300 Subject: [PATCH 02/72] add tests for ocw-ping-pong-example --- .../offchain-worker-ping-pong/src/tests.rs | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 frame/examples/offchain-worker-ping-pong/src/tests.rs diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs new file mode 100644 index 0000000000000..250534169bca6 --- /dev/null +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -0,0 +1,333 @@ +// 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_ok, parameter_types, + traits::{ConstU32, ConstU64}, +}; +use sp_core::{ + offchain::{testing, OffchainWorkerExt, TransactionPoolExt}, + sr25519::Signature, + H256, +}; + +use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; +use sp_runtime::{ + testing::{Header, TestXt}, + traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, + RuntimeAppPublic, +}; +use sp_api_hidden_includes_construct_runtime::hidden_include::traits::Hooks; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// For testing the module, we construct a mock runtime. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + PingPongOcwExample: example_offchain_worker::{Pallet, Call, Storage, Event, ValidateUnsigned}, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = sp_core::sr25519::Public; + type Lookup = IdentityLookup; + type Header = Header; + 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 = 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; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type AuthorityId = crypto::TestAuthId; + type GracePeriod = ConstU64<5>; + type UnsignedInterval = ConstU64<128>; + type UnsignedPriority = UnsignedPriority; + type MaxPings = ConstU32<64>; + type MaxAuthorities = ConstU32<64>; +} + +fn user_pub() -> sp_core::sr25519::Public { + sp_core::sr25519::Public::from_raw([1u8; 32]) +} + +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 it_aggregates_pings() { + sp_io::TestExternalities::default().execute_with(|| { + System::set_block_number(1); + + assert_eq!(PingPongOcwExample::pings().len(), 0); + + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), 69)); + assert_eq!(PingPongOcwExample::pings().len(), 1); + + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), 42)); + assert_eq!(PingPongOcwExample::pings().len(), 2); + + // advance the block number so that the ping is no longer valid + run_to_block(System::block_number() + 1); + + 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 (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)); + + t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); + // when + 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::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 (pool, pool_state) = testing::TestTransactionPoolExt::new(); + + let keystore = MemoryKeystore::new(); + + keystore + .sr25519_generate_new(crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE))) + .unwrap(); + + let public_key = *keystore.sr25519_public_keys(crate::crypto::Public::ID).get(0).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)); + + let price_payload = PongPayload { + block_number: 1, + nonce: NONCE, + public: ::Public::from(public_key), + }; + + t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); + // when + 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::PingPongOcwExample(crate::Call::pong_unsigned_with_signed_payload { + pong_payload: body, + signature, + }) = tx.call + { + assert_eq!(body, price_payload); + + let signature_valid = + ::Public, + ::BlockNumber, + > as SignedPayload>::verify::(&price_payload, signature); + + assert!(signature_valid); + } + }); +} + +#[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 (pool, pool_state) = testing::TestTransactionPoolExt::new(); + + let keystore = MemoryKeystore::new(); + + keystore + .sr25519_generate_new(crate::crypto::Public::ID, Some(&format!("{}/hunter1", PHRASE))) + .unwrap(); + + let public_key = *keystore.sr25519_public_keys(crate::crypto::Public::ID).get(0).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)); + + let pong_payload = PongPayload { + block_number: 1, + nonce: NONCE, + public: ::Public::from(public_key), + }; + + t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); + // when + 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::PingPongOcwExample(crate::Call::pong_unsigned_with_signed_payload { + pong_payload: body, + signature, + }) = tx.call + { + assert_eq!(body, pong_payload); + + let signature_valid = + ::Public, + ::BlockNumber, + > as SignedPayload>::verify::(&pong_payload, signature); + + assert!(signature_valid); + } + }); +} + +#[test] +fn should_submit_raw_unsigned_transaction_on_chain() { + const NONCE: u32 = 69; + + let (offchain, _offchain_state) = testing::TestOffchainExt::new(); + let (pool, pool_state) = testing::TestTransactionPoolExt::new(); + + let keystore = MemoryKeystore::new(); + + 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)); + + t.execute_with(|| { + // user sends ping + assert_ok!(PingPongOcwExample::ping(RuntimeOrigin::signed(user_pub()), NONCE)); + // when + PingPongOcwExample::ocw_pong_raw_unsigned(1).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, None); + assert_eq!( + tx.call, + RuntimeCall::PingPongOcwExample(crate::Call::pong_unsigned { + block_number: 1, + nonce: NONCE + }) + ); + }); +} From 5427c7aba10210d8741e34940184d7001aa739d3 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 15:26:56 -0300 Subject: [PATCH 03/72] rename offchain-worker-price-oracle example --- .../{offchain-worker => offchain-worker-price-oracle}/Cargo.toml | 0 .../{offchain-worker => offchain-worker-price-oracle}/README.md | 0 .../{offchain-worker => offchain-worker-price-oracle}/src/lib.rs | 0 .../src/tests.rs | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename frame/examples/{offchain-worker => offchain-worker-price-oracle}/Cargo.toml (100%) rename frame/examples/{offchain-worker => offchain-worker-price-oracle}/README.md (100%) rename frame/examples/{offchain-worker => offchain-worker-price-oracle}/src/lib.rs (100%) rename frame/examples/{offchain-worker => offchain-worker-price-oracle}/src/tests.rs (100%) diff --git a/frame/examples/offchain-worker/Cargo.toml b/frame/examples/offchain-worker-price-oracle/Cargo.toml similarity index 100% rename from frame/examples/offchain-worker/Cargo.toml rename to frame/examples/offchain-worker-price-oracle/Cargo.toml diff --git a/frame/examples/offchain-worker/README.md b/frame/examples/offchain-worker-price-oracle/README.md similarity index 100% rename from frame/examples/offchain-worker/README.md rename to frame/examples/offchain-worker-price-oracle/README.md diff --git a/frame/examples/offchain-worker/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs similarity index 100% rename from frame/examples/offchain-worker/src/lib.rs rename to frame/examples/offchain-worker-price-oracle/src/lib.rs diff --git a/frame/examples/offchain-worker/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs similarity index 100% rename from frame/examples/offchain-worker/src/tests.rs rename to frame/examples/offchain-worker-price-oracle/src/tests.rs From f0a7e286d9f54f29f1b66538d6d8b2e353558e4d Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 15:27:13 -0300 Subject: [PATCH 04/72] improve docs for ping-pong ocw example --- .../offchain-worker-ping-pong/README.md | 19 ++++++++++++++++++- .../offchain-worker-ping-pong/src/lib.rs | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index eca68a45f0cb7..ccfc3aafd4e50 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -34,7 +34,24 @@ In other words: ⚠️ **DO NOT USE UNSIGNED TRANSACTIONS UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!** ⚠️ -For `pong_signed`, more complex management models and session +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:9933' \ +--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 +}' +``` + +Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write +the keys into disk, and will persist in case the node is restarted (`author_insertKey` will not). + +More complex management models and session based key rotations should be considered, but that's outside the scope of this example. The logic around block numbers only helps create different conditions to trigger the different diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 044447f077f8a..6914b87f198a8 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -61,8 +61,25 @@ //! 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:9933' \ +//! --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 +//! }' +//! ``` +//! +//! Another alternative for the node admin is to use the key insert subcommand on the node's executable, +//! which will write the keys into disk, and will persist in case the node is restarted (`author_insertKey` will not). //! -//! For `pong_signed`, more complex management models and session +//! More complex management models and session //! based key rotations should be considered, but that's outside the scope of this example. //! //! The logic around block numbers only helps create different conditions to trigger the different From 153ffc7ff4b280ebbcf47e51233f447b6b172936 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:09:18 -0300 Subject: [PATCH 05/72] refactor price oracle ocw example --- .../offchain-worker-price-oracle/Cargo.toml | 4 +- .../offchain-worker-price-oracle/README.md | 36 +- .../offchain-worker-price-oracle/src/lib.rs | 460 +++--------------- .../offchain-worker-price-oracle/src/tests.rs | 168 +------ 4 files changed, 118 insertions(+), 550 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/Cargo.toml b/frame/examples/offchain-worker-price-oracle/Cargo.toml index 2eecdde6a8ff4..43b7731227b12 100644 --- a/frame/examples/offchain-worker-price-oracle/Cargo.toml +++ b/frame/examples/offchain-worker-price-oracle/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "pallet-example-offchain-worker" +name = "pallet-example-offchain-worker-price-oracle" version = "4.0.0-dev" 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" [package.metadata.docs.rs] diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md index 7b8905cda3074..ed0713e3b4102 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -1,29 +1,41 @@ -# Offchain Worker Example Pallet +# Price Oracle Offchain Worker Example Pallet -The Offchain Worker Example: A simple pallet demonstrating +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 --open` to view this module's +Run `cargo doc --package pallet-example-offchain-worker-price-oracle --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 +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. -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. + +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:9933' \ +--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 +}' +``` + +Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write the keys into disk, and will persist in case the node is restarted (author_insertKey will not). License: MIT-0 diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 6ce8524174200..70dd0df879b06 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -16,32 +16,45 @@ // limitations under the License. //! -//! # Offchain Worker Example Pallet +//! +//! # Price Oracle Offchain Worker Example Pallet //! -//! The Offchain Worker Example: A simple pallet demonstrating +//! 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 --open` to view this module's +//! Run `cargo doc --package pallet-example-offchain-worker-price-oracle --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.** +//! **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 +//! 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. -//! 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. +//! +//! 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:9933' \ +//! --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 +//! }' +//! ``` +//! +//! Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write the keys into disk, and will persist in case the node is restarted (author_insertKey will not). #![cfg_attr(not(feature = "std"), no_std)] @@ -128,8 +141,6 @@ pub mod pallet { /// 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 @@ -138,20 +149,6 @@ pub mod pallet { #[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; @@ -192,22 +189,55 @@ pub mod pallet { 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 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) => { + 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(RECENTLY_SENT)) => 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."), } + + } } @@ -221,70 +251,17 @@ pub mod pallet { /// 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. + /// todo: docs authorized #[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: T::BlockNumber, - 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()) - } + // todo: check authorized - #[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()); + // Add the price to the on-chain list. + Self::add_price(Some(who), price); Ok(().into()) } } @@ -297,147 +274,15 @@ pub mod pallet { 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<_, T::BlockNumber, 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: T::BlockNumber) -> 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 == T::BlockNumber::from(1u32) { - TransactionType::UnsignedForAny - } else if transaction_type == T::BlockNumber::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(); @@ -471,101 +316,6 @@ impl Pallet { Ok(()) } - /// A helper function to fetch the price and send a raw unsigned transaction. - fn fetch_price_and_send_raw_unsigned(block_number: T::BlockNumber) -> 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: T::BlockNumber, - ) -> 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: T::BlockNumber, - ) -> 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 @@ -667,56 +417,4 @@ impl Pallet { Some(prices.iter().fold(0_u32, |a, b| a.saturating_add(*b)) / prices.len() as u32) } } - - fn validate_transaction_parameters( - block_number: &T::BlockNumber, - 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/frame/examples/offchain-worker-price-oracle/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs index 3df7f4a8d5439..e4a09894605ea 100644 --- a/frame/examples/offchain-worker-price-oracle/src/tests.rs +++ b/frame/examples/offchain-worker-price-oracle/src/tests.rs @@ -46,7 +46,7 @@ frame_support::construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsic, { System: frame_system::{Pallet, Call, Config, Storage, Event}, - Example: example_offchain_worker::{Pallet, Call, Storage, Event, ValidateUnsigned}, + PriceOracleOcwExample: example_offchain_worker::{Pallet, Call, Storage, Event}, } ); @@ -115,8 +115,6 @@ 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>; } @@ -127,13 +125,13 @@ fn test_pub() -> sp_core::sr25519::Public { #[test] fn it_aggregates_the_price() { sp_io::TestExternalities::default().execute_with(|| { - assert_eq!(Example::average_price(), None); + assert_eq!(PriceOracleOcwExample::average_price(), None); - assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 27)); - assert_eq!(Example::average_price(), Some(27)); + assert_ok!(PriceOracleOcwExample::submit_price(RuntimeOrigin::signed(test_pub()), 27)); + assert_eq!(PriceOracleOcwExample::average_price(), Some(27)); - assert_ok!(Example::submit_price(RuntimeOrigin::signed(test_pub()), 43)); - assert_eq!(Example::average_price(), Some(35)); + assert_ok!(PriceOracleOcwExample::submit_price(RuntimeOrigin::signed(test_pub()), 43)); + assert_eq!(PriceOracleOcwExample::average_price(), Some(35)); }); } @@ -147,7 +145,7 @@ fn should_make_http_call_and_parse_result() { t.execute_with(|| { // when - let price = Example::fetch_price().unwrap(); + let price = PriceOracleOcwExample::fetch_price().unwrap(); // then assert_eq!(price, 15523); }); @@ -187,9 +185,9 @@ fn knows_how_to_mock_several_http_calls() { } t.execute_with(|| { - let price1 = Example::fetch_price().unwrap(); - let price2 = Example::fetch_price().unwrap(); - let price3 = Example::fetch_price().unwrap(); + 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); @@ -218,153 +216,13 @@ fn should_submit_signed_transaction_on_chain() { t.execute_with(|| { // when - Example::fetch_price_and_send_signed().unwrap(); + 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::Example(crate::Call::submit_price { price: 15523 })); - }); -} - -#[test] -fn should_submit_unsigned_transaction_on_chain_for_any_account() { - 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 public_key = *keystore.sr25519_public_keys(crate::crypto::Public::ID).get(0).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()); - - let price_payload = PricePayload { - block_number: 1, - price: 15523, - public: ::Public::from(public_key), - }; - - // let signature = price_payload.sign::().unwrap(); - t.execute_with(|| { - // when - Example::fetch_price_and_send_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, - signature, - }) = tx.call - { - assert_eq!(body, price_payload); - - let signature_valid = - ::Public, - ::BlockNumber, - > as SignedPayload>::verify::(&price_payload, signature); - - assert!(signature_valid); - } - }); -} - -#[test] -fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { - 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 public_key = *keystore.sr25519_public_keys(crate::crypto::Public::ID).get(0).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()); - - let price_payload = PricePayload { - block_number: 1, - price: 15523, - public: ::Public::from(public_key), - }; - - // let signature = price_payload.sign::().unwrap(); - t.execute_with(|| { - // when - Example::fetch_price_and_send_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, - signature, - }) = tx.call - { - assert_eq!(body, price_payload); - - let signature_valid = - ::Public, - ::BlockNumber, - > as SignedPayload>::verify::(&price_payload, signature); - - assert!(signature_valid); - } - }); -} - -#[test] -fn should_submit_raw_unsigned_transaction_on_chain() { - let (offchain, offchain_state) = testing::TestOffchainExt::new(); - let (pool, pool_state) = testing::TestTransactionPoolExt::new(); - - let keystore = MemoryKeystore::new(); - - 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 - Example::fetch_price_and_send_raw_unsigned(1).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, None); - assert_eq!( - tx.call, - RuntimeCall::Example(crate::Call::submit_price_unsigned { - block_number: 1, - price: 15523 - }) - ); + assert_eq!(tx.call, RuntimeCall::PriceOracleOcwExample(crate::Call::submit_price { price: 15523 })); }); } @@ -390,6 +248,6 @@ fn parse_price_works() { ]; for (json, expected) in test_data { - assert_eq!(expected, Example::parse_price(json)); + assert_eq!(expected, PriceOracleOcwExample::parse_price(json)); } } From f5ac7cc5d86172900dae98b9a6d122ea92a488a4 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:12:00 -0300 Subject: [PATCH 06/72] fix path for ocw-example-ping-pong on ci test script --- scripts/ci/gitlab/pipeline/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/gitlab/pipeline/test.yml b/scripts/ci/gitlab/pipeline/test.yml index f49e36c6a1057..05425602bec20 100644 --- a/scripts/ci/gitlab/pipeline/test.yml +++ b/scripts/ci/gitlab/pipeline/test.yml @@ -320,7 +320,7 @@ 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 ../basic - cargo build --locked --target=wasm32-unknown-unknown --no-default-features From 9632588049088e2942ea038a7345654b9f7f7433 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:14:10 -0300 Subject: [PATCH 07/72] fix workspace paths for refactored ocw examples --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5c413e7d09860..6cd36fdd451f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,8 @@ members = [ "frame/election-provider-support/solution-type", "frame/election-provider-support/solution-type/fuzzer", "frame/examples/basic", - "frame/examples/offchain-worker", + "frame/examples/offchain-worker-ping-pong", + "frame/examples/offchain-worker-price-oracle", "frame/examples/dev-mode", "frame/executive", "frame/nis", From 396f965386ac764bca8e1cd2e2e41b751687c249 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:16:32 -0300 Subject: [PATCH 08/72] fix unused imports --- frame/examples/offchain-worker-price-oracle/src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 70dd0df879b06..ab907681bbb4a 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -58,13 +58,12 @@ #![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, + AppCrypto, CreateSignedTransaction, SendSignedTransaction, + Signer, }, }; use lite_json::json::JsonValue; @@ -75,9 +74,6 @@ use sp_runtime::{ storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, Duration, }, - traits::Zero, - transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, - RuntimeDebug, }; use sp_std::vec::Vec; From b3159429af0a55f4b870c03bbe5a57dd0855a5fc Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:18:15 -0300 Subject: [PATCH 09/72] lint --- frame/examples/offchain-worker-price-oracle/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index ab907681bbb4a..26bc418f967cc 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -218,7 +218,7 @@ pub mod pallet { // 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) => { + Ok(_) => { if let Err(e) = Self::fetch_price_and_send_signed() { log::error!("Error: {}", e); } @@ -232,8 +232,6 @@ pub mod pallet { // already did. Err(MutateStorageError::ConcurrentModification(_)) => log::error!("OCW failed to acquire a lock."), } - - } } From 1414f5d349cf3350283115fdebe619aa22ec4835 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:19:53 -0300 Subject: [PATCH 10/72] fmt --- .../offchain-worker-ping-pong/src/lib.rs | 58 ++++++++++--------- .../offchain-worker-ping-pong/src/tests.rs | 7 ++- .../offchain-worker-price-oracle/src/lib.rs | 49 ++++++++-------- .../offchain-worker-price-oracle/src/tests.rs | 5 +- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 6914b87f198a8..0427b746df2e0 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -98,9 +98,7 @@ use frame_system::{ }; use sp_core::crypto::KeyTypeId; use sp_runtime::{ - offchain::{ - storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, - }, + offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, traits::Zero, transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, RuntimeDebug, @@ -241,10 +239,12 @@ pub mod pallet { let should_send = Self::choose_transaction_type(block_number); let res = match should_send { TransactionType::Signed => Self::ocw_pong_signed(), - TransactionType::UnsignedForAny => - Self::ocw_pong_unsigned_for_any_account(block_number), - TransactionType::UnsignedForAll => - Self::ocw_pong_unsigned_for_all_accounts(block_number), + TransactionType::UnsignedForAny => { + Self::ocw_pong_unsigned_for_any_account(block_number) + }, + TransactionType::UnsignedForAll => { + Self::ocw_pong_unsigned_for_all_accounts(block_number) + }, TransactionType::Raw => Self::ocw_pong_raw_unsigned(block_number), TransactionType::None => Ok(()), }; @@ -332,7 +332,10 @@ pub mod pallet { #[pallet::call_index(4)] #[pallet::weight({0})] - pub fn add_authority(origin: OriginFor, authority: T::AccountId) -> DispatchResultWithPostInfo { + pub fn add_authority( + origin: OriginFor, + authority: T::AccountId, + ) -> DispatchResultWithPostInfo { ensure_root(origin)?; ensure!(!Self::is_authority(&authority), Error::::AlreadyAuthority); @@ -350,7 +353,10 @@ pub mod pallet { #[pallet::call_index(5)] #[pallet::weight({0})] - pub fn remove_authority(origin: OriginFor, authority: T::AccountId) -> DispatchResultWithPostInfo { + pub fn remove_authority( + origin: OriginFor, + authority: T::AccountId, + ) -> DispatchResultWithPostInfo { ensure_root(origin)?; ensure!(Self::is_authority(&authority), Error::::NotAuthority); @@ -413,7 +419,7 @@ pub mod pallet { let signature_valid = SignedPayload::::verify::(payload, signature.clone()); if !signature_valid { - return InvalidTransaction::BadProof.into() + return InvalidTransaction::BadProof.into(); } Self::validate_transaction_parameters(&payload.block_number) } else if let Call::pong_unsigned { block_number, nonce: _n } = call { @@ -435,7 +441,8 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn authorities)] - pub(super) type Authorities = StorageValue<_, BoundedVec, ValueQuery>; + pub(super) type Authorities = + StorageValue<_, BoundedVec, ValueQuery>; /// Defines the block when next unsigned transaction will be accepted. /// @@ -500,8 +507,9 @@ impl Pallet { 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), + 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), } @@ -545,18 +553,16 @@ impl Pallet { } } - fn validate_transaction_parameters( - block_number: &T::BlockNumber, - ) -> TransactionValidity { + fn validate_transaction_parameters(block_number: &T::BlockNumber) -> 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() + 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() + return InvalidTransaction::Future.into(); } ValidTransaction::with_tag_prefix("ExampleOffchainWorker") @@ -593,7 +599,7 @@ impl Pallet { if !signer.can_sign() { return Err( "No local accounts available. Consider adding one via `author_insertKey` RPC.", - ) + ); } let pings = >::get(); @@ -623,14 +629,12 @@ impl Pallet { } /// A helper function to sign payload and send an unsigned pong transaction - fn ocw_pong_unsigned_for_any_account( - block_number: T::BlockNumber, - ) -> Result<(), &'static str> { + fn ocw_pong_unsigned_for_any_account(block_number: T::BlockNumber) -> Result<(), &'static str> { // Make sure we don't read the pings 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") + return Err("Too early to send unsigned transaction"); } let pings = >::get(); @@ -648,7 +652,6 @@ impl Pallet { ) .ok_or("No local accounts accounts available.")?; result.map_err(|()| "Unable to submit transaction")?; - } Ok(()) @@ -662,7 +665,7 @@ impl Pallet { // anyway. let next_unsigned_at = >::get(); if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") + return Err("Too early to send unsigned transaction"); } let pings = >::get(); @@ -680,10 +683,9 @@ impl Pallet { ); for (_account_id, result) in transaction_results.into_iter() { if result.is_err() { - return Err("Unable to submit transaction") + return Err("Unable to submit transaction"); } } - } Ok(()) @@ -695,7 +697,7 @@ impl Pallet { // anyway. let next_unsigned_at = >::get(); if next_unsigned_at > block_number { - return Err("Too early to send unsigned transaction") + return Err("Too early to send unsigned transaction"); } let pings = >::get(); diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index 250534169bca6..147e35f418272 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -28,13 +28,13 @@ use sp_core::{ H256, }; +use sp_api_hidden_includes_construct_runtime::hidden_include::traits::Hooks; use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; use sp_runtime::{ testing::{Header, TestXt}, traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, RuntimeAppPublic, }; -use sp_api_hidden_includes_construct_runtime::hidden_include::traits::Hooks; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -186,7 +186,10 @@ fn should_submit_signed_transaction_on_chain() { 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::PingPongOcwExample(crate::Call::pong_signed { nonce: NONCE })); + assert_eq!( + tx.call, + RuntimeCall::PingPongOcwExample(crate::Call::pong_signed { nonce: NONCE }) + ); }); } diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 26bc418f967cc..51646dc130344 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -61,19 +61,14 @@ use frame_support::traits::Get; use frame_system::{ self as system, - offchain::{ - AppCrypto, CreateSignedTransaction, SendSignedTransaction, - Signer, - }, + 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_runtime::offchain::{ + http, + storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, + Duration, }; use sp_std::vec::Vec; @@ -199,16 +194,18 @@ pub mod pallet { // 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), - } - }); + 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. @@ -224,13 +221,17 @@ pub mod pallet { } }, // We are in the grace period, we should not send a transaction this time. - Err(MutateStorageError::ValueFunctionFailed(RECENTLY_SENT)) => log::info!("Sent transaction too recently, waiting for grace period."), + Err(MutateStorageError::ValueFunctionFailed(RECENTLY_SENT)) => { + 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."), + Err(MutateStorageError::ConcurrentModification(_)) => { + log::error!("OCW failed to acquire a lock.") + }, } } } @@ -283,7 +284,7 @@ impl Pallet { 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. @@ -339,7 +340,7 @@ impl Pallet { // 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) + return Err(http::Error::Unknown); } // Next we want to fully read the response body and collect it to a vector of bytes. diff --git a/frame/examples/offchain-worker-price-oracle/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs index e4a09894605ea..1313dc2457a5b 100644 --- a/frame/examples/offchain-worker-price-oracle/src/tests.rs +++ b/frame/examples/offchain-worker-price-oracle/src/tests.rs @@ -222,7 +222,10 @@ fn should_submit_signed_transaction_on_chain() { 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 })); + assert_eq!( + tx.call, + RuntimeCall::PriceOracleOcwExample(crate::Call::submit_price { price: 15523 }) + ); }); } From 7a25b067fed932c3f5d611a2a75be7ba6d4dd313 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:32:22 -0300 Subject: [PATCH 11/72] update dependencies for offchain-worker-ping-pong --- frame/examples/offchain-worker-ping-pong/Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/Cargo.toml b/frame/examples/offchain-worker-ping-pong/Cargo.toml index 523b6b4145359..94e1bd21c3a4d 100644 --- a/frame/examples/offchain-worker-ping-pong/Cargo.toml +++ b/frame/examples/offchain-worker-ping-pong/Cargo.toml @@ -19,11 +19,11 @@ 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 = "8.0.0", default-features = false, path = "../../../primitives/core" } -sp-io = { version = "8.0.0", default-features = false, path = "../../../primitives/io" } -sp-keystore = { version = "0.14.0", optional = true, path = "../../../primitives/keystore" } -sp-runtime = { version = "8.0.0", default-features = false, path = "../../../primitives/runtime" } -sp-std = { version = "6.0.0", default-features = false, path = "../../../primitives/std" } +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" } cargo-watch = "8.4.0" [features] From 7817d7bfcfabc626b815a533c0fa9c3e53731e31 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:33:57 -0300 Subject: [PATCH 12/72] lint ocw example docs --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 2 +- frame/examples/offchain-worker-price-oracle/src/lib.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 0427b746df2e0..9df911f483759 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -71,7 +71,7 @@ //! --data-raw '{ //! "jsonrpc": "2.0", //! "method": "author_insertKey", -//! "params": ["pong","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], +//! "params": ["pong","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], //! "id": 1 //! }' //! ``` diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 51646dc130344..f2513846f0aac 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -49,12 +49,14 @@ //! --data-raw '{ //! "jsonrpc": "2.0", //! "method": "author_insertKey", -//! "params": ["btc!","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], +//! "params": ["btc!","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], //! "id": 1 //! }' //! ``` //! -//! Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write the keys into disk, and will persist in case the node is restarted (author_insertKey will not). +//! Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write the keys into disk, and will persist in case the node is restarted (`author_insertKey` will not). +//! +//! 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)] From 92db6afba052c21a3dc332f7cf48453b12583210 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:42:00 -0300 Subject: [PATCH 13/72] fix MaxAuthorities --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 9df911f483759..83be10f71bec3 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -442,7 +442,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn authorities)] pub(super) type Authorities = - StorageValue<_, BoundedVec, ValueQuery>; + StorageValue<_, BoundedVec, ValueQuery>; /// Defines the block when next unsigned transaction will be accepted. /// From 29018ff500999e7255af72cddfc013c9892b584f Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 17:59:03 -0300 Subject: [PATCH 14/72] add Authorities logic to ocw price oracle example --- .../offchain-worker-price-oracle/src/lib.rs | 71 +++++++++++++++++-- .../offchain-worker-price-oracle/src/tests.rs | 15 +++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index f2513846f0aac..fdf8eb3c5eaf9 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -145,6 +145,17 @@ pub mod pallet { /// Maximum number of prices. #[pallet::constant] type MaxPrices: Get; + + /// Maximum number of authorities. + #[pallet::constant] + type MaxAuthorities: Get; + } + + #[pallet::error] + pub enum Error { + NotAuthority, + AlreadyAuthority, + TooManyAuthorities, } #[pallet::pallet] @@ -248,17 +259,60 @@ pub mod pallet { /// In our example the `offchain worker` will create, sign & submit a transaction that /// calls this function passing the price. /// - /// todo: docs authorized + /// This only works if the caller is in `Authorities`. #[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)?; - // todo: check authorized + match Self::is_authority(&who) { + true => Self::add_price(Some(who), price), + false => return Err(Error::::NotAuthority.into()), + } + + Ok(().into()) + } + + #[pallet::call_index(1)] + #[pallet::weight({0})] + 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); + + Ok(().into()) + } + + #[pallet::call_index(2)] + #[pallet::weight({0})] + 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); - // Add the price to the on-chain list. - Self::add_price(Some(who), price); Ok(().into()) } } @@ -277,9 +331,18 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn prices)] pub(super) type Prices = StorageValue<_, BoundedVec, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn authorities)] + pub(super) type Authorities = + StorageValue<_, BoundedVec, ValueQuery>; } 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(); diff --git a/frame/examples/offchain-worker-price-oracle/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs index 1313dc2457a5b..1639b40d8e951 100644 --- a/frame/examples/offchain-worker-price-oracle/src/tests.rs +++ b/frame/examples/offchain-worker-price-oracle/src/tests.rs @@ -19,7 +19,7 @@ use crate as example_offchain_worker; use crate::*; use codec::Decode; use frame_support::{ - assert_ok, parameter_types, + assert_noop, assert_ok, parameter_types, traits::{ConstU32, ConstU64}, }; use sp_core::{ @@ -116,6 +116,7 @@ impl Config for Test { type AuthorityId = crypto::TestAuthId; type GracePeriod = ConstU64<5>; type MaxPrices = ConstU32<64>; + type MaxAuthorities = ConstU32<64>; } fn test_pub() -> sp_core::sr25519::Public { @@ -127,11 +128,23 @@ 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 + ); }); } From 26fa0f7c3aae8fa8c6f2f504b624ab17c21adb43 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:01:05 -0300 Subject: [PATCH 15/72] sanitize ocw ping pong example --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 83be10f71bec3..5a5b53cc95e4e 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -286,8 +286,9 @@ pub mod pallet { pub fn pong_signed(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - if Self::is_authority(&who) { - Self::deposit_event(Event::PongAckAuthenticated { nonce }); + match Self::is_authority(&who) { + true => Self::deposit_event(Event::PongAckAuthenticated { nonce }), + false => return Err(Error::::NotAuthority.into()), } Ok(().into()) From ec8cd0550e4c9bde45178aa0da64b4652f91277f Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:03:20 -0300 Subject: [PATCH 16/72] simplify ocw example READMEs --- frame/examples/offchain-worker-ping-pong/README.md | 3 +-- frame/examples/offchain-worker-price-oracle/README.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index ccfc3aafd4e50..4d48b83bf6921 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -1,8 +1,7 @@ # Ping-Pong Offchain Worker Example Pallet -The Ping-Pong Offchain Worker Example: A simple pallet demonstrating -concepts, APIs and structures common to most offchain workers. +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. diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md index ed0713e3b4102..cc482f2f5a577 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -1,8 +1,7 @@ # 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. +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. From e0e1e9d24d4aa5a913e4d8b5782d4e907fc7775c Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:13:29 -0300 Subject: [PATCH 17/72] improve READMEs of ocw examples --- frame/examples/offchain-worker-ping-pong/README.md | 3 +-- frame/examples/offchain-worker-price-oracle/README.md | 9 ++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index 4d48b83bf6921..a903415619c3f 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -47,8 +47,7 @@ $ curl --location --request POST 'http://localhost:9933' \ }' ``` -Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write -the keys into disk, and will persist in case the node is restarted (`author_insertKey` will not). +Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) 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/README.md b/frame/examples/offchain-worker-price-oracle/README.md index cc482f2f5a577..19645c78f2520 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -13,11 +13,11 @@ used in production.** 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. +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 last `64` values to compute +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. @@ -35,6 +35,9 @@ $ curl --location --request POST 'http://localhost:9933' \ }' ``` -Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write the keys into disk, and will persist in case the node is restarted (author_insertKey will not). +Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) 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. License: MIT-0 From 087e0882c43a8640d4007607fa2cf44aeb865883 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:16:27 -0300 Subject: [PATCH 18/72] improve docs of ocw examples --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 3 +-- frame/examples/offchain-worker-price-oracle/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 5a5b53cc95e4e..95442af0cf6dd 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -76,8 +76,7 @@ //! }' //! ``` //! -//! Another alternative for the node admin is to use the key insert subcommand on the node's executable, -//! which will write the keys into disk, and will persist in case the node is restarted (`author_insertKey` will not). +//! Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) 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 index fdf8eb3c5eaf9..cba96c49325fa 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -54,7 +54,7 @@ //! }' //! ``` //! -//! Another alternative for the node admin is to use the key insert subcommand on the node's executable, which will write the keys into disk, and will persist in case the node is restarted (`author_insertKey` will not). +//! Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) 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. From 1478ccd1367a067d02a8c0dba3e1c8a35a1b8574 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:19:10 -0300 Subject: [PATCH 19/72] lint ocw example READMEs --- frame/examples/offchain-worker-ping-pong/README.md | 2 +- frame/examples/offchain-worker-price-oracle/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index a903415619c3f..d88458888f56d 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -42,7 +42,7 @@ $ curl --location --request POST 'http://localhost:9933' \ --data-raw '{ "jsonrpc": "2.0", "method": "author_insertKey", - "params": ["pong","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], + "params": ["pong","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], "id": 1 }' ``` diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md index 19645c78f2520..b2e9e3cb1b364 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -30,7 +30,7 @@ $ curl --location --request POST 'http://localhost:9933' \ --data-raw '{ "jsonrpc": "2.0", "method": "author_insertKey", - "params": ["btc!","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], + "params": ["btc!","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], "id": 1 }' ``` From e55ef38a28fa5155138b145a6fc7b2b5d846dd9b Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:20:45 -0300 Subject: [PATCH 20/72] lint ocw ping pong example docs --- .../examples/offchain-worker-ping-pong/src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 95442af0cf6dd..1d67e61f5b3b8 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -24,10 +24,6 @@ //! Run `cargo doc --package pallet-example-offchain-worker-ping-pong --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.** //! @@ -52,16 +48,18 @@ //! - `PongAckAuthenticated`: emitted when the call was made by an **authenticated** offchain worker //! - `PongAckUnauthenticated`: emitted when the call was made by an **unauthenticated** offchain worker //! -//! 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` +//! 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`: //! @@ -455,7 +453,7 @@ pub mod pallet { } /// Payload used by this example crate to hold pong response -/// data required to submit a transaction. +/// data required to submit an unsigned transaction. #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, scale_info::TypeInfo)] pub struct PongPayload { block_number: BlockNumber, From e340f8b13ca7ee367d01c279b53da630ee2e699d Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:35:02 -0300 Subject: [PATCH 21/72] authorized OCWs don't need to pay fees --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 3 ++- frame/examples/offchain-worker-price-oracle/src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 1d67e61f5b3b8..f02399c4c1367 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -288,7 +288,8 @@ pub mod pallet { false => return Err(Error::::NotAuthority.into()), } - Ok(().into()) + // Authorized OCWs don't need to pay fees + Ok(Pays::No.into()) } #[pallet::call_index(2)] diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index cba96c49325fa..ee5215bcc9552 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -271,7 +271,8 @@ pub mod pallet { false => return Err(Error::::NotAuthority.into()), } - Ok(().into()) + // Authorized OCWs don't need to pay fees + Ok(Pays::No.into()) } #[pallet::call_index(1)] From 585394c07e672112705fcf4de828b187289047e7 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 18:37:31 -0300 Subject: [PATCH 22/72] lint ocw example README --- frame/examples/offchain-worker-price-oracle/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md index b2e9e3cb1b364..ffa01b473dfb2 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -39,5 +39,3 @@ Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCN More complex management models and session based key rotations should be considered, but that's outside the scope of this example. - -License: MIT-0 From f03b2ce4ecd96f368c4b87a5ed3df365c6c46864 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 19:02:13 -0300 Subject: [PATCH 23/72] sanitize Cargo.toml for ocw-example-ping-pong --- .../examples/offchain-worker-ping-pong/Cargo.toml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/Cargo.toml b/frame/examples/offchain-worker-ping-pong/Cargo.toml index 94e1bd21c3a4d..a3428c3e9f854 100644 --- a/frame/examples/offchain-worker-ping-pong/Cargo.toml +++ b/frame/examples/offchain-worker-ping-pong/Cargo.toml @@ -24,9 +24,20 @@ sp-io = { version = "23.0.0", default-features = false, path = "../../../primiti 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" } -cargo-watch = "8.4.0" [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"] +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"] From 34b9c811996ef273252eaa3e21b2020e216ba236 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 19:25:55 -0300 Subject: [PATCH 24/72] simplify logic for ocw ping pong example (send all kinds of txs, regardless of block number) --- .../offchain-worker-ping-pong/README.md | 5 +- .../offchain-worker-ping-pong/src/lib.rs | 126 +++--------------- .../offchain-worker-ping-pong/src/tests.rs | 1 - 3 files changed, 18 insertions(+), 114 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index d88458888f56d..97e4f946912bf 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -50,7 +50,4 @@ $ curl --location --request POST 'http://localhost:9933' \ Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) 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. - -The logic around block numbers only helps create different conditions to trigger the different -kinds of transactions. It is not strictly necessary when implementing offchain workers. \ No newline at end of file +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 index f02399c4c1367..f2224244f780b 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -78,9 +78,6 @@ //! //! More complex management models and session //! based key rotations should be considered, but that's outside the scope of this example. -//! -//! The logic around block numbers only helps create different conditions to trigger the different -//! kinds of transactions. It is not strictly necessary when implementing offchain workers. #![cfg_attr(not(feature = "std"), no_std)] @@ -95,8 +92,6 @@ use frame_system::{ }; use sp_core::crypto::KeyTypeId; use sp_runtime::{ - offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, - traits::Zero, transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, RuntimeDebug, }; @@ -163,14 +158,6 @@ pub mod pallet { // 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` @@ -230,23 +217,23 @@ pub mod pallet { let parent_hash = >::block_hash(block_number - 1u32.into()); log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash); - // 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::ocw_pong_signed(), - TransactionType::UnsignedForAny => { - Self::ocw_pong_unsigned_for_any_account(block_number) - }, - TransactionType::UnsignedForAll => { - Self::ocw_pong_unsigned_for_all_accounts(block_number) - }, - TransactionType::Raw => Self::ocw_pong_raw_unsigned(block_number), - TransactionType::None => Ok(()), - }; - if let Err(e) = res { - log::error!("Error: {}", e); + // we send every kind of transaction in this example + { + if let Err(e) = Self::ocw_pong_signed() { + log::error!("Error: {}", e); + } + + if let Err(e) = Self::ocw_pong_unsigned_for_any_account(block_number) { + log::error!("Error: {}", e); + } + + if let Err(e) = Self::ocw_pong_unsigned_for_all_accounts(block_number) { + log::error!("Error: {}", e); + } + + if let Err(e) = Self::ocw_pong_raw_unsigned(block_number) { + log::error!("Error: {}", e); + } } } @@ -468,90 +455,11 @@ impl SignedPayload for PongPayload Pallet { fn is_authority(who: &T::AccountId) -> bool { >::get().contains(who) } - /// 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: T::BlockNumber) -> 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 == T::BlockNumber::from(1u32) { - TransactionType::UnsignedForAny - } else if transaction_type == T::BlockNumber::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, - } - } - fn validate_transaction_parameters(block_number: &T::BlockNumber) -> TransactionValidity { // Now let's check if the transaction has any chance to succeed. let next_unsigned_at = >::get(); diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index 147e35f418272..35bc706ecaaa4 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -115,7 +115,6 @@ parameter_types! { impl Config for Test { type RuntimeEvent = RuntimeEvent; type AuthorityId = crypto::TestAuthId; - type GracePeriod = ConstU64<5>; type UnsignedInterval = ConstU64<128>; type UnsignedPriority = UnsignedPriority; type MaxPings = ConstU32<64>; From ede32f21becbe1c4e2bef2cbcbdae245cef053f7 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 19:28:03 -0300 Subject: [PATCH 25/72] fix ports for curl example of ocw examples --- frame/examples/offchain-worker-ping-pong/README.md | 2 +- frame/examples/offchain-worker-ping-pong/src/lib.rs | 2 +- frame/examples/offchain-worker-price-oracle/README.md | 2 +- frame/examples/offchain-worker-price-oracle/src/lib.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index 97e4f946912bf..7e1a3d7e2fb05 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -37,7 +37,7 @@ Here's an example of how a node admin can inject some keys into the keystore, so can call `pong_signed`: ```bash -$ curl --location --request POST 'http://localhost:9933' \ +$ curl --location --request POST 'http://localhost:9944' \ --header 'Content-Type: application/json' \ --data-raw '{ "jsonrpc": "2.0", diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index f2224244f780b..aeceb393b003a 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -64,7 +64,7 @@ //! can call `pong_signed`: //! //! ```bash -//! $ curl --location --request POST 'http://localhost:9933' \ +//! $ curl --location --request POST 'http://localhost:9944' \ //! --header 'Content-Type: application/json' \ //! --data-raw '{ //! "jsonrpc": "2.0", diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md index ffa01b473dfb2..5c44542b6d3c5 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -25,7 +25,7 @@ Only authorized keys are allowed to submit the price. The authorization key shou Here's an example of how a node admin can inject some keys into the keystore: ```bash -$ curl --location --request POST 'http://localhost:9933' \ +$ curl --location --request POST 'http://localhost:9944' \ --header 'Content-Type: application/json' \ --data-raw '{ "jsonrpc": "2.0", diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index ee5215bcc9552..b37cdfd109f0d 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -44,7 +44,7 @@ //! Here's an example of how a node admin can inject some keys into the keystore: //! //! ```bash -//! $ curl --location --request POST 'http://localhost:9933' \ +//! $ curl --location --request POST 'http://localhost:9944' \ //! --header 'Content-Type: application/json' \ //! --data-raw '{ //! "jsonrpc": "2.0", From 83a097b5ea3d43b0904faadb427611407ed4b184 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 19:33:40 -0300 Subject: [PATCH 26/72] reduce UnsignedInterval for mock ocw-example-ping-pong --- frame/examples/offchain-worker-ping-pong/src/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index 35bc706ecaaa4..40c4c4a91a2a8 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -115,7 +115,7 @@ parameter_types! { impl Config for Test { type RuntimeEvent = RuntimeEvent; type AuthorityId = crypto::TestAuthId; - type UnsignedInterval = ConstU64<128>; + type UnsignedInterval = ConstU64<16>; type UnsignedPriority = UnsignedPriority; type MaxPings = ConstU32<64>; type MaxAuthorities = ConstU32<64>; From 4f4c40d250fa725bed25e4d40e500ee4b0ca080c Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 19:36:26 -0300 Subject: [PATCH 27/72] sanitize comments for ocw-example-ping-pong events --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index aeceb393b003a..a786c46e5d385 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -365,9 +365,9 @@ pub mod pallet { pub enum Event { /// Event generated when new ping is received. Ping { nonce: u32 }, - /// Event generated when new pong_authenticated transaction is accepted. + /// Event generated when new pong_signed transaction is accepted. PongAckAuthenticated { nonce: u32 }, - /// Event generated when new pong_unauthenticated transaction is accepted. + /// Event generated when new pong_unsigned* transaction is accepted. PongAckUnauthenticated { nonce: u32 }, } From 0872689c12b40675d7353dd04cc1d9799cae1472 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 19:53:22 -0300 Subject: [PATCH 28/72] ocw-example-ping-pong multiplexes unsigned tx type --- .../offchain-worker-ping-pong/src/lib.rs | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index a786c46e5d385..bc83f9171facf 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -94,6 +94,7 @@ use sp_core::crypto::KeyTypeId; use sp_runtime::{ transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, RuntimeDebug, + traits::Zero, }; #[cfg(test)] @@ -217,23 +218,28 @@ pub mod pallet { let parent_hash = >::block_hash(block_number - 1u32.into()); log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash); - // we send every kind of transaction in this example - { - if let Err(e) = Self::ocw_pong_signed() { + // try to send unsigned (depends on `NextUnsignedAt`) + // we also choose which kind 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 == T::BlockNumber::from(1u32) { + // node needs to be loaded with keys if let Err(e) = Self::ocw_pong_unsigned_for_any_account(block_number) { log::error!("Error: {}", e); } - + } else if unsigned_type == T::BlockNumber::from(2u32) { + // node needs to be loaded with keys if let Err(e) = Self::ocw_pong_unsigned_for_all_accounts(block_number) { log::error!("Error: {}", e); } + } - if let Err(e) = Self::ocw_pong_raw_unsigned(block_number) { - log::error!("Error: {}", e); - } + // try to send a pong_signed (node needs to be loaded with keys) + if let Err(e) = Self::ocw_pong_signed() { + log::error!("Error: {}", e); } } @@ -290,7 +296,10 @@ pub mod pallet { ensure_none(origin)?; // Emit the PongAckUnauthenticated event - Self::deposit_event(Event::PongAckUnauthenticated { nonce }); + 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(); @@ -308,7 +317,10 @@ pub mod pallet { // This ensures that the function can only be called via unsigned transaction. ensure_none(origin)?; - Self::deposit_event(Event::PongAckUnauthenticated { nonce: pong_payload.nonce }); + 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(); @@ -359,6 +371,12 @@ pub mod pallet { } } + #[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)] @@ -368,7 +386,7 @@ pub mod pallet { /// Event generated when new pong_signed transaction is accepted. PongAckAuthenticated { nonce: u32 }, /// Event generated when new pong_unsigned* transaction is accepted. - PongAckUnauthenticated { nonce: u32 }, + PongAckUnauthenticated { nonce: u32, unsigned_type: UnsignedType }, } #[pallet::validate_unsigned] From 6434b81115f1ae2aa818a7d2a56ecbdf1e0ee501 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 20:31:44 -0300 Subject: [PATCH 29/72] check NextUnsignedAt earlier --- .../offchain-worker-ping-pong/src/lib.rs | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index bc83f9171facf..7e732f268d5b5 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -218,22 +218,25 @@ pub mod pallet { let parent_hash = >::block_hash(block_number - 1u32.into()); log::debug!("Current block: {:?} (parent hash: {:?})", block_number, parent_hash); - // try to send unsigned (depends on `NextUnsignedAt`) - // we also choose which kind 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 == T::BlockNumber::from(1u32) { - // node needs to be loaded with keys - if let Err(e) = Self::ocw_pong_unsigned_for_any_account(block_number) { - log::error!("Error: {}", e); - } - } else if unsigned_type == T::BlockNumber::from(2u32) { - // node needs to be loaded with keys - if let Err(e) = Self::ocw_pong_unsigned_for_all_accounts(block_number) { - log::error!("Error: {}", e); + // 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 == T::BlockNumber::from(1u32) { + // node needs to be loaded with keys + if let Err(e) = Self::ocw_pong_unsigned_for_any_account(block_number) { + log::error!("Error: {}", e); + } + } else if unsigned_type == T::BlockNumber::from(2u32) { + // node needs to be loaded with keys + if let Err(e) = Self::ocw_pong_unsigned_for_all_accounts(block_number) { + log::error!("Error: {}", e); + } } } @@ -555,13 +558,6 @@ impl Pallet { /// A helper function to sign payload and send an unsigned pong transaction fn ocw_pong_unsigned_for_any_account(block_number: T::BlockNumber) -> Result<(), &'static str> { - // Make sure we don't read the pings 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"); - } - let pings = >::get(); for p in pings { let Ping(nonce) = p; @@ -586,13 +582,6 @@ impl Pallet { fn ocw_pong_unsigned_for_all_accounts( block_number: T::BlockNumber, ) -> Result<(), &'static str> { - // Make sure we don't read the pings 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"); - } - let pings = >::get(); for p in pings { let Ping(nonce) = p; @@ -618,13 +607,6 @@ impl Pallet { /// A helper function to send a raw unsigned pong transaction. fn ocw_pong_raw_unsigned(block_number: T::BlockNumber) -> Result<(), &'static str> { - // Make sure we don't read the ping 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"); - } - let pings = >::get(); for p in pings { let Ping(nonce) = p; From 0344910685fa897571f75571b7b324416dd2d39e Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 20:33:38 -0300 Subject: [PATCH 30/72] document that OCW account needs to be funded --- frame/examples/offchain-worker-ping-pong/README.md | 2 +- frame/examples/offchain-worker-ping-pong/src/lib.rs | 4 ++-- frame/examples/offchain-worker-price-oracle/README.md | 2 +- frame/examples/offchain-worker-price-oracle/src/lib.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index 7e1a3d7e2fb05..1fda136b29ac0 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -47,7 +47,7 @@ $ curl --location --request POST 'http://localhost:9944' \ }' ``` -Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) is added to `Authorities` in the runtime by adding it via `add_authority` extrinsic (from `root`). +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 index 7e732f268d5b5..c13c40ae94d20 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -74,7 +74,7 @@ //! }' //! ``` //! -//! Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) is added to `Authorities` in the runtime by adding it via `add_authority` extrinsic (from `root`). +//! 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. @@ -240,7 +240,7 @@ pub mod pallet { } } - // try to send a pong_signed (node needs to be loaded with keys) + // 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); } diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md index 5c44542b6d3c5..b86b50bf09319 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -35,7 +35,7 @@ $ curl --location --request POST 'http://localhost:9944' \ }' ``` -Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) is added to `Authorities` in the runtime by adding it via `add_authority` extrinsic (from `root`). +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 index b37cdfd109f0d..217674fef430f 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -54,7 +54,7 @@ //! }' //! ``` //! -//! Then make sure that the corresponding address (`5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg`) is added to `Authorities` in the runtime by adding it via `add_authority` extrinsic (from `root`). +//! 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. From 9cf6d06bc0f701dffe65c200adf9b3d9ee00bf19 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 21:07:34 -0300 Subject: [PATCH 31/72] fmt --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index c13c40ae94d20..844eb7a17019f 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -92,9 +92,9 @@ use frame_system::{ }; use sp_core::crypto::KeyTypeId; use sp_runtime::{ + traits::Zero, transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction}, RuntimeDebug, - traits::Zero, }; #[cfg(test)] From 75b2068b05dfed92cb5ae65de47a1c08ec935478 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Wed, 31 May 2023 21:19:14 -0300 Subject: [PATCH 32/72] sanitize ocw-example-ping-pong docs --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 844eb7a17019f..9974220b43a42 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -411,7 +411,7 @@ pub mod pallet { /// authorized OCWs. Anyone could have signed those payloads, even malicious actors trying /// to "impersonate" an OCW. /// - /// There are NO implicit security assumptions about here! + /// 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 { From 305416a7ab1c585075ebbd9044cac2c8adc4a37e Mon Sep 17 00:00:00 2001 From: bernardo Date: Thu, 8 Jun 2023 18:07:36 -0300 Subject: [PATCH 33/72] Update frame/examples/offchain-worker-ping-pong/README.md Co-authored-by: Bruno Galvao --- frame/examples/offchain-worker-ping-pong/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index 1fda136b29ac0..3222ac4d62307 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -6,7 +6,7 @@ A simple pallet demonstrating concepts, APIs and structures common to most offch 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. +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/), From cbd15dac249eb5be3899b350b2b9df54e505bff2 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Thu, 8 Jun 2023 18:16:11 -0300 Subject: [PATCH 34/72] rename pong_payload --- frame/examples/offchain-worker-ping-pong/src/tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index 40c4c4a91a2a8..fcbe5158a9b9d 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -213,7 +213,7 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { t.register_extension(TransactionPoolExt::new(pool)); t.register_extension(KeystoreExt::new(keystore)); - let price_payload = PongPayload { + let pong_payload = PongPayload { block_number: 1, nonce: NONCE, public: ::Public::from(public_key), @@ -233,13 +233,13 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { signature, }) = tx.call { - assert_eq!(body, price_payload); + assert_eq!(body, pong_payload); let signature_valid = ::Public, ::BlockNumber, - > as SignedPayload>::verify::(&price_payload, signature); + > as SignedPayload>::verify::(&pong_payload, signature); assert!(signature_valid); } From 77e792969cfca5059796a5b64bb7439c32daead9 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Thu, 8 Jun 2023 18:22:30 -0300 Subject: [PATCH 35/72] sanitize comment --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 9974220b43a42..88bece76c936b 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -495,9 +495,7 @@ impl Pallet { 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). + // transactions in the pool. .priority(T::UnsignedPriority::get().saturating_add(0)) // 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, From 1a5cd20cf1ba349100f8de37e42f5c1e04947e23 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Thu, 8 Jun 2023 18:32:57 -0300 Subject: [PATCH 36/72] add missing events --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 8 ++++++++ frame/examples/offchain-worker-price-oracle/src/lib.rs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 88bece76c936b..c053367fdfc81 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -349,6 +349,8 @@ pub mod pallet { Authorities::::set(authorities); + Self::deposit_event(Event::AuthorityAdded { authority }); + Ok(().into()) } @@ -370,6 +372,8 @@ pub mod pallet { Authorities::::set(authorities); + Self::deposit_event(Event::AuthorityAdded { authority }); + Ok(().into()) } } @@ -390,6 +394,10 @@ pub mod pallet { 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 }, } #[pallet::validate_unsigned] diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 217674fef430f..6e6df57f7860d 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -293,6 +293,8 @@ pub mod pallet { Authorities::::set(authorities); + Self::deposit_event(Event::AuthorityAdded { authority }); + Ok(().into()) } @@ -314,6 +316,8 @@ pub mod pallet { Authorities::::set(authorities); + Self::deposit_event(Event::AuthorityAdded { authority }); + Ok(().into()) } } @@ -324,6 +328,10 @@ pub mod pallet { 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. From 31723f91b4f2d88e689c5d29d04aeb2d24796b0d Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Thu, 8 Jun 2023 18:37:36 -0300 Subject: [PATCH 37/72] rearrange pallet code for ocw examples --- .../offchain-worker-ping-pong/src/lib.rs | 90 +++++++++---------- .../offchain-worker-price-oracle/src/lib.rs | 48 +++++----- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index c053367fdfc81..385ca1a55a1bd 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -190,6 +190,51 @@ pub mod pallet { 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<_, T::BlockNumber, ValueQuery>; + #[pallet::pallet] pub struct Pallet(_); @@ -378,28 +423,6 @@ pub mod pallet { } } - #[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 }, - } - #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; @@ -444,29 +467,6 @@ pub mod pallet { } } } - - /// 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<_, T::BlockNumber, ValueQuery>; } /// Payload used by this example crate to hold pong response diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 6e6df57f7860d..5924c785669aa 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -151,6 +151,30 @@ pub mod pallet { 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>; + + #[pallet::storage] + #[pallet::getter(fn authorities)] + pub(super) type Authorities = + StorageValue<_, BoundedVec, ValueQuery>; + #[pallet::error] pub enum Error { NotAuthority, @@ -321,30 +345,6 @@ pub mod pallet { 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 }, - /// 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>; - - #[pallet::storage] - #[pallet::getter(fn authorities)] - pub(super) type Authorities = - StorageValue<_, BoundedVec, ValueQuery>; } impl Pallet { From 24704aa82fa72ce96cc1489cb4639d3f4c87315a Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Thu, 8 Jun 2023 18:39:43 -0300 Subject: [PATCH 38/72] improve comments about keys on unsigned tx with signed payloads --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 385ca1a55a1bd..79f2613539e6c 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -273,12 +273,12 @@ pub mod pallet { log::error!("Error: {}", e); } } else if unsigned_type == T::BlockNumber::from(1u32) { - // node needs to be loaded with keys + // 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 == T::BlockNumber::from(2u32) { - // node needs to be loaded with keys + // 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); } From 7fa89195080258da5e0d22d497ad62b4f35636f7 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Thu, 8 Jun 2023 20:55:09 -0300 Subject: [PATCH 39/72] improve comments on ocw ping pong example --- frame/examples/offchain-worker-ping-pong/README.md | 4 ++-- frame/examples/offchain-worker-ping-pong/src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index 3222ac4d62307..da26d37bbbe63 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -21,8 +21,8 @@ block, it reacts by sending a transaction to send a Pong with the corresponding 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 -- `PongAckUnauthenticated`: emitted when the call was made by an **unauthenticated** offchain worker +- `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` diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 79f2613539e6c..fb210613e603f 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -45,8 +45,8 @@ //! 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 -//! - `PongAckUnauthenticated`: emitted when the call was made by an **unauthenticated** offchain worker +//! - `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 25e1aeb960f8e12e4199f951ec1ec812d661b9d0 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:09:21 -0400 Subject: [PATCH 40/72] Update scripts/ci/gitlab/pipeline/test.yml Co-authored-by: Guillaume Yu Thiolliere --- scripts/ci/gitlab/pipeline/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/ci/gitlab/pipeline/test.yml b/scripts/ci/gitlab/pipeline/test.yml index 05425602bec20..8e44e4b27ae90 100644 --- a/scripts/ci/gitlab/pipeline/test.yml +++ b/scripts/ci/gitlab/pipeline/test.yml @@ -322,6 +322,9 @@ test-frame-examples-compile-to-wasm: - rusty-cachier snapshot create - 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 + - cargo build --locked --target=wasm32-unknown-unknown --no-default-features - cd ../basic - cargo build --locked --target=wasm32-unknown-unknown --no-default-features - rusty-cachier cache upload From 01af46e9972a70e0cc25703971b216b93df34e61 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:11:44 -0400 Subject: [PATCH 41/72] remove markdown-link-check-disable --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index fb210613e603f..84dfe132b0f04 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -15,7 +15,6 @@ // 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 From 0f0234035f3637123a773c7721b69e011db48c31 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:12:40 -0400 Subject: [PATCH 42/72] remove markdown-link-check-disable --- frame/examples/offchain-worker-ping-pong/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/README.md b/frame/examples/offchain-worker-ping-pong/README.md index da26d37bbbe63..56526fd5ba325 100644 --- a/frame/examples/offchain-worker-ping-pong/README.md +++ b/frame/examples/offchain-worker-ping-pong/README.md @@ -1,4 +1,3 @@ - # Ping-Pong Offchain Worker Example Pallet A simple pallet demonstrating concepts, APIs and structures common to most offchain workers. From 37814da8cbfe1579087158bc47443a079a837012 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:14:42 -0400 Subject: [PATCH 43/72] remove markdown-link-check-disable --- frame/examples/offchain-worker-price-oracle/README.md | 1 - frame/examples/offchain-worker-price-oracle/src/lib.rs | 2 -- 2 files changed, 3 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/README.md b/frame/examples/offchain-worker-price-oracle/README.md index b86b50bf09319..13872548142bf 100644 --- a/frame/examples/offchain-worker-price-oracle/README.md +++ b/frame/examples/offchain-worker-price-oracle/README.md @@ -1,4 +1,3 @@ - # Price Oracle Offchain Worker Example Pallet A simple pallet demonstrating concepts, APIs and structures common to most offchain workers. diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 5924c785669aa..5233de6e34430 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -15,8 +15,6 @@ // 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 From 1c97f28b86e714c72f54207b9be07b6e4edac8b3 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:17:41 -0400 Subject: [PATCH 44/72] add dev_mode --- .../examples/offchain-worker-ping-pong/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 84dfe132b0f04..78e9ae60794c1 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -141,7 +141,7 @@ pub mod crypto { pub use pallet::*; -#[frame_support::pallet] +#[frame_support::pallet(dev_mode)] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; @@ -301,7 +301,7 @@ pub mod pallet { #[pallet::call] impl Pallet { #[pallet::call_index(0)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn ping(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { let _who = ensure_signed(origin)?; @@ -319,7 +319,7 @@ pub mod pallet { } #[pallet::call_index(1)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn pong_signed(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; @@ -333,7 +333,7 @@ pub mod pallet { } #[pallet::call_index(2)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn pong_unsigned( origin: OriginFor, _block_number: T::BlockNumber, @@ -355,7 +355,7 @@ pub mod pallet { } #[pallet::call_index(3)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn pong_unsigned_with_signed_payload( origin: OriginFor, pong_payload: PongPayload, @@ -376,7 +376,7 @@ pub mod pallet { } #[pallet::call_index(4)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn add_authority( origin: OriginFor, authority: T::AccountId, @@ -399,7 +399,7 @@ pub mod pallet { } #[pallet::call_index(5)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn remove_authority( origin: OriginFor, authority: T::AccountId, From 27287380bd96b675816c3d6908724710b62d9f1f Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:20:46 -0400 Subject: [PATCH 45/72] add dev_mode --- frame/examples/offchain-worker-price-oracle/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 5233de6e34430..65e39be5fcb37 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -117,7 +117,7 @@ pub mod crypto { pub use pallet::*; -#[frame_support::pallet] +#[frame_support::pallet(dev_mode)] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; @@ -283,7 +283,7 @@ pub mod pallet { /// /// This only works if the caller is in `Authorities`. #[pallet::call_index(0)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn submit_price(origin: OriginFor, price: u32) -> DispatchResultWithPostInfo { // Retrieve sender of the transaction. let who = ensure_signed(origin)?; @@ -298,7 +298,7 @@ pub mod pallet { } #[pallet::call_index(1)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn add_authority( origin: OriginFor, authority: T::AccountId, @@ -321,7 +321,7 @@ pub mod pallet { } #[pallet::call_index(2)] - #[pallet::weight({0})] + #[pallet::weight(0)] pub fn remove_authority( origin: OriginFor, authority: T::AccountId, From 46fb7cd147b70ad60b86473d88f2b1738bab78c2 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:25:00 -0400 Subject: [PATCH 46/72] wrap using rewrap extension --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 78e9ae60794c1..96e94c8bbd929 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -468,8 +468,8 @@ pub mod pallet { } } -/// Payload used by this example crate to hold pong response -/// data required to submit an unsigned transaction. +/// 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, From 1a78520c15708b4fe715b9ad6a31565658be5450 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:31:19 -0400 Subject: [PATCH 47/72] update comments with rewrap --- .../offchain-worker-ping-pong/src/lib.rs | 160 ++++++++++-------- 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 96e94c8bbd929..00cfa4fd04c5f 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -101,16 +101,16 @@ 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`). +/// 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. +/// 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; @@ -228,8 +228,8 @@ pub mod pallet { /// 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. + /// 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<_, T::BlockNumber, ValueQuery>; @@ -241,24 +241,26 @@ pub mod pallet { 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. + /// 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: T::BlockNumber) { - // 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. + // 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. + // 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. + // 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); @@ -284,7 +286,8 @@ pub mod pallet { } } - // try to send a pong_signed (node needs to be loaded with keys, account needs to be funded) + // 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); } @@ -339,7 +342,8 @@ pub mod pallet { _block_number: T::BlockNumber, nonce: u32, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction. + // This ensures that the function can only be called via unsigned + // transaction. ensure_none(origin)?; // Emit the PongAckUnauthenticated event @@ -348,7 +352,8 @@ pub mod pallet { unsigned_type: UnsignedType::RawUnsigned, }); - // now increment the block number at which we expect next unsigned transaction. + // 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()) @@ -361,7 +366,8 @@ pub mod pallet { pong_payload: PongPayload, _signature: T::Signature, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned transaction. + // This ensures that the function can only be called via unsigned + // transaction. ensure_none(origin)?; Self::deposit_event(Event::PongAckUnauthenticated { @@ -369,7 +375,8 @@ pub mod pallet { unsigned_type: UnsignedType::UnsignedWithSignedPayload, }); - // now increment the block number at which we expect next unsigned transaction. + // 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()) @@ -428,18 +435,21 @@ pub mod pallet { /// 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. + /// 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! + /// ⚠ 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**! + /// 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. + /// 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 { @@ -449,10 +459,10 @@ pub mod pallet { 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) + // ⚠ 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 { @@ -501,27 +511,27 @@ impl Pallet { } ValidTransaction::with_tag_prefix("ExampleOffchainWorker") - // We set base priority to 2**20 and hope it's included before any other - // transactions in the pool. + // We set base priority to 2**20 and hope it's included before any + // other transactions in the pool. .priority(T::UnsignedPriority::get().saturating_add(0)) - // 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. + // 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. + // 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() } @@ -539,14 +549,15 @@ impl Pallet { 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`. + // 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. + // 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 } }); @@ -615,19 +626,20 @@ impl Pallet { let pings = >::get(); for p in pings { let Ping(nonce) = p; - // 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. + // 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) + // 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 + // 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.")?; } From 17cee6549ddbb777d2188310d567e6747574c818 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:36:46 -0400 Subject: [PATCH 48/72] update comments with rewrap --- .../offchain-worker-ping-pong/src/lib.rs | 68 +++--- .../offchain-worker-price-oracle/src/lib.rs | 225 ++++++++++-------- 2 files changed, 164 insertions(+), 129 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 00cfa4fd04c5f..02d4dd21ca120 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -20,47 +20,54 @@ //! 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. +//! 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.** +//! **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. +//! 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. +//! 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). +//! 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. +//! 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 +//! - `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**. +//! 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: +//! 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!** ⚠️ +//! ⚠️ **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`: +//! 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' \ @@ -73,10 +80,13 @@ //! }' //! ``` //! -//! 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`). +//! 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. +//! 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)] diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 65e39be5fcb37..24fda57c28c22 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -20,26 +20,29 @@ //! 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. +//! 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.** +//! **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. +//! 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. +//! 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. +//! 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: +//! Here's an example of how a node admin can inject some keys into the +//! keystore: //! //! ```bash //! $ curl --location --request POST 'http://localhost:9944' \ @@ -52,9 +55,13 @@ //! }' //! ``` //! -//! 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`). +//! 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. +//! 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)] @@ -77,16 +84,18 @@ 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`). +/// 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. +/// 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; @@ -187,83 +196,94 @@ pub mod pallet { 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. + /// 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: T::BlockNumber) { - // 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. + // 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. + // 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. + // 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. + // 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); - /// A friendlier name for the error that is going to be returned in case we are in the grace - /// period. + /// 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. + // 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. + // 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. + // 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. + // 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. + // 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. + // 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. + // We are in the grace period, we should not send a transaction + // this time. Err(MutateStorageError::ValueFunctionFailed(RECENTLY_SENT)) => { 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. + // 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.") }, @@ -276,10 +296,10 @@ pub mod pallet { 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 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)] @@ -358,18 +378,19 @@ impl Pallet { "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. + // 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`. + // 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. + // 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 } }); @@ -385,39 +406,42 @@ impl Pallet { /// 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. + // 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 + // 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. + // 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. + // 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. + // 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. + // 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. @@ -441,7 +465,8 @@ impl Pallet { /// 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. + /// 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()? { From 3992198bb8a2b2ec6029744b86fef570087458a3 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:51:06 -0400 Subject: [PATCH 49/72] cargo fmt -p pallet-example-offchain-worker-price-oracle --- Cargo.lock | 19 ++++++++++++++++++- .../offchain-worker-price-oracle/src/lib.rs | 16 +++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e37cc1cb52c90..dc0804b863941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6494,7 +6494,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", diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 24fda57c28c22..f1747fde3af15 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -180,7 +180,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn authorities)] pub(super) type Authorities = - StorageValue<_, BoundedVec, ValueQuery>; + StorageValue<_, BoundedVec, ValueQuery>; #[pallet::error] pub enum Error { @@ -250,9 +250,8 @@ pub mod pallet { // 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) - }, + 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), @@ -268,11 +267,10 @@ pub mod pallet { match res { // The value has been set correctly, which means we can safely // send a transaction now. - Ok(_) => { + 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(RECENTLY_SENT)) => { @@ -376,7 +374,7 @@ impl Pallet { 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. @@ -436,7 +434,7 @@ impl Pallet { // response. if response.code != 200 { log::warn!("Unexpected status code: {}", response.code); - return Err(http::Error::Unknown); + return Err(http::Error::Unknown) } // Next we want to fully read the response body and collect it to a From 69c1ae49c36d315c8e9a05d85eed3d776dd194d2 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 18:52:46 -0400 Subject: [PATCH 50/72] cargo fmt -p pallet-example-offchain-worker-ping-pong --- .../offchain-worker-ping-pong/src/lib.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 02d4dd21ca120..b18e6d69dcf74 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -47,10 +47,10 @@ //! 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 +//! - `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 @@ -233,7 +233,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn authorities)] pub(super) type Authorities = - StorageValue<_, BoundedVec, ValueQuery>; + StorageValue<_, BoundedVec, ValueQuery>; /// Defines the block when next unsigned transaction will be accepted. /// @@ -476,7 +476,7 @@ pub mod pallet { let signature_valid = SignedPayload::::verify::(payload, signature.clone()); if !signature_valid { - return InvalidTransaction::BadProof.into(); + return InvalidTransaction::BadProof.into() } Self::validate_transaction_parameters(&payload.block_number) } else if let Call::pong_unsigned { block_number, nonce: _n } = call { @@ -512,12 +512,12 @@ impl Pallet { // 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(); + 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(); + return InvalidTransaction::Future.into() } ValidTransaction::with_tag_prefix("ExampleOffchainWorker") @@ -552,7 +552,7 @@ impl Pallet { if !signer.can_sign() { return Err( "No local accounts available. Consider adding one via `author_insertKey` RPC.", - ); + ) } let pings = >::get(); @@ -623,7 +623,7 @@ impl Pallet { ); for (_account_id, result) in transaction_results.into_iter() { if result.is_err() { - return Err("Unable to submit transaction"); + return Err("Unable to submit transaction") } } } From a4c42160bd93f1ad08edf31f05c5af3412c473d3 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Tue, 20 Jun 2023 19:16:20 -0400 Subject: [PATCH 51/72] add ocw examples as dependencies --- Cargo.lock | 26 +++++++++++++++++++------- frame/examples/Cargo.toml | 9 ++++++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55ddcc1c48a8f..34db32d5819dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6554,41 +6554,41 @@ dependencies = [ ] [[package]] -name = "pallet-example-offchain-worker-ping-pong" +name = "pallet-example-kitchensink" version = "4.0.0-dev" dependencies = [ + "frame-benchmarking", "frame-support", "frame-system", - "lite-json", "log", + "pallet-balances", "parity-scale-codec", "scale-info", "sp-core", "sp-io", - "sp-keystore", "sp-runtime", "sp-std", ] [[package]] -name = "pallet-example-offchain-worker-price-oracle" +name = "pallet-example-offchain-worker-ping-pong" version = "4.0.0-dev" dependencies = [ - "frame-benchmarking", "frame-support", "frame-system", + "lite-json", "log", - "pallet-balances", "parity-scale-codec", "scale-info", "sp-core", "sp-io", + "sp-keystore", "sp-runtime", "sp-std", ] [[package]] -name = "pallet-example-offchain-worker-ping-pong" +name = "pallet-example-offchain-worker-price-oracle" version = "4.0.0-dev" dependencies = [ "frame-support", @@ -6604,6 +6604,18 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-examples" +version = "4.0.0-dev" +dependencies = [ + "pallet-default-config-example", + "pallet-dev-mode", + "pallet-example-basic", + "pallet-example-kitchensink", + "pallet-example-offchain-worker-ping-pong", + "pallet-example-offchain-worker-price-oracle", +] + [[package]] name = "pallet-fast-unstake" version = "4.0.0-dev" diff --git a/frame/examples/Cargo.toml b/frame/examples/Cargo.toml index 57def091b1f63..3e141c34563a6 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" } @@ -23,14 +24,16 @@ 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", ] 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", ] \ No newline at end of file From d4b76cba12a2e0c6178f6e992f32134472fe3319 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Wed, 21 Jun 2023 09:26:49 -0400 Subject: [PATCH 52/72] remove duplicate cargo build --- scripts/ci/gitlab/pipeline/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/ci/gitlab/pipeline/test.yml b/scripts/ci/gitlab/pipeline/test.yml index 43c903f229279..b827cd22aa161 100644 --- a/scripts/ci/gitlab/pipeline/test.yml +++ b/scripts/ci/gitlab/pipeline/test.yml @@ -324,7 +324,6 @@ test-frame-examples-compile-to-wasm: - 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 - - cargo build --locked --target=wasm32-unknown-unknown --no-default-features - cd ../basic - cargo build --locked --target=wasm32-unknown-unknown --no-default-features - rusty-cachier cache upload From 4f4e4565d9acb67128993c7283e2dc5bbdb9e9c4 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Sun, 16 Jul 2023 15:12:05 -0500 Subject: [PATCH 53/72] use `DefaultConfig` - https://github.com/paritytech/substrate/pull/13454 --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 5 ++++- frame/examples/offchain-worker-price-oracle/src/lib.rs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index b18e6d69dcf74..8d30ef9efb325 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -158,12 +158,14 @@ pub mod pallet { use frame_system::pallet_prelude::*; /// This pallet's configuration trait - #[pallet::config] + #[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 @@ -172,6 +174,7 @@ pub mod pallet { /// /// This ensures that we only accept unsigned transactions once, every `UnsignedInterval` /// blocks. + #[pallet::no_default] #[pallet::constant] type UnsignedInterval: Get; diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index f1747fde3af15..8bb381f7cfd36 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -133,12 +133,14 @@ pub mod pallet { use frame_system::pallet_prelude::*; /// This pallet's configuration trait - #[pallet::config] + #[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. @@ -146,6 +148,7 @@ pub mod pallet { /// 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; From b4bd8290c0583304e323491f7f38eafa79bdfb11 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 20 Jul 2023 18:12:54 +0200 Subject: [PATCH 54/72] implementation of default config for offchain-worker-ping-pong --- .../offchain-worker-ping-pong/src/lib.rs | 17 +++++++++++++++++ .../offchain-worker-ping-pong/src/tests.rs | 11 ++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 8d30ef9efb325..f4a2affc61d6b 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -489,6 +489,23 @@ pub mod pallet { } } } + + /// 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 diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index fcbe5158a9b9d..5d198fce8a877 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -17,9 +17,11 @@ use crate as example_offchain_worker; use crate::*; +use frame_support::derive_impl; +use pallet::config_preludes::*; use codec::Decode; use frame_support::{ - assert_ok, parameter_types, + assert_ok, traits::{ConstU32, ConstU64}, }; use sp_core::{ @@ -108,17 +110,12 @@ 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 UnsignedInterval = ConstU64<16>; - type UnsignedPriority = UnsignedPriority; - type MaxPings = ConstU32<64>; - type MaxAuthorities = ConstU32<64>; } fn user_pub() -> sp_core::sr25519::Public { From 0a8010d938d4b66963f96e00b8b2f82509d3434a Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 20 Jul 2023 18:16:44 +0200 Subject: [PATCH 55/72] implementation of default config for offchain-worker-price-oracle --- .../offchain-worker-ping-pong/src/tests.rs | 3 +- .../offchain-worker-price-oracle/src/lib.rs | 29 +++++++++++++++---- .../offchain-worker-price-oracle/src/tests.rs | 6 ++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index 5d198fce8a877..3b65408ffadf7 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -17,11 +17,10 @@ use crate as example_offchain_worker; use crate::*; -use frame_support::derive_impl; use pallet::config_preludes::*; use codec::Decode; use frame_support::{ - assert_ok, + assert_ok, derive_impl, traits::{ConstU32, ConstU64}, }; use sp_core::{ diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 8bb381f7cfd36..5bf535e3f7b06 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -253,8 +253,9 @@ pub mod pallet { // 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), + 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), @@ -270,10 +271,11 @@ pub mod pallet { match res { // The value has been set correctly, which means we can safely // send a transaction now. - Ok(_) => + 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(RECENTLY_SENT)) => { @@ -364,6 +366,21 @@ pub mod pallet { 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 { @@ -377,7 +394,7 @@ impl Pallet { 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. @@ -437,7 +454,7 @@ impl Pallet { // response. if response.code != 200 { log::warn!("Unexpected status code: {}", response.code); - return Err(http::Error::Unknown) + return Err(http::Error::Unknown); } // Next we want to fully read the response body and collect it to a diff --git a/frame/examples/offchain-worker-price-oracle/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs index 1639b40d8e951..8e9f0ba11d549 100644 --- a/frame/examples/offchain-worker-price-oracle/src/tests.rs +++ b/frame/examples/offchain-worker-price-oracle/src/tests.rs @@ -19,9 +19,10 @@ use crate as example_offchain_worker; use crate::*; use codec::Decode; use frame_support::{ - assert_noop, assert_ok, parameter_types, + assert_noop, assert_ok, derive_impl, parameter_types, traits::{ConstU32, ConstU64}, }; +use pallet::config_preludes::*; use sp_core::{ offchain::{testing, OffchainWorkerExt, TransactionPoolExt}, sr25519::Signature, @@ -111,12 +112,11 @@ 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 MaxPrices = ConstU32<64>; - type MaxAuthorities = ConstU32<64>; } fn test_pub() -> sp_core::sr25519::Public { From eef403f8e683a57765e25621f953bfc58ddc63ed Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Fri, 21 Jul 2023 10:30:48 +0200 Subject: [PATCH 56/72] fix issue with BlockNumber in offchain-worker examples --- .../offchain-worker-ping-pong/src/lib.rs | 28 +++++++++---------- .../offchain-worker-ping-pong/src/tests.rs | 7 +++-- .../offchain-worker-price-oracle/src/lib.rs | 6 ++-- .../offchain-worker-price-oracle/src/tests.rs | 23 ++++++--------- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index f4a2affc61d6b..90ffca0604e46 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -98,6 +98,7 @@ use frame_system::{ AppCrypto, CreateSignedTransaction, SendSignedTransaction, SendUnsignedTransaction, SignedPayload, Signer, SigningTypes, SubmitTransaction, }, + pallet_prelude::*, }; use sp_core::crypto::KeyTypeId; use sp_runtime::{ @@ -155,7 +156,6 @@ pub use pallet::*; pub mod pallet { use super::*; use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; /// This pallet's configuration trait #[pallet::config(with_default)] @@ -176,7 +176,7 @@ pub mod pallet { /// blocks. #[pallet::no_default] #[pallet::constant] - type UnsignedInterval: Get; + type UnsignedInterval: Get>; /// A configuration for base priority of unsigned transactions. /// @@ -245,7 +245,7 @@ pub mod pallet { /// storage entry defines when new transaction is going to be accepted. #[pallet::storage] #[pallet::getter(fn next_unsigned_at)] - pub(super) type NextUnsignedAt = StorageValue<_, T::BlockNumber, ValueQuery>; + pub(super) type NextUnsignedAt = StorageValue<_, BlockNumberFor, ValueQuery>; #[pallet::pallet] pub struct Pallet(_); @@ -261,7 +261,7 @@ pub mod pallet { /// 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: T::BlockNumber) { + 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 @@ -286,12 +286,12 @@ pub mod pallet { if let Err(e) = Self::ocw_pong_raw_unsigned(block_number) { log::error!("Error: {}", e); } - } else if unsigned_type == T::BlockNumber::from(1u32) { + } 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 == T::BlockNumber::from(2u32) { + } 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); @@ -307,7 +307,7 @@ pub mod pallet { } /// clean Pings - fn on_initialize(_: T::BlockNumber) -> Weight { + fn on_initialize(_: BlockNumberFor) -> Weight { Pings::::kill(); Weight::zero() } @@ -352,7 +352,7 @@ pub mod pallet { #[pallet::weight(0)] pub fn pong_unsigned( origin: OriginFor, - _block_number: T::BlockNumber, + _block_number: BlockNumberFor, nonce: u32, ) -> DispatchResultWithPostInfo { // This ensures that the function can only be called via unsigned @@ -376,7 +376,7 @@ pub mod pallet { #[pallet::weight(0)] pub fn pong_unsigned_with_signed_payload( origin: OriginFor, - pong_payload: PongPayload, + pong_payload: PongPayload>, _signature: T::Signature, ) -> DispatchResultWithPostInfo { // This ensures that the function can only be called via unsigned @@ -517,7 +517,7 @@ pub struct PongPayload { public: Public, } -impl SignedPayload for PongPayload { +impl SignedPayload for PongPayload> { fn public(&self) -> T::Public { self.public.clone() } @@ -528,7 +528,7 @@ impl Pallet { >::get().contains(who) } - fn validate_transaction_parameters(block_number: &T::BlockNumber) -> TransactionValidity { + 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 { @@ -603,7 +603,7 @@ impl Pallet { } /// A helper function to sign payload and send an unsigned pong transaction - fn ocw_pong_unsigned_for_any_account(block_number: T::BlockNumber) -> Result<(), &'static str> { + fn ocw_pong_unsigned_for_any_account(block_number: BlockNumberFor) -> Result<(), &'static str> { let pings = >::get(); for p in pings { let Ping(nonce) = p; @@ -626,7 +626,7 @@ impl Pallet { /// A helper function to sign payload and send an unsigned pong transaction fn ocw_pong_unsigned_for_all_accounts( - block_number: T::BlockNumber, + block_number: BlockNumberFor, ) -> Result<(), &'static str> { let pings = >::get(); for p in pings { @@ -652,7 +652,7 @@ impl Pallet { } /// A helper function to send a raw unsigned pong transaction. - fn ocw_pong_raw_unsigned(block_number: T::BlockNumber) -> Result<(), &'static str> { + fn ocw_pong_raw_unsigned(block_number: BlockNumberFor) -> Result<(), &'static str> { let pings = >::get(); for p in pings { let Ping(nonce) = p; diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index 7d20325112cf4..578c41b2608f4 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -36,6 +36,7 @@ use sp_runtime::{ traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, RuntimeAppPublic, }; +use frame_system::pallet_prelude::BlockNumberFor; type Block = frame_system::mocking::MockBlock; @@ -43,7 +44,7 @@ type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( pub enum Test { - System: frame_system::{Pallet, Call, Config, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, PingPongOcwExample: example_offchain_worker::{Pallet, Call, Storage, Event, ValidateUnsigned}, } ); @@ -229,7 +230,7 @@ fn should_submit_unsigned_transaction_on_chain_for_any_account() { let signature_valid = ::Public, - ::BlockNumber, + BlockNumberFor, > as SignedPayload>::verify::(&pong_payload, signature); assert!(signature_valid); @@ -283,7 +284,7 @@ fn should_submit_unsigned_transaction_on_chain_for_all_accounts() { let signature_valid = ::Public, - ::BlockNumber, + BlockNumberFor, > as SignedPayload>::verify::(&pong_payload, signature); assert!(signature_valid); diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 5bf535e3f7b06..bd20a74950027 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -150,7 +150,7 @@ pub mod pallet { /// sending between distinct runs of this offchain worker. #[pallet::no_default] #[pallet::constant] - type GracePeriod: Get; + type GracePeriod: Get>; /// Maximum number of prices. #[pallet::constant] @@ -209,7 +209,7 @@ pub mod pallet { /// handle that. /// /// You can use `Local Storage` API to coordinate runs of the worker. - fn offchain_worker(block_number: T::BlockNumber) { + 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 @@ -248,7 +248,7 @@ pub mod pallet { // 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>| { + 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 diff --git a/frame/examples/offchain-worker-price-oracle/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs index 8e9f0ba11d549..f6f0540c04fa2 100644 --- a/frame/examples/offchain-worker-price-oracle/src/tests.rs +++ b/frame/examples/offchain-worker-price-oracle/src/tests.rs @@ -20,7 +20,7 @@ use crate::*; use codec::Decode; use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, - traits::{ConstU32, ConstU64}, + traits::{ConstU64}, }; use pallet::config_preludes::*; use sp_core::{ @@ -31,22 +31,18 @@ use sp_core::{ use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; use sp_runtime::{ - testing::{Header, TestXt}, + testing::{TestXt}, traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, RuntimeAppPublic, }; -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; // For testing the module, we construct a mock runtime. frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, + pub enum Test { - System: frame_system::{Pallet, Call, Config, Storage, Event}, + System: frame_system::{Pallet, Call, Config, Storage, Event}, PriceOracleOcwExample: example_offchain_worker::{Pallet, Call, Storage, Event}, } ); @@ -57,14 +53,13 @@ impl frame_system::Config for Test { type BlockLength = (); type DbWeight = (); type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Index = u64; - type BlockNumber = u64; + type Nonce = u64; type Hash = H256; + type RuntimeCall = RuntimeCall; type Hashing = BlakeTwo256; type AccountId = sp_core::sr25519::Public; type Lookup = IdentityLookup; - type Header = Header; + type Block = Block; type RuntimeEvent = RuntimeEvent; type BlockHashCount = ConstU64<250>; type Version = (); @@ -75,7 +70,7 @@ impl frame_system::Config for Test { type SystemWeightInfo = (); type SS58Prefix = (); type OnSetCode = (); - type MaxConsumers = ConstU32<16>; + type MaxConsumers = frame_support::traits::ConstU32<16>; } type Extrinsic = TestXt; @@ -96,7 +91,7 @@ where impl frame_system::offchain::CreateSignedTransaction for Test where - RuntimeCall: From, + RuntimeCall: From { fn create_transaction>( call: RuntimeCall, From fea918cca2f3d3276bf67679f88e0b82628a8f3c Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Fri, 21 Jul 2023 11:34:34 +0200 Subject: [PATCH 57/72] cargo fmt --- .../offchain-worker-ping-pong/src/lib.rs | 4 +++- .../offchain-worker-ping-pong/src/tests.rs | 5 ++--- .../offchain-worker-price-oracle/src/lib.rs | 21 +++++++++---------- .../offchain-worker-price-oracle/src/tests.rs | 9 +++----- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 90ffca0604e46..b8d620a31a87a 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -603,7 +603,9 @@ impl Pallet { } /// 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> { + fn ocw_pong_unsigned_for_any_account( + block_number: BlockNumberFor, + ) -> Result<(), &'static str> { let pings = >::get(); for p in pings { let Ping(nonce) = p; diff --git a/frame/examples/offchain-worker-ping-pong/src/tests.rs b/frame/examples/offchain-worker-ping-pong/src/tests.rs index 578c41b2608f4..99e98cc1a0660 100644 --- a/frame/examples/offchain-worker-ping-pong/src/tests.rs +++ b/frame/examples/offchain-worker-ping-pong/src/tests.rs @@ -17,18 +17,19 @@ use crate as example_offchain_worker; use crate::*; -use pallet::config_preludes::*; use codec::Decode; use frame_support::{ 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::{ @@ -36,7 +37,6 @@ use sp_runtime::{ traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, RuntimeAppPublic, }; -use frame_system::pallet_prelude::BlockNumberFor; type Block = frame_system::mocking::MockBlock; @@ -105,7 +105,6 @@ where } } - #[derive_impl(TestDefaultConfig as pallet::DefaultConfig)] impl Config for Test { type RuntimeEvent = RuntimeEvent; diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index bd20a74950027..511e52a70fa47 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -247,20 +247,20 @@ pub mod pallet { // 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>| { + 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) - }, + 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 @@ -271,11 +271,10 @@ pub mod pallet { match res { // The value has been set correctly, which means we can safely // send a transaction now. - Ok(_) => { + 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(RECENTLY_SENT)) => { @@ -394,7 +393,7 @@ impl Pallet { 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. @@ -454,7 +453,7 @@ impl Pallet { // response. if response.code != 200 { log::warn!("Unexpected status code: {}", response.code); - return Err(http::Error::Unknown); + return Err(http::Error::Unknown) } // Next we want to fully read the response body and collect it to a diff --git a/frame/examples/offchain-worker-price-oracle/src/tests.rs b/frame/examples/offchain-worker-price-oracle/src/tests.rs index f6f0540c04fa2..65c047bd2c176 100644 --- a/frame/examples/offchain-worker-price-oracle/src/tests.rs +++ b/frame/examples/offchain-worker-price-oracle/src/tests.rs @@ -18,10 +18,7 @@ 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 frame_support::{assert_noop, assert_ok, derive_impl, parameter_types, traits::ConstU64}; use pallet::config_preludes::*; use sp_core::{ offchain::{testing, OffchainWorkerExt, TransactionPoolExt}, @@ -31,7 +28,7 @@ use sp_core::{ use sp_keystore::{testing::MemoryKeystore, Keystore, KeystoreExt}; use sp_runtime::{ - testing::{TestXt}, + testing::TestXt, traits::{BlakeTwo256, Extrinsic as ExtrinsicT, IdentifyAccount, IdentityLookup, Verify}, RuntimeAppPublic, }; @@ -91,7 +88,7 @@ where impl frame_system::offchain::CreateSignedTransaction for Test where - RuntimeCall: From + RuntimeCall: From, { fn create_transaction>( call: RuntimeCall, From ecf6135a9e05e618843fa2c192b0a9ca8ff32824 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 15:00:03 -0400 Subject: [PATCH 58/72] Update frame/examples/offchain-worker-ping-pong/Cargo.toml Co-authored-by: Oliver Tale-Yazdi --- frame/examples/offchain-worker-ping-pong/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/Cargo.toml b/frame/examples/offchain-worker-ping-pong/Cargo.toml index a3428c3e9f854..efd79f1642174 100644 --- a/frame/examples/offchain-worker-ping-pong/Cargo.toml +++ b/frame/examples/offchain-worker-ping-pong/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-example-offchain-worker-ping-pong" -version = "4.0.0-dev" +version = "1.0.0" authors = ["Parity Technologies "] edition = "2021" license = "MIT-0" From c70033994a645c7748bab80b39b457054a3684c6 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 14:04:07 -0500 Subject: [PATCH 59/72] start at 1.0.0 --- frame/examples/offchain-worker-price-oracle/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-price-oracle/Cargo.toml b/frame/examples/offchain-worker-price-oracle/Cargo.toml index 8d2fc8b6c3e4b..215ce8b41546e 100644 --- a/frame/examples/offchain-worker-price-oracle/Cargo.toml +++ b/frame/examples/offchain-worker-price-oracle/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-example-offchain-worker-price-oracle" -version = "4.0.0-dev" +version = "1.0.0" authors = ["Parity Technologies "] edition = "2021" license = "MIT-0" From 1b837484888a7416b6e5b59c3f53ee865a174176 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 15:11:45 -0400 Subject: [PATCH 60/72] Update frame/examples/offchain-worker-ping-pong/src/lib.rs Co-authored-by: Oliver Tale-Yazdi --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index b8d620a31a87a..786f6ebbaf8a0 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -317,7 +317,6 @@ pub mod pallet { #[pallet::call] impl Pallet { #[pallet::call_index(0)] - #[pallet::weight(0)] pub fn ping(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { let _who = ensure_signed(origin)?; From 089f95e50b927d6295b17ed4c3c0d9534442bd84 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 14:14:03 -0500 Subject: [PATCH 61/72] remove weight 0, no need as pallet dev mode does this automatically --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 5 ----- frame/examples/offchain-worker-price-oracle/src/lib.rs | 3 --- 2 files changed, 8 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 786f6ebbaf8a0..7b38b7b28a71a 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -334,7 +334,6 @@ pub mod pallet { } #[pallet::call_index(1)] - #[pallet::weight(0)] pub fn pong_signed(origin: OriginFor, nonce: u32) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; @@ -348,7 +347,6 @@ pub mod pallet { } #[pallet::call_index(2)] - #[pallet::weight(0)] pub fn pong_unsigned( origin: OriginFor, _block_number: BlockNumberFor, @@ -372,7 +370,6 @@ pub mod pallet { } #[pallet::call_index(3)] - #[pallet::weight(0)] pub fn pong_unsigned_with_signed_payload( origin: OriginFor, pong_payload: PongPayload>, @@ -395,7 +392,6 @@ pub mod pallet { } #[pallet::call_index(4)] - #[pallet::weight(0)] pub fn add_authority( origin: OriginFor, authority: T::AccountId, @@ -418,7 +414,6 @@ pub mod pallet { } #[pallet::call_index(5)] - #[pallet::weight(0)] pub fn remove_authority( origin: OriginFor, authority: T::AccountId, diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 511e52a70fa47..add6aa8bebf0a 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -305,7 +305,6 @@ pub mod pallet { /// /// This only works if the caller is in `Authorities`. #[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)?; @@ -320,7 +319,6 @@ pub mod pallet { } #[pallet::call_index(1)] - #[pallet::weight(0)] pub fn add_authority( origin: OriginFor, authority: T::AccountId, @@ -343,7 +341,6 @@ pub mod pallet { } #[pallet::call_index(2)] - #[pallet::weight(0)] pub fn remove_authority( origin: OriginFor, authority: T::AccountId, From 6537cd9c3ded3c025d26db84fbdebe3dc2571420 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 15:32:12 -0400 Subject: [PATCH 62/72] Update frame/examples/offchain-worker-ping-pong/Cargo.toml Co-authored-by: Oliver Tale-Yazdi --- frame/examples/offchain-worker-ping-pong/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/frame/examples/offchain-worker-ping-pong/Cargo.toml b/frame/examples/offchain-worker-ping-pong/Cargo.toml index efd79f1642174..ffc7b7e808858 100644 --- a/frame/examples/offchain-worker-ping-pong/Cargo.toml +++ b/frame/examples/offchain-worker-ping-pong/Cargo.toml @@ -8,6 +8,7 @@ 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"] From b034436ed9f98c6405f4817aedb1e31ff7576069 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 15:32:21 -0400 Subject: [PATCH 63/72] Update frame/examples/offchain-worker-ping-pong/src/lib.rs Co-authored-by: Oliver Tale-Yazdi --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 7b38b7b28a71a..d7164b7789142 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -537,7 +537,7 @@ impl Pallet { 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().saturating_add(0)) + .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. From 3e604e0f4d8272c4f4459db081795c0df2af2407 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 15:32:43 -0400 Subject: [PATCH 64/72] Update frame/examples/offchain-worker-price-oracle/Cargo.toml Co-authored-by: Oliver Tale-Yazdi --- frame/examples/offchain-worker-price-oracle/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/frame/examples/offchain-worker-price-oracle/Cargo.toml b/frame/examples/offchain-worker-price-oracle/Cargo.toml index 215ce8b41546e..6060dce2630ea 100644 --- a/frame/examples/offchain-worker-price-oracle/Cargo.toml +++ b/frame/examples/offchain-worker-price-oracle/Cargo.toml @@ -8,6 +8,7 @@ homepage = "https://substrate.io" repository = "https://github.com/paritytech/substrate/" description = "FRAME example pallet for offchain worker (price oracle)" readme = "README.md" +publish = false [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] From a08af900907f27bcad68479b59c7d4c8f400ea55 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 14:36:56 -0500 Subject: [PATCH 65/72] add doc comments for Authorities storage --- frame/examples/offchain-worker-price-oracle/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index add6aa8bebf0a..288520ea2a00f 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -180,6 +180,7 @@ pub mod pallet { #[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 = From f96dacdf4c980349d7dee37d5be9c9c133e02538 Mon Sep 17 00:00:00 2001 From: Bruno Galvao Date: Fri, 28 Jul 2023 14:58:26 -0500 Subject: [PATCH 66/72] re-wrap to 100 line width --- .../offchain-worker-ping-pong/src/lib.rs | 258 ++++++++--------- .../offchain-worker-price-oracle/src/lib.rs | 259 ++++++++---------- 2 files changed, 236 insertions(+), 281 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index d7164b7789142..d39311b066cf4 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -1,50 +1,46 @@ // This file is part of Substrate. -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 +// 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 +// 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 +// 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. +// 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. +//! 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. +//! 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.** +//! **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. +//! 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. +//! `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). +//! 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. +//! 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 @@ -52,22 +48,20 @@ //! - `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**. +//! 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: +//! 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!** ⚠️ +//! ⚠️ **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`: +//! 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' \ @@ -75,18 +69,21 @@ //! --data-raw '{ //! "jsonrpc": "2.0", //! "method": "author_insertKey", -//! "params": ["pong","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], +//! "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`). +//! (`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. +//! 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)] @@ -112,16 +109,15 @@ 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`). +/// 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. +/// 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; @@ -180,8 +176,8 @@ pub mod pallet { /// 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. + /// This is exposed so that it can be tuned for particular runtime, when multiple pallets + /// send unsigned transactions. #[pallet::constant] type UnsignedPriority: Get; @@ -240,9 +236,9 @@ pub mod pallet { /// 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. + /// 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>; @@ -254,26 +250,23 @@ pub mod pallet { 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. + /// 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. + // 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. + // 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. + // 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); @@ -299,8 +292,8 @@ pub mod pallet { } } - // try to send a pong_signed (node needs to be loaded with keys, - // account needs to be funded) + // 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); } @@ -352,8 +345,7 @@ pub mod pallet { _block_number: BlockNumberFor, nonce: u32, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned - // transaction. + // This ensures that the function can only be called via unsigned transaction. ensure_none(origin)?; // Emit the PongAckUnauthenticated event @@ -362,8 +354,7 @@ pub mod pallet { unsigned_type: UnsignedType::RawUnsigned, }); - // now increment the block number at which we expect next unsigned - // transaction. + // 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()) @@ -375,8 +366,7 @@ pub mod pallet { pong_payload: PongPayload>, _signature: T::Signature, ) -> DispatchResultWithPostInfo { - // This ensures that the function can only be called via unsigned - // transaction. + // This ensures that the function can only be called via unsigned transaction. ensure_none(origin)?; Self::deposit_event(Event::PongAckUnauthenticated { @@ -384,8 +374,7 @@ pub mod pallet { unsigned_type: UnsignedType::UnsignedWithSignedPayload, }); - // now increment the block number at which we expect next unsigned - // transaction. + // 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()) @@ -442,21 +431,17 @@ pub mod pallet { /// 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. + /// 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! + /// ⚠ 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**! + /// 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. + /// 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 { @@ -466,10 +451,9 @@ pub mod pallet { 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) + // ⚠ 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 { @@ -502,8 +486,8 @@ pub mod pallet { } } -/// Payload used by this example crate to hold pong response data required to -/// submit an unsigned transaction. +/// 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, @@ -535,26 +519,23 @@ impl Pallet { } ValidTransaction::with_tag_prefix("ExampleOffchainWorker") - // We set base priority to 2**20 and hope it's included before any - // other transactions in the pool. + // 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. + // 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. + // 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 + // 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() @@ -573,15 +554,14 @@ impl Pallet { 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`. + // 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. + // 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 } }); @@ -652,20 +632,18 @@ impl Pallet { let pings = >::get(); for p in pings { let Ping(nonce) = p; - // 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. + // 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) + // 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. + // 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.")?; } diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index 288520ea2a00f..ef36e1cbb5364 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -1,48 +1,41 @@ // This file is part of Substrate. -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 +// 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 +// 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 +// 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. +// 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. +//! 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. +//! 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.** +//! **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. +//! 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. +//! 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. +//! 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: +//! Here's an example of how a node admin can inject some keys into the keystore: //! //! ```bash //! $ curl --location --request POST 'http://localhost:9944' \ @@ -50,18 +43,21 @@ //! --data-raw '{ //! "jsonrpc": "2.0", //! "method": "author_insertKey", -//! "params": ["btc!","bread tongue spell stadium clean grief coin rent spend total practice document","0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"], +//! "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`). +//! (`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. +//! 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)] @@ -84,18 +80,17 @@ 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. +/// 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`). +/// 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. +/// 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; @@ -145,9 +140,9 @@ pub mod pallet { /// 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. + /// 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>; @@ -200,93 +195,84 @@ pub mod pallet { 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. + /// 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. + /// 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. + // 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. + // 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. + // 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. + // 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); - /// A friendlier name for the error that is going to be returned in - /// case we are in the grace period. + /// 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. + // 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. + // 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. + // 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. + // 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. + // 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. + // 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. + // We are in the grace period, we should not send a transaction this time. Err(MutateStorageError::ValueFunctionFailed(RECENTLY_SENT)) => { 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. + // 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.") }, @@ -299,10 +285,10 @@ pub mod pallet { 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 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)] @@ -393,19 +379,17 @@ impl Pallet { "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. + // 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`. + // 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. + // 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 } }); @@ -421,42 +405,36 @@ impl Pallet { /// 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. + // 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. + // 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. + // 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. + // 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. + // 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. + // 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. @@ -480,8 +458,7 @@ impl Pallet { /// 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. + /// 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()? { From 99ff6572f5bb0e3429d0fa028e3ff6c50a4050fe Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Fri, 11 Aug 2023 16:16:53 -0300 Subject: [PATCH 67/72] remove RECENTLY_SENT alias --- frame/examples/offchain-worker-price-oracle/src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index ef36e1cbb5364..bb9bf0b2999e5 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -224,10 +224,6 @@ pub mod pallet { let average: Option = Self::average_price(); log::debug!("Current price: {:?}", average); - /// 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. @@ -244,7 +240,7 @@ pub mod pallet { // 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), + Err(()), // In every other case we attempt to acquire the lock and send a // transaction. _ => Ok(block_number), @@ -265,7 +261,7 @@ pub mod pallet { log::error!("Error: {}", e); }, // We are in the grace period, we should not send a transaction this time. - Err(MutateStorageError::ValueFunctionFailed(RECENTLY_SENT)) => { + 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 From c19023d429d9cc24c151ac82f5af630d532f061d Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Fri, 11 Aug 2023 16:19:45 -0300 Subject: [PATCH 68/72] fmt --- .../offchain-worker-ping-pong/src/lib.rs | 14 +++++++------- .../offchain-worker-price-oracle/src/lib.rs | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index d39311b066cf4..369279e16485a 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -70,8 +70,8 @@ //! "jsonrpc": "2.0", //! "method": "author_insertKey", //! "params": [ -//! "pong", -//! "bread tongue spell stadium clean grief coin rent spend total practice document", +//! "pong", +//! "bread tongue spell stadium clean grief coin rent spend total practice document", //! "0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30" //! ], //! "id": 1 @@ -457,7 +457,7 @@ pub mod pallet { let signature_valid = SignedPayload::::verify::(payload, signature.clone()); if !signature_valid { - return InvalidTransaction::BadProof.into() + return InvalidTransaction::BadProof.into(); } Self::validate_transaction_parameters(&payload.block_number) } else if let Call::pong_unsigned { block_number, nonce: _n } = call { @@ -510,12 +510,12 @@ impl Pallet { // 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() + 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() + return InvalidTransaction::Future.into(); } ValidTransaction::with_tag_prefix("ExampleOffchainWorker") @@ -547,7 +547,7 @@ impl Pallet { if !signer.can_sign() { return Err( "No local accounts available. Consider adding one via `author_insertKey` RPC.", - ) + ); } let pings = >::get(); @@ -619,7 +619,7 @@ impl Pallet { ); for (_account_id, result) in transaction_results.into_iter() { if result.is_err() { - return Err("Unable to submit transaction") + return Err("Unable to submit transaction"); } } } diff --git a/frame/examples/offchain-worker-price-oracle/src/lib.rs b/frame/examples/offchain-worker-price-oracle/src/lib.rs index bb9bf0b2999e5..90425237ebd9c 100644 --- a/frame/examples/offchain-worker-price-oracle/src/lib.rs +++ b/frame/examples/offchain-worker-price-oracle/src/lib.rs @@ -44,8 +44,8 @@ //! "jsonrpc": "2.0", //! "method": "author_insertKey", //! "params": [ -//! "btc!", -//! "bread tongue spell stadium clean grief coin rent spend total practice document", +//! "btc!", +//! "bread tongue spell stadium clean grief coin rent spend total practice document", //! "0xb6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30" //! ], //! "id": 1 @@ -239,8 +239,7 @@ pub mod pallet { 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(()), + 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), @@ -256,10 +255,11 @@ pub mod pallet { match res { // The value has been set correctly, which means we can safely send a transaction // now. - Ok(_) => + 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.") @@ -373,7 +373,7 @@ impl Pallet { 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. @@ -425,7 +425,7 @@ impl Pallet { // 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) + return Err(http::Error::Unknown); } // Next we want to fully read the response body and collect it to a vector of bytes. Note From 63b1029df0599bef5ba463ead38b690470317fd2 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Fri, 11 Aug 2023 16:49:54 -0300 Subject: [PATCH 69/72] enforce error propagation in ocw_pong_signed --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 369279e16485a..4a7cf42863563 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -568,7 +568,10 @@ impl Pallet { 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), + Err(e) => { + log::error!("[{:?}] Failed to submit transaction: {:?}", acc.id, e); + return Err("Failed to submit transaction"); + }, } } } From c2368f44f6f98aaa33dfa3c7db3f36e2159faac1 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Fri, 11 Aug 2023 16:59:52 -0300 Subject: [PATCH 70/72] fix comment about ValidateUnsigned trait --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 4a7cf42863563..b4cdcd3f8d15c 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -644,9 +644,9 @@ impl Pallet { // 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. + // writing our implementation for the `ValidateUnsigned` trait. 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.")?; } From 5ec0432f479fb1c5b49476e439642923cb0c20d7 Mon Sep 17 00:00:00 2001 From: "Bernardo A. Rodrigues" Date: Fri, 11 Aug 2023 17:01:12 -0300 Subject: [PATCH 71/72] fix typo --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index b4cdcd3f8d15c..07b084e137d32 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -645,7 +645,7 @@ impl Pallet { // // 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 carefuly implement unsigned validation logic, as any mistakes can lead + // 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.")?; From 4abbf7f64222f0a47331624c0d0042d888cf6b10 Mon Sep 17 00:00:00 2001 From: bernardo Date: Fri, 11 Aug 2023 17:10:05 -0300 Subject: [PATCH 72/72] simplify syntax on for loop Co-authored-by: Oliver Tale-Yazdi --- frame/examples/offchain-worker-ping-pong/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frame/examples/offchain-worker-ping-pong/src/lib.rs b/frame/examples/offchain-worker-ping-pong/src/lib.rs index 07b084e137d32..b9664dded852d 100644 --- a/frame/examples/offchain-worker-ping-pong/src/lib.rs +++ b/frame/examples/offchain-worker-ping-pong/src/lib.rs @@ -633,8 +633,7 @@ impl Pallet { /// 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 p in pings { - let Ping(nonce) = p; + 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.