diff --git a/bin/node/executor/tests/common.rs b/bin/node/executor/tests/common.rs
index 6b6ef272f8a35..5a51e4312c5e8 100644
--- a/bin/node/executor/tests/common.rs
+++ b/bin/node/executor/tests/common.rs
@@ -15,10 +15,21 @@
// along with Substrate. If not, see .
use codec::{Encode, Decode};
+use frame_system::offchain::AppCrypto;
use frame_support::Hashable;
use sp_state_machine::TestExternalities as CoreTestExternalities;
-use sp_core::{NeverNativeValue, NativeOrEncoded, traits::{CodeExecutor, RuntimeCode}};
-use sp_runtime::{ApplyExtrinsicResult, traits::{Header as HeaderT, BlakeTwo256}};
+use sp_core::{
+ NeverNativeValue, NativeOrEncoded,
+ crypto::KeyTypeId,
+ sr25519::Signature,
+ traits::{CodeExecutor, RuntimeCode},
+};
+use sp_runtime::{
+ ApplyExtrinsicResult,
+ MultiSigner,
+ MultiSignature,
+ traits::{Header as HeaderT, BlakeTwo256},
+};
use sc_executor::{NativeExecutor, WasmExecutionMethod};
use sc_executor::error::Result;
@@ -31,6 +42,25 @@ use node_primitives::{Hash, BlockNumber};
use node_testing::keyring::*;
use sp_externalities::Externalities;
+pub const TEST_KEY_TYPE_ID: KeyTypeId = KeyTypeId(*b"test");
+
+pub mod sr25519 {
+ mod app_sr25519 {
+ use sp_application_crypto::{app_crypto, sr25519};
+ use super::super::TEST_KEY_TYPE_ID;
+ app_crypto!(sr25519, TEST_KEY_TYPE_ID);
+ }
+
+ pub type AuthorityId = app_sr25519::Public;
+}
+
+pub struct TestAuthorityId;
+impl AppCrypto for TestAuthorityId {
+ type RuntimeAppPublic = sr25519::AuthorityId;
+ type GenericSignature = Signature;
+ type GenericPublic = sp_core::sr25519::Public;
+}
+
/// The wasm runtime code.
///
/// `compact` since it is after post-processing with wasm-gc which performs tree-shaking thus
diff --git a/bin/node/executor/tests/submit_transaction.rs b/bin/node/executor/tests/submit_transaction.rs
index d92f3e3202f6e..3a41c3483c280 100644
--- a/bin/node/executor/tests/submit_transaction.rs
+++ b/bin/node/executor/tests/submit_transaction.rs
@@ -15,24 +15,29 @@
// along with Substrate. If not, see .
use node_runtime::{
- Call, Executive, Indices, Runtime, TransactionSubmitterOf, UncheckedExtrinsic,
+ Executive, Indices, Runtime, UncheckedExtrinsic,
};
use sp_application_crypto::AppKey;
use sp_core::testing::KeyStore;
-use sp_core::traits::KeystoreExt;
-use sp_core::offchain::{
- TransactionPoolExt,
- testing::TestTransactionPoolExt,
+use sp_core::{
+ offchain::{
+ TransactionPoolExt,
+ testing::TestTransactionPoolExt,
+ },
+ traits::KeystoreExt,
+};
+use frame_system::{
+ offchain::{
+ Signer,
+ SubmitTransaction,
+ SendSignedTransaction,
+ }
};
-use frame_system::offchain::{SubmitSignedTransaction, SubmitUnsignedTransaction};
-use pallet_im_online::sr25519::AuthorityPair as Key;
use codec::Decode;
pub mod common;
use self::common::*;
-type SubmitTransaction = TransactionSubmitterOf;
-
#[test]
fn should_submit_unsigned_transaction() {
let mut t = new_test_ext(COMPACT_CODE, false);
@@ -49,8 +54,7 @@ fn should_submit_unsigned_transaction() {
};
let call = pallet_im_online::Call::heartbeat(heartbeat_data, signature);
- >
- ::submit_unsigned(call)
+ SubmitTransaction::>::submit_unsigned_transaction(call.into())
.unwrap();
assert_eq!(state.read().transactions.len(), 1)
@@ -66,23 +70,16 @@ fn should_submit_signed_transaction() {
t.register_extension(TransactionPoolExt::new(pool));
let keystore = KeyStore::new();
- keystore.write().sr25519_generate_new(Key::ID, Some(&format!("{}/hunter1", PHRASE))).unwrap();
- keystore.write().sr25519_generate_new(Key::ID, Some(&format!("{}/hunter2", PHRASE))).unwrap();
- keystore.write().sr25519_generate_new(Key::ID, Some(&format!("{}/hunter3", PHRASE))).unwrap();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter1", PHRASE))).unwrap();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter2", PHRASE))).unwrap();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter3", PHRASE))).unwrap();
t.register_extension(KeystoreExt(keystore));
t.execute_with(|| {
- let keys = >
- ::find_all_local_keys();
- assert_eq!(keys.len(), 3, "Missing keys: {:?}", keys);
-
- let can_sign = >
- ::can_sign();
- assert!(can_sign, "Since there are keys, `can_sign` should return true");
-
- let call = pallet_balances::Call::transfer(Default::default(), Default::default());
- let results =
- >::submit_signed(call);
+ let results = Signer::::all_accounts()
+ .send_signed_transaction(|_| {
+ pallet_balances::Call::transfer(Default::default(), Default::default())
+ });
let len = results.len();
assert_eq!(len, 3);
@@ -98,27 +95,26 @@ fn should_submit_signed_twice_from_the_same_account() {
t.register_extension(TransactionPoolExt::new(pool));
let keystore = KeyStore::new();
- keystore.write().sr25519_generate_new(Key::ID, Some(&format!("{}/hunter1", PHRASE))).unwrap();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter1", PHRASE))).unwrap();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter2", PHRASE))).unwrap();
t.register_extension(KeystoreExt(keystore));
t.execute_with(|| {
- let call = pallet_balances::Call::transfer(Default::default(), Default::default());
- let results =
- >::submit_signed(call);
+ let result = Signer::::any_account()
+ .send_signed_transaction(|_| {
+ pallet_balances::Call::transfer(Default::default(), Default::default())
+ });
- let len = results.len();
- assert_eq!(len, 1);
- assert_eq!(results.into_iter().filter_map(|x| x.1.ok()).count(), len);
+ assert!(result.is_some());
assert_eq!(state.read().transactions.len(), 1);
// submit another one from the same account. The nonce should be incremented.
- let call = pallet_balances::Call::transfer(Default::default(), Default::default());
- let results =
- >::submit_signed(call);
+ let result = Signer::::any_account()
+ .send_signed_transaction(|_| {
+ pallet_balances::Call::transfer(Default::default(), Default::default())
+ });
- let len = results.len();
- assert_eq!(len, 1);
- assert_eq!(results.into_iter().filter_map(|x| x.1.ok()).count(), len);
+ assert!(result.is_some());
assert_eq!(state.read().transactions.len(), 2);
// now check that the transaction nonces are not equal
@@ -136,6 +132,60 @@ fn should_submit_signed_twice_from_the_same_account() {
});
}
+#[test]
+fn should_submit_signed_twice_from_all_accounts() {
+ let mut t = new_test_ext(COMPACT_CODE, false);
+ let (pool, state) = TestTransactionPoolExt::new();
+ t.register_extension(TransactionPoolExt::new(pool));
+
+ let keystore = KeyStore::new();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter1", PHRASE))).unwrap();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter2", PHRASE))).unwrap();
+ t.register_extension(KeystoreExt(keystore));
+
+ t.execute_with(|| {
+ let results = Signer::::all_accounts()
+ .send_signed_transaction(|_| {
+ pallet_balances::Call::transfer(Default::default(), Default::default())
+ });
+
+ let len = results.len();
+ assert_eq!(len, 2);
+ assert_eq!(results.into_iter().filter_map(|x| x.1.ok()).count(), len);
+ assert_eq!(state.read().transactions.len(), 2);
+
+ // submit another one from the same account. The nonce should be incremented.
+ let results = Signer::::all_accounts()
+ .send_signed_transaction(|_| {
+ pallet_balances::Call::transfer(Default::default(), Default::default())
+ });
+
+ let len = results.len();
+ assert_eq!(len, 2);
+ assert_eq!(results.into_iter().filter_map(|x| x.1.ok()).count(), len);
+ assert_eq!(state.read().transactions.len(), 4);
+
+ // now check that the transaction nonces are not equal
+ let s = state.read();
+ fn nonce(tx: UncheckedExtrinsic) -> frame_system::CheckNonce {
+ let extra = tx.signature.unwrap().2;
+ extra.3
+ }
+ let nonce1 = nonce(UncheckedExtrinsic::decode(&mut &*s.transactions[0]).unwrap());
+ let nonce2 = nonce(UncheckedExtrinsic::decode(&mut &*s.transactions[1]).unwrap());
+ let nonce3 = nonce(UncheckedExtrinsic::decode(&mut &*s.transactions[2]).unwrap());
+ let nonce4 = nonce(UncheckedExtrinsic::decode(&mut &*s.transactions[3]).unwrap());
+ assert!(
+ nonce1 != nonce3,
+ "Transactions should have different nonces. Got: 1st tx nonce: {:?}, 2nd nonce: {:?}", nonce1, nonce3
+ );
+ assert!(
+ nonce2 != nonce4,
+ "Transactions should have different nonces. Got: 1st tx nonce: {:?}, 2nd tx nonce: {:?}", nonce2, nonce4
+ );
+ });
+}
+
#[test]
fn submitted_transaction_should_be_valid() {
use codec::Encode;
@@ -148,13 +198,14 @@ fn submitted_transaction_should_be_valid() {
t.register_extension(TransactionPoolExt::new(pool));
let keystore = KeyStore::new();
- keystore.write().sr25519_generate_new(Key::ID, Some(&format!("{}/hunter1", PHRASE))).unwrap();
+ keystore.write().sr25519_generate_new(sr25519::AuthorityId::ID, Some(&format!("{}/hunter1", PHRASE))).unwrap();
t.register_extension(KeystoreExt(keystore));
t.execute_with(|| {
- let call = pallet_balances::Call::transfer(Default::default(), Default::default());
- let results =
- >::submit_signed(call);
+ let results = Signer::::all_accounts()
+ .send_signed_transaction(|_| {
+ pallet_balances::Call::transfer(Default::default(), Default::default())
+ });
let len = results.len();
assert_eq!(len, 1);
assert_eq!(results.into_iter().filter_map(|x| x.1.ok()).count(), len);
diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs
index 337242f884ddc..b1797fffb3321 100644
--- a/bin/node/runtime/src/lib.rs
+++ b/bin/node/runtime/src/lib.rs
@@ -46,11 +46,10 @@ use sp_version::NativeVersion;
use sp_core::OpaqueMetadata;
use pallet_grandpa::AuthorityList as GrandpaAuthorityList;
use pallet_grandpa::fg_primitives;
-use pallet_im_online::sr25519::{AuthorityId as ImOnlineId};
+use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId;
use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo;
use pallet_contracts_rpc_runtime_api::ContractExecResult;
-use frame_system::offchain::TransactionSubmitter;
use sp_inherents::{InherentData, CheckInherentsResult};
#[cfg(any(feature = "std", test))]
@@ -60,6 +59,7 @@ pub use pallet_balances::Call as BalancesCall;
pub use pallet_contracts::Gas;
pub use frame_support::StorageValue;
pub use pallet_staking::StakerStatus;
+use codec::Encode;
/// Implementations of some helper traits passed into runtime modules as associated types.
pub mod impls;
@@ -73,50 +73,6 @@ use constants::{time::*, currency::*};
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
-/// A transaction submitter with the given key type.
-pub type TransactionSubmitterOf = TransactionSubmitter;
-
-/// Submits transaction with the node's public and signature type. Adheres to the signed extension
-/// format of the chain.
-impl frame_system::offchain::CreateTransaction for Runtime {
- type Public = ::Signer;
- type Signature = Signature;
-
- fn create_transaction>(
- call: Call,
- public: Self::Public,
- account: AccountId,
- index: Index,
- ) -> Option<(Call, ::SignaturePayload)> {
- // take the biggest period possible.
- let period = BlockHashCount::get()
- .checked_next_power_of_two()
- .map(|c| c / 2)
- .unwrap_or(2) as u64;
- let current_block = System::block_number()
- .saturated_into::()
- // The `System::block_number` is initialized with `n+1`,
- // so the actual block number is `n`.
- .saturating_sub(1);
- let tip = 0;
- let extra: SignedExtra = (
- frame_system::CheckVersion::::new(),
- frame_system::CheckGenesis::::new(),
- frame_system::CheckEra::::from(generic::Era::mortal(period, current_block)),
- frame_system::CheckNonce::::from(index),
- frame_system::CheckWeight::::new(),
- pallet_transaction_payment::ChargeTransactionPayment::::from(tip),
- Default::default(),
- );
- let raw_payload = SignedPayload::new(call, extra).map_err(|e| {
- debug::warn!("Unable to create signed payload: {:?}", e);
- }).ok()?;
- let signature = TSigner::sign(public, &raw_payload)?;
- let address = Indices::unlookup(account);
- let (call, extra, _) = raw_payload.deconstruct();
- Some((call, (address, signature, extra)))
- }
-}
/// Runtime version.
pub const VERSION: RuntimeVersion = RuntimeVersion {
@@ -127,7 +83,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// and set impl_version to 0. If only runtime
// implementation changes and behavior does not, then leave spec_version as
// is and increment impl_version.
- spec_version: 243,
+ spec_version: 244,
impl_version: 0,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
@@ -362,7 +318,6 @@ impl pallet_staking::Trait for Runtime {
type NextNewSession = Session;
type ElectionLookahead = ElectionLookahead;
type Call = Call;
- type SubmitTransaction = TransactionSubmitterOf<()>;
type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator;
type UnsignedPriority = StakingUnsignedPriority;
}
@@ -549,11 +504,63 @@ parameter_types! {
pub const StakingUnsignedPriority: TransactionPriority = TransactionPriority::max_value() / 2;
}
+
+impl frame_system::offchain::CreateSignedTransaction for Runtime where
+ Call: From,
+{
+ fn create_transaction>(
+ call: Call,
+ public: ::Signer,
+ account: AccountId,
+ nonce: Index,
+ ) -> Option<(Call, ::SignaturePayload)> {
+ // take the biggest period possible.
+ let period = BlockHashCount::get()
+ .checked_next_power_of_two()
+ .map(|c| c / 2)
+ .unwrap_or(2) as u64;
+ let current_block = System::block_number()
+ .saturated_into::()
+ // The `System::block_number` is initialized with `n+1`,
+ // so the actual block number is `n`.
+ .saturating_sub(1);
+ let tip = 0;
+ let extra: SignedExtra = (
+ frame_system::CheckVersion::::new(),
+ frame_system::CheckGenesis::::new(),
+ frame_system::CheckEra::::from(generic::Era::mortal(period, current_block)),
+ frame_system::CheckNonce::::from(nonce),
+ frame_system::CheckWeight::::new(),
+ pallet_transaction_payment::ChargeTransactionPayment::::from(tip),
+ Default::default(),
+ );
+ let raw_payload = SignedPayload::new(call, extra).map_err(|e| {
+ debug::warn!("Unable to create signed payload: {:?}", e);
+ }).ok()?;
+ let signature = raw_payload.using_encoded(|payload| {
+ C::sign(payload, public)
+ })?;
+ let address = Indices::unlookup(account);
+ let (call, extra, _) = raw_payload.deconstruct();
+ Some((call, (address, signature.into(), extra)))
+ }
+}
+
+impl frame_system::offchain::SigningTypes for Runtime {
+ type Public = ::Signer;
+ type Signature = Signature;
+}
+
+impl frame_system::offchain::SendTransactionTypes for Runtime where
+ Call: From,
+{
+ type OverarchingCall = Call;
+ type Extrinsic = UncheckedExtrinsic;
+}
+
impl pallet_im_online::Trait for Runtime {
type AuthorityId = ImOnlineId;
type Event = Event;
- type Call = Call;
- type SubmitTransaction = TransactionSubmitterOf;
type SessionDuration = SessionDuration;
type ReportUnresponsiveness = Offences;
type UnsignedPriority = ImOnlineUnsignedPriority;
@@ -924,28 +931,14 @@ impl_runtime_apis! {
#[cfg(test)]
mod tests {
use super::*;
- use frame_system::offchain::{SignAndSubmitTransaction, SubmitSignedTransaction};
+ use frame_system::offchain::CreateSignedTransaction;
#[test]
fn validate_transaction_submitter_bounds() {
fn is_submit_signed_transaction() where
- T: SubmitSignedTransaction<
- Runtime,
- Call,
- >,
- {}
-
- fn is_sign_and_submit_transaction() where
- T: SignAndSubmitTransaction<
- Runtime,
- Call,
- Extrinsic=UncheckedExtrinsic,
- CreateTransaction=Runtime,
- Signer=ImOnlineId,
- >,
+ T: CreateSignedTransaction,
{}
- is_submit_signed_transaction::>();
- is_sign_and_submit_transaction::>();
+ is_submit_signed_transaction::();
}
}
diff --git a/frame/example-offchain-worker/src/lib.rs b/frame/example-offchain-worker/src/lib.rs
index 29a4859c78e21..d2ebd1159e228 100644
--- a/frame/example-offchain-worker/src/lib.rs
+++ b/frame/example-offchain-worker/src/lib.rs
@@ -22,12 +22,12 @@
//! Run `cargo doc --package pallet-example-offchain-worker --open` to view this module's
//! documentation.
//!
-//! - \[`pallet_example_offchain_worker::Trait`](./trait.Trait.html)
-//! - \[`Call`](./enum.Call.html)
-//! - \[`Module`](./struct.Module.html)
+//! - [`pallet_example_offchain_worker::Trait`](./trait.Trait.html)
+//! - [`Call`](./enum.Call.html)
+//! - [`Module`](./struct.Module.html)
//!
//!
-//! \## Overview
+//! ## Overview
//!
//! In this example we are going to build a very simplistic, naive and definitely NOT
//! production-ready oracle for BTC/USD price.
@@ -40,15 +40,24 @@
//! one unsigned transaction floating in the network.
#![cfg_attr(not(feature = "std"), no_std)]
+use frame_system::{
+ self as system,
+ ensure_signed,
+ ensure_none,
+ offchain::{
+ AppCrypto, CreateSignedTransaction, SendUnsignedTransaction,
+ SignedPayload, SigningTypes, Signer, SubmitTransaction,
+ }
+};
use frame_support::{
debug,
dispatch::DispatchResult, decl_module, decl_storage, decl_event,
traits::Get,
weights::{SimpleDispatchInfo, MINIMUM_WEIGHT},
};
-use frame_system::{self as system, ensure_signed, ensure_none, offchain};
use sp_core::crypto::KeyTypeId;
use sp_runtime::{
+ RuntimeDebug,
offchain::{http, Duration, storage::StorageValueRef},
traits::Zero,
transaction_validity::{
@@ -56,6 +65,7 @@ use sp_runtime::{
TransactionPriority,
},
};
+use codec::{Encode, Decode};
use sp_std::vec::Vec;
use lite_json::json::JsonValue;
@@ -76,18 +86,25 @@ pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"btc!");
/// the types with this pallet-specific identifier.
pub mod crypto {
use super::KEY_TYPE;
- use sp_runtime::app_crypto::{app_crypto, sr25519};
+ use sp_runtime::{
+ app_crypto::{app_crypto, sr25519},
+ traits::Verify,
+ };
+ use sp_core::sr25519::Signature as Sr25519Signature;
app_crypto!(sr25519, KEY_TYPE);
+
+ pub struct TestAuthId;
+ impl frame_system::offchain::AppCrypto<::Signer, Sr25519Signature> for TestAuthId {
+ type RuntimeAppPublic = Public;
+ type GenericSignature = sp_core::sr25519::Signature;
+ type GenericPublic = sp_core::sr25519::Public;
+ }
}
/// This pallet's configuration trait
-pub trait Trait: frame_system::Trait {
- /// The type to sign and submit transactions.
- type SubmitSignedTransaction:
- offchain::SubmitSignedTransaction::Call>;
- /// The type to submit unsigned transactions.
- type SubmitUnsignedTransaction:
- offchain::SubmitUnsignedTransaction::Call>;
+pub trait Trait: CreateSignedTransaction> {
+ /// The identifier type for an offchain worker.
+ type AuthorityId: AppCrypto;
/// The overarching event type.
type Event: From> + Into<::Event>;
@@ -115,6 +132,21 @@ pub trait Trait: frame_system::Trait {
type UnsignedPriority: Get;
}
+/// Payload used by this example crate to hold price
+/// data required to submit a transaction.
+#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
+pub struct PricePayload {
+ block_number: BlockNumber,
+ price: u32,
+ public: Public,
+}
+
+impl SignedPayload for PricePayload {
+ fn public(&self) -> T::Public {
+ self.public.clone()
+ }
+}
+
decl_storage! {
trait Store for Module as ExampleOffchainWorker {
/// A vector of recently submitted prices.
@@ -196,6 +228,22 @@ decl_module! {
Ok(())
}
+ #[weight = SimpleDispatchInfo::FixedNormal(10_000)]
+ pub fn submit_price_unsigned_with_signed_payload(
+ origin,
+ price_payload: PricePayload,
+ _signature: T::Signature,
+ ) -> DispatchResult {
+ // 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(Default::default(), price_payload.price);
+ // now increment the block number at which we expect next unsigned transaction.
+ let current_block = >::block_number();
+ >::put(current_block + T::UnsignedInterval::get());
+ Ok(())
+ }
+
/// Offchain Worker entry point.
///
/// By implementing `fn offchain_worker` within `decl_module!` you declare a new offchain
@@ -236,7 +284,9 @@ decl_module! {
let should_send = Self::choose_transaction_type(block_number);
let res = match should_send {
TransactionType::Signed => Self::fetch_price_and_send_signed(),
- TransactionType::Unsigned => Self::fetch_price_and_send_unsigned(block_number),
+ 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 {
@@ -248,7 +298,9 @@ decl_module! {
enum TransactionType {
Signed,
- Unsigned,
+ UnsignedForAny,
+ UnsignedForAll,
+ Raw,
None,
}
@@ -311,12 +363,11 @@ impl Module {
// 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 send_signed = block_number % 2.into() == Zero::zero();
- if send_signed {
- TransactionType::Signed
- } else {
- TransactionType::Unsigned
- }
+ let transaction_type = block_number % 3.into();
+ if transaction_type == Zero::zero() { TransactionType::Signed }
+ else if transaction_type == T::BlockNumber::from(1) { TransactionType::UnsignedForAny }
+ else if transaction_type == T::BlockNumber::from(2) { TransactionType::UnsignedForAll }
+ else { TransactionType::Raw }
},
// We are in the grace period, we should not send a transaction this time.
Err(RECENTLY_SENT) => TransactionType::None,
@@ -331,44 +382,43 @@ impl Module {
/// A helper function to fetch the price and send signed transaction.
fn fetch_price_and_send_signed() -> Result<(), &'static str> {
- use system::offchain::SubmitSignedTransaction;
- // Firstly we check if there are any accounts in the local keystore that are capable of
- // signing the transaction.
- // If not it doesn't even make sense to make external HTTP requests, since we won't be able
- // to put the results back on-chain.
- if !T::SubmitSignedTransaction::can_sign() {
+ use frame_system::offchain::SendSignedTransaction;
+
+ let signer = Signer::::all_accounts();
+ if !signer.can_sign() {
return Err(
"No local accounts available. Consider adding one via `author_insertKey` RPC."
)?
}
-
// Make an external HTTP request to fetch the current price.
// Note this call will block until response is received.
let price = Self::fetch_price().map_err(|_| "Failed to fetch price")?;
- // 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.
- let call = Call::submit_price(price);
-
- // Using `SubmitSignedTransaction` associated type we create and submit a transaction
+ // 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 = T::SubmitSignedTransaction::submit_signed(call);
+ let results = signer.send_signed_transaction(
+ |_account| {
+ // Received price is wrapped into a call to `submit_price` public function of this pallet.
+ // This means that the transaction, when executed, will simply call that function passing
+ // `price` as an argument.
+ Call::submit_price(price)
+ }
+ );
+
for (acc, res) in &results {
match res {
- Ok(()) => debug::info!("[{:?}] Submitted price of {} cents", acc, price),
- Err(e) => debug::error!("[{:?}] Failed to submit transaction: {:?}", acc, e),
+ Ok(()) => debug::info!("[{:?}] Submitted price of {} cents", acc.id, price),
+ Err(e) => debug::error!("[{:?}] Failed to submit transaction: {:?}", acc.id, e),
}
}
Ok(())
}
- /// A helper function to fetch the price and send unsigned transaction.
- fn fetch_price_and_send_unsigned(block_number: T::BlockNumber) -> Result<(), &'static str> {
- use system::offchain::SubmitUnsignedTransaction;
+ /// 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();
@@ -385,14 +435,101 @@ impl Module {
// passing `price` as an argument.
let call = Call::submit_price_unsigned(block_number, price);
- // Now let's create an unsigned transaction out of this call and submit it to the pool.
+ // 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.
- T::SubmitUnsignedTransaction::submit_unsigned(call)
- .map_err(|()| "Unable to submit unsigned transaction.".into())
+ //
+ 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")?;
+
+ // 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 with a signed payload
+ SubmitTransaction::>::submit_unsigned_transaction(call.into())
+ .map_err(|()| "Unable to submit unsigned transaction.")?;
+
+ // -- 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(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")?;
+
+ // 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 with a signed payload
+ SubmitTransaction::>::submit_unsigned_transaction(call.into())
+ .map_err(|()| "Unable to submit unsigned transaction.")?;
+
+ // -- 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(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.
@@ -507,6 +644,58 @@ impl Module {
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()
+ }
}
#[allow(deprecated)] // ValidateUnsigned
@@ -523,54 +712,16 @@ impl frame_support::unsigned::ValidateUnsigned for Module {
call: &Self::Call,
) -> TransactionValidity {
// Firstly let's check that we call the right function.
- if let Call::submit_price_unsigned(block_number, new_price) = call {
- // 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();
+ if let Call::submit_price_unsigned_with_signed_payload(
+ ref payload, ref signature
+ ) = call {
+ let signature_valid = SignedPayload::::verify::(payload, signature.clone());
+ if !signature_valid {
+ return InvalidTransaction::BadProof.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 to make sure 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()
+ Self::validate_transaction_parameters(&payload.block_number, &payload.price)
+ } else if let Call::submit_price_unsigned(block_number, new_price) = call {
+ Self::validate_transaction_parameters(block_number, new_price)
} else {
InvalidTransaction::Call.into()
}
diff --git a/frame/example-offchain-worker/src/tests.rs b/frame/example-offchain-worker/src/tests.rs
index 279de7ef4a382..aebcbde451b15 100644
--- a/frame/example-offchain-worker/src/tests.rs
+++ b/frame/example-offchain-worker/src/tests.rs
@@ -16,7 +16,7 @@
use crate::*;
-use codec::Decode;
+use codec::{Encode, Decode};
use frame_support::{
assert_ok, impl_outer_origin, parameter_types,
weights::Weight,
@@ -24,13 +24,17 @@ use frame_support::{
use sp_core::{
H256,
offchain::{OffchainExt, TransactionPoolExt, testing},
+ sr25519::Signature,
testing::KeyStore,
traits::KeystoreExt,
};
use sp_runtime::{
Perbill, RuntimeAppPublic,
testing::{Header, TestXt},
- traits::{BlakeTwo256, IdentityLookup, Extrinsic as ExtrinsicsT},
+ traits::{
+ BlakeTwo256, IdentityLookup, Extrinsic as ExtrinsicT,
+ IdentifyAccount, Verify,
+ },
};
impl_outer_origin! {
@@ -40,7 +44,7 @@ impl_outer_origin! {
// For testing the module, we construct most of a mock runtime. This means
// first constructing a configuration type (`Test`) which `impl`s each of the
// configuration traits of modules we want to use.
-#[derive(Clone, Eq, PartialEq)]
+#[derive(Clone, Eq, PartialEq, Encode, Decode)]
pub struct Test;
parameter_types! {
pub const BlockHashCount: u64 = 250;
@@ -72,22 +76,29 @@ impl frame_system::Trait for Test {
}
type Extrinsic = TestXt, ()>;
-type SubmitTransaction = frame_system::offchain::TransactionSubmitter<
- crypto::Public,
- Test,
- Extrinsic
->;
-
-impl frame_system::offchain::CreateTransaction for Test {
- type Public = sp_core::sr25519::Public;
- type Signature = sp_core::sr25519::Signature;
-
- fn create_transaction>(
- call: ::Call,
- _public: Self::Public,
- _account: ::AccountId,
- nonce: ::Index,
- ) -> Option<(::Call, ::SignaturePayload)> {
+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
+ Call: From,
+{
+ type OverarchingCall = Call;
+ type Extrinsic = Extrinsic;
+}
+
+impl frame_system::offchain::CreateSignedTransaction for Test where
+ Call: From,
+{
+ fn create_transaction>(
+ call: Call,
+ _public: ::Signer,
+ _account: AccountId,
+ nonce: u64,
+ ) -> Option<(Call, ::SignaturePayload)> {
Some((call, (nonce, ())))
}
}
@@ -100,9 +111,8 @@ parameter_types! {
impl Trait for Test {
type Event = ();
+ type AuthorityId = crypto::TestAuthId;
type Call = Call;
- type SubmitSignedTransaction = SubmitTransaction;
- type SubmitUnsignedTransaction = SubmitTransaction;
type GracePeriod = GracePeriod;
type UnsignedInterval = UnsignedInterval;
type UnsignedPriority = UnsignedPriority;
@@ -172,18 +182,128 @@ fn should_submit_signed_transaction_on_chain() {
}
#[test]
-fn should_submit_unsigned_transaction_on_chain() {
+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 = KeyStore::new();
+
+ keystore.write().sr25519_generate_new(
+ crate::crypto::Public::ID,
+ Some(&format!("{}/hunter1", PHRASE))
+ ).unwrap();
+
let mut t = sp_io::TestExternalities::default();
t.register_extension(OffchainExt::new(offchain));
t.register_extension(TransactionPoolExt::new(pool));
+ t.register_extension(KeystoreExt(keystore.clone()));
+
+ price_oracle_response(&mut offchain_state.write());
+
+ let public_key = keystore.read()
+ .sr25519_public_keys(crate::crypto::Public::ID)
+ .get(0)
+ .unwrap()
+ .clone();
+
+ 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 Call::submit_price_unsigned_with_signed_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 = KeyStore::new();
+
+ keystore.write().sr25519_generate_new(
+ crate::crypto::Public::ID,
+ Some(&format!("{}/hunter1", PHRASE))
+ ).unwrap();
+
+ let mut t = sp_io::TestExternalities::default();
+ t.register_extension(OffchainExt::new(offchain));
+ t.register_extension(TransactionPoolExt::new(pool));
+ t.register_extension(KeystoreExt(keystore.clone()));
+
+ price_oracle_response(&mut offchain_state.write());
+
+ let public_key = keystore.read()
+ .sr25519_public_keys(crate::crypto::Public::ID)
+ .get(0)
+ .unwrap()
+ .clone();
+
+ 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 Call::submit_price_unsigned_with_signed_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 = KeyStore::new();
+
+ let mut t = sp_io::TestExternalities::default();
+ t.register_extension(OffchainExt::new(offchain));
+ t.register_extension(TransactionPoolExt::new(pool));
+ t.register_extension(KeystoreExt(keystore));
price_oracle_response(&mut offchain_state.write());
t.execute_with(|| {
// when
- Example::fetch_price_and_send_unsigned(1).unwrap();
+ 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());
diff --git a/frame/im-online/src/lib.rs b/frame/im-online/src/lib.rs
index 1137fc2699fe3..813b11bbc9360 100644
--- a/frame/im-online/src/lib.rs
+++ b/frame/im-online/src/lib.rs
@@ -98,7 +98,10 @@ use frame_support::{
weights::{SimpleDispatchInfo, MINIMUM_WEIGHT},
};
use frame_system::{self as system, ensure_none};
-use frame_system::offchain::SubmitUnsignedTransaction;
+use frame_system::offchain::{
+ SendTransactionTypes,
+ SubmitTransaction,
+};
pub mod sr25519 {
mod app_sr25519 {
@@ -221,19 +224,13 @@ pub struct Heartbeat
pub authority_index: AuthIndex,
}
-pub trait Trait: frame_system::Trait + pallet_session::historical::Trait {
+pub trait Trait: SendTransactionTypes> + pallet_session::historical::Trait {
/// The identifier type for an authority.
type AuthorityId: Member + Parameter + RuntimeAppPublic + Default + Ord;
/// The overarching event type.
type Event: From> + Into<::Event>;
- /// A dispatchable call type.
- type Call: From>;
-
- /// A transaction submitter.
- type SubmitTransaction: SubmitUnsignedTransaction::Call>;
-
/// An expected duration of the session.
///
/// This parameter is used to determine the longevity of `heartbeat` transaction
@@ -444,6 +441,7 @@ impl Module {
}
let session_index = >::current_index();
+
Ok(Self::local_authority_keys()
.map(move |(authority_index, key)|
Self::send_single_heartbeat(authority_index, key, session_index, block_number)
@@ -467,7 +465,9 @@ impl Module {
session_index,
authority_index,
};
+
let signature = key.sign(&heartbeat_data.encode()).ok_or(OffchainErr::FailedSigning)?;
+
Ok(Call::heartbeat(heartbeat_data, signature))
};
@@ -492,7 +492,7 @@ impl Module {
call,
);
- T::SubmitTransaction::submit_unsigned(call)
+ SubmitTransaction::>::submit_unsigned_transaction(call.into())
.map_err(|_| OffchainErr::SubmitTransaction)?;
Ok(())
@@ -501,9 +501,18 @@ impl Module {
}
fn local_authority_keys() -> impl Iterator {
- // we run only when a local authority key is configured
+ // on-chain storage
+ //
+ // At index `idx`:
+ // 1. A (ImOnline) public key to be used by a validator at index `idx` to send im-online
+ // heartbeats.
let authorities = Keys::::get();
+
+ // local keystore
+ //
+ // All `ImOnline` public (+private) keys currently in the local keystore.
let mut local_keys = T::AuthorityId::all();
+
local_keys.sort();
authorities.into_iter()
@@ -565,6 +574,11 @@ impl Module {
Keys::::put(keys);
}
}
+
+ #[cfg(test)]
+ fn set_keys(keys: Vec) {
+ Keys::::put(&keys)
+ }
}
impl sp_runtime::BoundToRuntimeAppPublic for Module {
diff --git a/frame/im-online/src/mock.rs b/frame/im-online/src/mock.rs
index d620bb51b7436..e9b5ef95ef4a0 100644
--- a/frame/im-online/src/mock.rs
+++ b/frame/im-online/src/mock.rs
@@ -27,7 +27,6 @@ use sp_runtime::testing::{Header, UintAuthorityId, TestXt};
use sp_runtime::traits::{IdentityLookup, BlakeTwo256, ConvertInto};
use sp_core::H256;
use frame_support::{impl_outer_origin, impl_outer_dispatch, parameter_types, weights::Weight};
-
use frame_system as system;
impl_outer_origin!{
pub enum Origin for Runtime {}
@@ -40,7 +39,11 @@ impl_outer_dispatch! {
}
thread_local! {
- pub static VALIDATORS: RefCell